SVN: new Show Working Copies view, with Configure Branches from there
[idea/community.git] / plugins / svn4idea / src / org / jetbrains / idea / svn / mergeinfo / BranchInfo.java
1 /*
2  * Copyright 2000-2009 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.diagnostic.Logger;
19 import com.intellij.openapi.util.Comparing;
20 import org.jetbrains.idea.svn.SvnVcs;
21 import org.jetbrains.idea.svn.history.SvnChangeList;
22 import org.tmatesoft.svn.core.*;
23 import org.tmatesoft.svn.core.internal.util.SVNMergeInfoUtil;
24 import org.tmatesoft.svn.core.internal.util.SVNPathUtil;
25 import org.tmatesoft.svn.core.wc.SVNInfo;
26 import org.tmatesoft.svn.core.wc.SVNPropertyData;
27 import org.tmatesoft.svn.core.wc.SVNRevision;
28 import org.tmatesoft.svn.core.wc.SVNWCClient;
29
30 import java.io.File;
31 import java.util.*;
32
33 public class BranchInfo {
34   private final static Logger LOG = Logger.getInstance("#org.jetbrains.idea.svn.mergeinfo.BranchInfo");
35   // repo path in branch in format path@revision -> merged revisions
36   private final Map<String, Set<Long>> myPathMergedMap;
37   private final Map<String, Set<Long>> myNonInheritablePathMergedMap;
38
39   private boolean myMixedRevisionsFound;
40
41   // revision in trunk -> whether merged into branch
42   private final Map<Long, SvnMergeInfoCache.MergeCheckResult> myAlreadyCalculatedMap;
43   private final Object myCalculatedLock = new Object();
44
45   private final String myRepositoryRoot;
46   private final String myBranchUrl;
47   private final String myTrunkUrl;
48   private final String myTrunkCorrected;
49   private final SVNWCClient myClient;
50   private final SvnVcs myVcs;
51
52   private SvnMergeInfoCache.CopyRevison myCopyRevison;
53
54   public BranchInfo(final SvnVcs vcs, final String repositoryRoot, final String branchUrl, final String trunkUrl,
55                      final String trunkCorrected, final SVNWCClient client) {
56     myVcs = vcs;
57     myRepositoryRoot = repositoryRoot;
58     myBranchUrl = branchUrl;
59     myTrunkUrl = trunkUrl;
60     myTrunkCorrected = trunkCorrected;
61     myClient = client;
62
63     myPathMergedMap = new HashMap<String, Set<Long>>();
64     myNonInheritablePathMergedMap = new HashMap<String, Set<Long>>();
65
66     myAlreadyCalculatedMap = new HashMap<Long, SvnMergeInfoCache.MergeCheckResult>();
67   }
68
69   private long calculateCopyRevision(final String branchPath) {
70     if (myCopyRevison != null && Comparing.equal(myCopyRevison.getPath(), branchPath)) {
71       return myCopyRevison.getRevision();
72     }
73     myCopyRevison = new SvnMergeInfoCache.CopyRevison(myVcs, branchPath, myRepositoryRoot, myBranchUrl, myTrunkUrl);
74     return -1;
75   }
76
77   public void clear() {
78     myPathMergedMap.clear();
79     synchronized (myCalculatedLock) {
80       myAlreadyCalculatedMap.clear();
81     }
82     myMixedRevisionsFound = false;
83   }
84
85   public void halfClear(final long listNumber) {
86     myPathMergedMap.clear();
87     synchronized (myCalculatedLock) {
88       myAlreadyCalculatedMap.remove(listNumber);
89     }
90     myMixedRevisionsFound = false;
91   }
92
93   public MergeinfoCached getCached() {
94     synchronized (myCalculatedLock) {
95       final long revision;
96       if (myCopyRevison != null && myCopyRevison.getRevision() != -1) {
97         revision = myCopyRevison.getRevision();
98       } else {
99         revision = -1;
100       }
101       return new MergeinfoCached(Collections.unmodifiableMap(myAlreadyCalculatedMap), revision);
102     }
103   }
104
105   public SvnMergeInfoCache.MergeCheckResult checkList(final SvnChangeList list, final String branchPath) {
106     synchronized (myCalculatedLock) {
107       final long revision = calculateCopyRevision(branchPath);
108       if (revision != -1 && revision >= list.getNumber()) {
109         return SvnMergeInfoCache.MergeCheckResult.COMMON;
110       }
111
112       final SvnMergeInfoCache.MergeCheckResult calculated = myAlreadyCalculatedMap.get(list.getNumber());
113       if (calculated != null) {
114         return calculated;
115       }
116
117       final SvnMergeInfoCache.MergeCheckResult result = checkAlive(list, branchPath);
118       myAlreadyCalculatedMap.put(list.getNumber(), result);
119       return result;
120     }
121   }
122
123   private SvnMergeInfoCache.MergeCheckResult checkAlive(final SvnChangeList list, final String branchPath) {
124     final SVNInfo info = getInfo(new File(branchPath));
125     if (info == null || info.getURL() == null || (! SVNPathUtil.isAncestor(myBranchUrl, info.getURL().toString()))) {
126       return SvnMergeInfoCache.MergeCheckResult.NOT_MERGED;
127     }
128     final String subPathUnderBranch = SVNPathUtil.getRelativePath(myBranchUrl, info.getURL().toString());
129
130     final Set<SvnMergeInfoCache.MergeCheckResult> result = new HashSet<SvnMergeInfoCache.MergeCheckResult>();
131     result.addAll(checkPaths(list.getNumber(), list.getAddedPaths(), branchPath, subPathUnderBranch));
132     if (result.contains(SvnMergeInfoCache.MergeCheckResult.NOT_EXISTS)) {
133       return SvnMergeInfoCache.MergeCheckResult.NOT_EXISTS;
134     }
135     result.addAll(checkPaths(list.getNumber(), list.getDeletedPaths(), branchPath, subPathUnderBranch));
136     if (result.contains(SvnMergeInfoCache.MergeCheckResult.NOT_EXISTS)) {
137       return SvnMergeInfoCache.MergeCheckResult.NOT_EXISTS;
138     }
139     result.addAll(checkPaths(list.getNumber(), list.getChangedPaths(), branchPath, subPathUnderBranch));
140
141     if (result.contains(SvnMergeInfoCache.MergeCheckResult.NOT_EXISTS)) {
142       return SvnMergeInfoCache.MergeCheckResult.NOT_EXISTS;
143     } else if (result.contains(SvnMergeInfoCache.MergeCheckResult.NOT_MERGED)) {
144       return SvnMergeInfoCache.MergeCheckResult.NOT_MERGED;
145     }
146     return SvnMergeInfoCache.MergeCheckResult.MERGED;
147   }
148
149   private List<SvnMergeInfoCache.MergeCheckResult> checkPaths(final long number, final Collection<String> paths,
150                                                               final String branchPath, final String subPathUnderBranch) {
151     final List<SvnMergeInfoCache.MergeCheckResult> result = new ArrayList<SvnMergeInfoCache.MergeCheckResult>();
152     final String myTrunkPathCorrespondingToLocalBranchPath = SVNPathUtil.append(myTrunkCorrected, subPathUnderBranch);
153     for (String path : paths) {
154       final String absoluteInTrunkPath = SVNPathUtil.append(myRepositoryRoot, path);
155       if (! absoluteInTrunkPath.startsWith(myTrunkPathCorrespondingToLocalBranchPath)) {
156         result.add(SvnMergeInfoCache.MergeCheckResult.NOT_EXISTS);
157         return result;
158       }
159       final String relativeToTrunkPath = absoluteInTrunkPath.substring(myTrunkPathCorrespondingToLocalBranchPath.length());
160       final String localPathInBranch = new File(branchPath, relativeToTrunkPath).getAbsolutePath();
161       
162       final SvnMergeInfoCache.MergeCheckResult pathResult = checkPathGoingUp(number, -1, branchPath, localPathInBranch, path, true);
163       result.add(pathResult);
164     }
165     return result;
166   }
167
168   private SvnMergeInfoCache.MergeCheckResult goUp(final long revisionAsked, final long targetRevision, final String branchRootPath,
169                                                               final String path, final String trunkUrl) {
170     final String newTrunkUrl = SVNPathUtil.removeTail(trunkUrl).trim();
171     if (newTrunkUrl.length() == 0 || "/".equals(newTrunkUrl)) {
172       return SvnMergeInfoCache.MergeCheckResult.NOT_MERGED;
173     }
174     final String newPath = new File(path).getParent();
175     if (newPath.length() < branchRootPath.length()) {
176       // we are higher than WC root -> go into repo only
177       if (targetRevision == -1) {
178         // no paths in local copy
179         return SvnMergeInfoCache.MergeCheckResult.NOT_EXISTS;
180       }
181       final SVNInfo svnInfo = getInfo(new File(branchRootPath));
182       if (svnInfo == null || svnInfo.getRevision() == null || svnInfo.getURL() == null) {
183         return SvnMergeInfoCache.MergeCheckResult.NOT_MERGED;
184       }
185       try {
186         return goUpInRepo(revisionAsked, targetRevision, svnInfo.getURL().removePathTail(), newTrunkUrl);
187       }
188       catch (SVNException e) {
189         LOG.info(e);
190         return SvnMergeInfoCache.MergeCheckResult.NOT_MERGED;
191       }
192     }
193     
194     return checkPathGoingUp(revisionAsked, targetRevision, branchRootPath, newPath, newTrunkUrl, false);
195   }
196
197   private SvnMergeInfoCache.MergeCheckResult goUpInRepo(final long revisionAsked, final long targetRevision, final SVNURL branchUrl,
198                                                         final String trunkUrl) {
199     final String branchAsString = branchUrl.toString();
200     final String keyString = branchAsString + "@" + targetRevision;
201     final Set<Long> mergeInfo = myPathMergedMap.get(keyString);
202     if (mergeInfo != null) {
203       // take from self or first parent with info; do not go further
204       return SvnMergeInfoCache.MergeCheckResult.getInstance(mergeInfo.contains(revisionAsked));
205     }
206
207     final SVNPropertyData mergeinfoProperty;
208     try {
209         mergeinfoProperty = myClient.doGetProperty(branchUrl, SVNProperty.MERGE_INFO, SVNRevision.UNDEFINED, SVNRevision.create(targetRevision));
210     }
211     catch (SVNException e) {
212       LOG.info(e);
213       return SvnMergeInfoCache.MergeCheckResult.NOT_MERGED;
214     }
215
216     if (mergeinfoProperty == null) {
217       final String newTrunkUrl = SVNPathUtil.removeTail(trunkUrl).trim();
218       final SVNURL newBranchUrl;
219       try {
220         newBranchUrl = branchUrl.removePathTail();
221       }
222       catch (SVNException e) {
223         LOG.info(e);
224         return SvnMergeInfoCache.MergeCheckResult.NOT_MERGED;
225       }
226       final String absoluteTrunk = SVNPathUtil.append(myRepositoryRoot, newTrunkUrl);
227       if ((1 >= newTrunkUrl.length()) || (myRepositoryRoot.length() >= newBranchUrl.toString().length()) ||
228         (newBranchUrl.equals(absoluteTrunk))) {
229         return SvnMergeInfoCache.MergeCheckResult.NOT_MERGED;
230       }
231       // go up
232       return goUpInRepo(revisionAsked, targetRevision, newBranchUrl, newTrunkUrl);
233     }
234     // process
235     return processMergeinfoProperty(keyString, revisionAsked, mergeinfoProperty.getValue(), trunkUrl, false);
236   }
237
238   private SVNInfo getInfo(final File pathFile) {
239     try {
240       return myClient.doInfo(pathFile, SVNRevision.WORKING);
241     } catch (SVNException e) {
242       //
243     }
244     return null;
245   }
246
247   private SvnMergeInfoCache.MergeCheckResult checkPathGoingUp(final long revisionAsked, final long targetRevision, final String branchRootPath,
248                                                               final String path, final String trunkUrl, final boolean self) {
249     final File pathFile = new File(path);
250
251     if (targetRevision == -1) {
252       // we didn't find existing item on the path jet
253       // check whether we locally have path
254       if (! pathFile.exists()) {
255         // go into parent
256         return goUp(revisionAsked, targetRevision, branchRootPath, path, trunkUrl);
257       }
258     }
259     
260     final SVNInfo svnInfo = getInfo(pathFile);
261     if (svnInfo == null || svnInfo.getRevision() == null || svnInfo.getURL() == null) {
262       LOG.info("Svninfo for " + pathFile + " is null or not full.");
263       return SvnMergeInfoCache.MergeCheckResult.NOT_MERGED;
264     }
265
266     final long actualRevision = svnInfo.getRevision().getNumber();
267     final long targetRevisionCorrected = (targetRevision == -1) ? actualRevision : targetRevision;
268     
269     // here we know local URL and revision
270
271     // check existing info
272     final String keyString = path + "@" + targetRevisionCorrected;
273     final Set<Long> selfInfo = self ? myNonInheritablePathMergedMap.get(keyString) : null;
274     final Set<Long> mergeInfo = myPathMergedMap.get(keyString);
275     if (mergeInfo != null || selfInfo != null) {
276       final boolean merged = ((mergeInfo != null) && mergeInfo.contains(revisionAsked)) ||
277                              ((selfInfo != null) && selfInfo.contains(revisionAsked));
278       // take from self or first parent with info; do not go further 
279       return SvnMergeInfoCache.MergeCheckResult.getInstance(merged);
280     }
281
282     final SVNPropertyData mergeinfoProperty;
283     try {
284       if (actualRevision == targetRevisionCorrected) {
285         // look in WC
286         mergeinfoProperty = myClient.doGetProperty(pathFile, SVNProperty.MERGE_INFO, SVNRevision.WORKING, SVNRevision.WORKING);
287       } else {
288         // in repo
289         myMixedRevisionsFound = true;
290         mergeinfoProperty = myClient.doGetProperty(svnInfo.getURL(), SVNProperty.MERGE_INFO, SVNRevision.UNDEFINED, SVNRevision.create(targetRevisionCorrected));
291       }
292     }
293     catch (SVNException e) {
294       LOG.info(e);
295       return SvnMergeInfoCache.MergeCheckResult.NOT_MERGED;
296     }
297
298     if (mergeinfoProperty == null) {
299       // go up
300       return goUp(revisionAsked, targetRevisionCorrected, branchRootPath, path, trunkUrl);
301     }
302     // process
303     return processMergeinfoProperty(keyString, revisionAsked, mergeinfoProperty.getValue(), trunkUrl, self);
304   }
305
306   private SvnMergeInfoCache.MergeCheckResult processMergeinfoProperty(final String pathWithRevisionNumber, final long revisionAsked,
307                                                                       final SVNPropertyValue value, final String trunkRelativeUrl,
308                                                                       final boolean self) {
309     final String valueAsString = value.toString().trim();
310
311     // empty mergeinfo
312     if (valueAsString.length() == 0) {
313       myPathMergedMap.put(pathWithRevisionNumber, Collections.<Long>emptySet());
314       return SvnMergeInfoCache.MergeCheckResult.NOT_MERGED;
315     }
316
317     final Map<String, SVNMergeRangeList> map;
318     try {
319       map = SVNMergeInfoUtil.parseMergeInfo(new StringBuffer(value.getString()), null);
320     }
321     catch (SVNException e) {
322       LOG.info(e);
323       return SvnMergeInfoCache.MergeCheckResult.NOT_MERGED;
324     }
325
326     for (String key : map.keySet()) {
327       if ((key != null) && (trunkRelativeUrl.startsWith(key))) {
328         final Set<Long> revisions = new HashSet<Long>();
329         final Set<Long> nonInheritableRevisions = new HashSet<Long>();
330
331         final SVNMergeRangeList rangesList = map.get(key);
332
333         boolean result = false;
334         for (SVNMergeRange range : rangesList.getRanges()) {
335           // SVN does not include start revision in range
336           final long startRevision = range.getStartRevision() + 1;
337           final long endRevision = range.getEndRevision();
338           final boolean isInheritable = range.isInheritable();
339           final boolean inInterval = (revisionAsked >= startRevision) && (revisionAsked <= endRevision);
340
341           if ((isInheritable || self) && inInterval) {
342             result = true;
343           }
344
345           for (long i = startRevision; i <= endRevision; i++) {
346             if (isInheritable) {
347               revisions.add(i);
348             } else {
349               nonInheritableRevisions.add(i);
350             }
351           }
352         }
353         myPathMergedMap.put(pathWithRevisionNumber, revisions);
354         if (! nonInheritableRevisions.isEmpty()) {
355           myNonInheritablePathMergedMap.put(pathWithRevisionNumber, nonInheritableRevisions);
356         }
357
358         return SvnMergeInfoCache.MergeCheckResult.getInstance(result);
359       }
360     }
361     myPathMergedMap.put(pathWithRevisionNumber, Collections.<Long>emptySet());
362     return SvnMergeInfoCache.MergeCheckResult.NOT_MERGED;
363   }
364
365   public boolean isMixedRevisionsFound() {
366     return myMixedRevisionsFound;
367   }
368 }