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