vcs: Refactored "AreaMap" - removed unused methods, code simplified
[idea/community.git] / plugins / svn4idea / src / org / jetbrains / idea / svn / mergeinfo / OneShotMergeInfoHelper.java
1 /*
2  * Copyright 2000-2010 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package org.jetbrains.idea.svn.mergeinfo;
17
18 import com.intellij.openapi.progress.ProgressManager;
19 import com.intellij.openapi.util.SystemInfo;
20 import com.intellij.openapi.vcs.AreaMap;
21 import com.intellij.openapi.vcs.VcsException;
22 import com.intellij.util.PairProcessor;
23 import org.jetbrains.annotations.NotNull;
24 import org.jetbrains.annotations.Nullable;
25 import org.jetbrains.idea.svn.SvnPropertyKeys;
26 import org.jetbrains.idea.svn.api.Depth;
27 import org.jetbrains.idea.svn.commandLine.SvnBindException;
28 import org.jetbrains.idea.svn.history.LogEntry;
29 import org.jetbrains.idea.svn.history.LogEntryPath;
30 import org.jetbrains.idea.svn.history.LogHierarchyNode;
31 import org.jetbrains.idea.svn.history.SvnChangeList;
32 import org.jetbrains.idea.svn.integrate.MergeContext;
33 import org.jetbrains.idea.svn.properties.PropertyConsumer;
34 import org.jetbrains.idea.svn.properties.PropertyData;
35 import org.tmatesoft.svn.core.*;
36 import org.tmatesoft.svn.core.internal.util.SVNPathUtil;
37 import org.tmatesoft.svn.core.wc.SVNRevision;
38 import org.tmatesoft.svn.core.wc2.SvnTarget;
39
40 import java.io.File;
41 import java.util.Collection;
42 import java.util.LinkedList;
43 import java.util.Map;
44 import java.util.Set;
45
46 import static com.intellij.openapi.util.io.FileUtil.getRelativePath;
47 import static com.intellij.openapi.util.io.FileUtil.toSystemIndependentName;
48 import static com.intellij.openapi.util.text.StringUtil.toUpperCase;
49 import static com.intellij.util.ObjectUtils.notNull;
50 import static com.intellij.util.containers.ContainerUtil.*;
51 import static org.jetbrains.idea.svn.SvnUtil.ensureStartSlash;
52 import static org.jetbrains.idea.svn.mergeinfo.SvnMergeInfoCache.MergeCheckResult;
53 import static org.tmatesoft.svn.core.internal.util.SVNPathUtil.isAncestor;
54
55 public class OneShotMergeInfoHelper implements MergeChecker {
56
57   @NotNull private final MergeContext myMergeContext;
58   @NotNull private final Map<Long, Collection<String>> myPartiallyMerged;
59   // subpath [file] (local) to (subpathURL - merged FROM - to ranges list)
60   @NotNull private final AreaMap<String, Map<String, SVNMergeRangeList>> myMergeInfoMap;
61   @NotNull private final Object myMergeInfoLock;
62
63   public OneShotMergeInfoHelper(@NotNull MergeContext mergeContext) {
64     myMergeContext = mergeContext;
65     myPartiallyMerged = newHashMap();
66     myMergeInfoLock = new Object();
67     // TODO: Rewrite without AreaMap usage
68     myMergeInfoMap = new AreaMap<>();
69   }
70
71   @Override
72   public void prepare() throws VcsException {
73     Depth depth = Depth.allOrEmpty(myMergeContext.getVcs().getSvnConfiguration().isCheckNestedForQuickMerge());
74     File file = myMergeContext.getWcInfo().getRootInfo().getIoFile();
75
76     myMergeContext.getVcs().getFactory(file).createPropertyClient()
77       .getProperty(SvnTarget.fromFile(file), SvnPropertyKeys.MERGE_INFO, SVNRevision.WORKING, depth, createPropertyHandler());
78   }
79
80   @Nullable
81   public Collection<String> getNotMergedPaths(@NotNull SvnChangeList changeList) {
82     return myPartiallyMerged.get(changeList.getNumber());
83   }
84
85   @NotNull
86   public MergeCheckResult checkList(@NotNull SvnChangeList changeList) {
87     Set<String> notMergedPaths = newHashSet();
88     boolean hasMergedPaths = false;
89
90     for (String path : changeList.getAffectedPaths()) {
91       //noinspection EnumSwitchStatementWhichMissesCases
92       switch (checkPath(path, changeList.getNumber())) {
93         case MERGED:
94           hasMergedPaths = true;
95           break;
96         case NOT_MERGED:
97           notMergedPaths.add(path);
98           break;
99       }
100     }
101
102     if (hasMergedPaths && !notMergedPaths.isEmpty()) {
103       myPartiallyMerged.put(changeList.getNumber(), notMergedPaths);
104     }
105
106     return notMergedPaths.isEmpty()
107            ? hasMergedPaths ? MergeCheckResult.MERGED : MergeCheckResult.NOT_EXISTS
108            : MergeCheckResult.NOT_MERGED;
109   }
110
111   @NotNull
112   public MergeCheckResult checkPath(@NotNull String repositoryRelativePath, long revisionNumber) {
113     MergeCheckResult result = MergeCheckResult.NOT_EXISTS;
114     String sourceRelativePath =
115       SVNPathUtil.getRelativePath(myMergeContext.getRepositoryRelativeSourcePath(), ensureStartSlash(repositoryRelativePath));
116
117     // TODO: SVNPathUtil.getRelativePath() is @NotNull - probably we need to check also isEmpty() here?
118     if (sourceRelativePath != null) {
119       InfoProcessor processor = new InfoProcessor(sourceRelativePath, myMergeContext.getRepositoryRelativeSourcePath(), revisionNumber);
120
121       synchronized (myMergeInfoLock) {
122         myMergeInfoMap.getSimiliar(
123           toKey(sourceRelativePath),
124           (parentUrl, childUrl) -> ".".equals(parentUrl) || isAncestor(ensureStartSlash(parentUrl), ensureStartSlash(childUrl)),
125           processor);
126       }
127
128       result = MergeCheckResult.getInstance(processor.isMerged());
129     }
130
131     return result;
132   }
133
134   private static class InfoProcessor implements PairProcessor<String, Map<String, SVNMergeRangeList>> {
135
136     @NotNull private final String myRepositoryRelativeSourcePath;
137     private boolean myIsMerged;
138     @NotNull private final String mySourceRelativePath;
139     private final long myRevisionNumber;
140
141     public InfoProcessor(@NotNull String sourceRelativePath, @NotNull String repositoryRelativeSourcePath, long revisionNumber) {
142       mySourceRelativePath = sourceRelativePath;
143       myRevisionNumber = revisionNumber;
144       myRepositoryRelativeSourcePath = ensureStartSlash(repositoryRelativeSourcePath);
145     }
146
147     public boolean isMerged() {
148       return myIsMerged;
149     }
150
151     // TODO: Try to unify with BranchInfo.processMergeinfoProperty()
152     public boolean process(@NotNull String workingCopyRelativePath, @NotNull Map<String, SVNMergeRangeList> mergedPathsMap) {
153       boolean processed = false;
154       boolean isCurrentPath = workingCopyRelativePath.equals(mySourceRelativePath);
155
156       if (mergedPathsMap.isEmpty()) {
157         myIsMerged = false;
158         processed = true;
159       }
160       else {
161         String mergedPathAffectingSourcePath =
162           find(mergedPathsMap.keySet(), path -> isAncestor(myRepositoryRelativeSourcePath, ensureStartSlash(path)));
163
164         if (mergedPathAffectingSourcePath != null) {
165           SVNMergeRangeList mergeRangeList = mergedPathsMap.get(mergedPathAffectingSourcePath);
166
167           processed = true;
168           myIsMerged = exists(mergeRangeList.getRanges(),
169                               range -> BranchInfo.isInRange(range, myRevisionNumber) && (range.isInheritable() || isCurrentPath));
170         }
171       }
172
173       return processed;
174     }
175   }
176
177   @NotNull
178   private PropertyConsumer createPropertyHandler() {
179     return new PropertyConsumer() {
180       public void handleProperty(@NotNull File path, @NotNull PropertyData property) throws SVNException {
181         String workingCopyRelativePath = getWorkingCopyRelativePath(path);
182         Map<String, SVNMergeRangeList> mergeInfo = parseMergeInfo(property);
183
184         synchronized (myMergeInfoLock) {
185           myMergeInfoMap.put(toKey(workingCopyRelativePath), mergeInfo);
186         }
187       }
188
189       public void handleProperty(SVNURL url, PropertyData property) throws SVNException {
190       }
191
192       public void handleProperty(long revision, PropertyData property) throws SVNException {
193       }
194
195       @NotNull
196       private Map<String, SVNMergeRangeList> parseMergeInfo(@NotNull PropertyData property) throws SVNException {
197         try {
198           return BranchInfo.parseMergeInfo(notNull(property.getValue()));
199         }
200         catch (SvnBindException e) {
201           throw new SVNException(SVNErrorMessage.create(SVNErrorCode.MERGE_INFO_PARSE_ERROR, e), e);
202         }
203       }
204     };
205   }
206
207   @NotNull
208   private String getWorkingCopyRelativePath(@NotNull File file) {
209     return toSystemIndependentName(notNull(getRelativePath(myMergeContext.getWcInfo().getRootInfo().getIoFile(), file)));
210   }
211
212   @NotNull
213   private static String toKey(@NotNull String path) {
214     return SystemInfo.isFileSystemCaseSensitive ? path : toUpperCase(path);
215   }
216
217   // true if errors found
218   public boolean checkListForPaths(@NotNull LogHierarchyNode node) {
219     // TODO: Such filtering logic is not clear enough so far (and probably not correct for all cases - for instance when we perform merge
220     // TODO: from branch1 to branch2 and have revision which contain merge changes from branch3 to branch1.
221     // TODO: In this case paths of child log entries will not contain neither urls from branch1 nor from branch2 - and checkEntry() method
222     // TODO: will return true => so such revision will not be used (and displayed) further.
223
224     // TODO: Why do we check entries recursively - we have a revision - set of changes in the "merge from" branch? Why do we need to check
225     // TODO: where they came from - we want avoid some circular merges or what? Does subversion itself perform such checks or not?
226     boolean isLocalChange = or(node.getChildren(), this::checkForSubtree);
227
228     return isLocalChange ||
229            checkForEntry(node.getMe(), myMergeContext.getRepositoryRelativeWorkingCopyPath(),
230                          myMergeContext.getRepositoryRelativeSourcePath());
231   }
232
233   /**
234    * TODO: Why checkForEntry() from checkListForPaths() and checkForSubtree() are called with swapped parameters.
235    */
236   // true if errors found
237   private boolean checkForSubtree(@NotNull LogHierarchyNode tree) {
238     LinkedList<LogHierarchyNode> queue = new LinkedList<>();
239     queue.addLast(tree);
240
241     while (!queue.isEmpty()) {
242       LogHierarchyNode element = queue.removeFirst();
243       ProgressManager.checkCanceled();
244
245       if (checkForEntry(element.getMe(), myMergeContext.getRepositoryRelativeSourcePath(),
246                         myMergeContext.getRepositoryRelativeWorkingCopyPath())) {
247         return true;
248       }
249       queue.addAll(element.getChildren());
250     }
251     return false;
252   }
253
254   // true if errors found
255   // checks if either some changed path is in current branch => treat as local change
256   // or if no changed paths in current branch, checks if at least one path in "merge from" branch
257   // NOTE: this fails for "merge-source" log entries from other branches - when all changed paths are from some
258   // third branch - this logic treats such log entry as local.
259   private static boolean checkForEntry(@NotNull LogEntry entry, @NotNull String localURL, @NotNull String relativeBranch) {
260     boolean atLeastOneUnderBranch = false;
261
262     for (LogEntryPath path : entry.getChangedPaths().values()) {
263       if (isAncestor(localURL, path.getPath())) {
264         return true;
265       }
266       if (!atLeastOneUnderBranch && isAncestor(relativeBranch, path.getPath())) {
267         atLeastOneUnderBranch = true;
268       }
269     }
270     return !atLeastOneUnderBranch;
271   }
272 }