2 * Copyright 2000-2009 JetBrains s.r.o.
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
16 package org.jetbrains.idea.svn.mergeinfo;
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;
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;
39 private boolean myMixedRevisionsFound;
41 // revision in trunk -> whether merged into branch
42 private final Map<Long, SvnMergeInfoCache.MergeCheckResult> myAlreadyCalculatedMap;
43 private final Object myCalculatedLock = new Object();
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;
52 private SvnMergeInfoCache.CopyRevison myCopyRevison;
54 public BranchInfo(final SvnVcs vcs, final String repositoryRoot, final String branchUrl, final String trunkUrl,
55 final String trunkCorrected, final SVNWCClient client) {
57 myRepositoryRoot = repositoryRoot;
58 myBranchUrl = branchUrl;
59 myTrunkUrl = trunkUrl;
60 myTrunkCorrected = trunkCorrected;
63 myPathMergedMap = new HashMap<String, Set<Long>>();
64 myNonInheritablePathMergedMap = new HashMap<String, Set<Long>>();
66 myAlreadyCalculatedMap = new HashMap<Long, SvnMergeInfoCache.MergeCheckResult>();
69 private long calculateCopyRevision(final String branchPath) {
70 if (myCopyRevison != null && Comparing.equal(myCopyRevison.getPath(), branchPath)) {
71 return myCopyRevison.getRevision();
73 myCopyRevison = new SvnMergeInfoCache.CopyRevison(myVcs, branchPath, myRepositoryRoot, myBranchUrl, myTrunkUrl);
78 myPathMergedMap.clear();
79 synchronized (myCalculatedLock) {
80 myAlreadyCalculatedMap.clear();
82 myMixedRevisionsFound = false;
85 public void halfClear(final long listNumber) {
86 myPathMergedMap.clear();
87 synchronized (myCalculatedLock) {
88 myAlreadyCalculatedMap.remove(listNumber);
90 myMixedRevisionsFound = false;
93 public MergeinfoCached getCached() {
94 synchronized (myCalculatedLock) {
96 if (myCopyRevison != null && myCopyRevison.getRevision() != -1) {
97 revision = myCopyRevison.getRevision();
101 return new MergeinfoCached(Collections.unmodifiableMap(myAlreadyCalculatedMap), revision);
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;
112 final SvnMergeInfoCache.MergeCheckResult calculated = myAlreadyCalculatedMap.get(list.getNumber());
113 if (calculated != null) {
117 final SvnMergeInfoCache.MergeCheckResult result = checkAlive(list, branchPath);
118 myAlreadyCalculatedMap.put(list.getNumber(), result);
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;
128 final String subPathUnderBranch = SVNPathUtil.getRelativePath(myBranchUrl, info.getURL().toString());
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;
135 result.addAll(checkPaths(list.getNumber(), list.getDeletedPaths(), branchPath, subPathUnderBranch));
136 if (result.contains(SvnMergeInfoCache.MergeCheckResult.NOT_EXISTS)) {
137 return SvnMergeInfoCache.MergeCheckResult.NOT_EXISTS;
139 result.addAll(checkPaths(list.getNumber(), list.getChangedPaths(), branchPath, subPathUnderBranch));
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;
146 return SvnMergeInfoCache.MergeCheckResult.MERGED;
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);
159 final String relativeToTrunkPath = absoluteInTrunkPath.substring(myTrunkPathCorrespondingToLocalBranchPath.length());
160 final String localPathInBranch = new File(branchPath, relativeToTrunkPath).getAbsolutePath();
162 final SvnMergeInfoCache.MergeCheckResult pathResult = checkPathGoingUp(number, -1, branchPath, localPathInBranch, path, true);
163 result.add(pathResult);
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;
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;
181 final SVNInfo svnInfo = getInfo(new File(branchRootPath));
182 if (svnInfo == null || svnInfo.getRevision() == null || svnInfo.getURL() == null) {
183 return SvnMergeInfoCache.MergeCheckResult.NOT_MERGED;
186 return goUpInRepo(revisionAsked, targetRevision, svnInfo.getURL().removePathTail(), newTrunkUrl);
188 catch (SVNException e) {
190 return SvnMergeInfoCache.MergeCheckResult.NOT_MERGED;
194 return checkPathGoingUp(revisionAsked, targetRevision, branchRootPath, newPath, newTrunkUrl, false);
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));
207 final SVNPropertyData mergeinfoProperty;
209 mergeinfoProperty = myClient.doGetProperty(branchUrl, SVNProperty.MERGE_INFO, SVNRevision.UNDEFINED, SVNRevision.create(targetRevision));
211 catch (SVNException e) {
213 return SvnMergeInfoCache.MergeCheckResult.NOT_MERGED;
216 if (mergeinfoProperty == null) {
217 final String newTrunkUrl = SVNPathUtil.removeTail(trunkUrl).trim();
218 final SVNURL newBranchUrl;
220 newBranchUrl = branchUrl.removePathTail();
222 catch (SVNException e) {
224 return SvnMergeInfoCache.MergeCheckResult.NOT_MERGED;
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;
232 return goUpInRepo(revisionAsked, targetRevision, newBranchUrl, newTrunkUrl);
235 return processMergeinfoProperty(keyString, revisionAsked, mergeinfoProperty.getValue(), trunkUrl, false);
238 private SVNInfo getInfo(final File pathFile) {
240 return myClient.doInfo(pathFile, SVNRevision.WORKING);
241 } catch (SVNException e) {
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);
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()) {
256 return goUp(revisionAsked, targetRevision, branchRootPath, path, trunkUrl);
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;
266 final long actualRevision = svnInfo.getRevision().getNumber();
267 final long targetRevisionCorrected = (targetRevision == -1) ? actualRevision : targetRevision;
269 // here we know local URL and revision
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);
282 final SVNPropertyData mergeinfoProperty;
284 if (actualRevision == targetRevisionCorrected) {
286 mergeinfoProperty = myClient.doGetProperty(pathFile, SVNProperty.MERGE_INFO, SVNRevision.WORKING, SVNRevision.WORKING);
289 myMixedRevisionsFound = true;
290 mergeinfoProperty = myClient.doGetProperty(svnInfo.getURL(), SVNProperty.MERGE_INFO, SVNRevision.UNDEFINED, SVNRevision.create(targetRevisionCorrected));
293 catch (SVNException e) {
295 return SvnMergeInfoCache.MergeCheckResult.NOT_MERGED;
298 if (mergeinfoProperty == null) {
300 return goUp(revisionAsked, targetRevisionCorrected, branchRootPath, path, trunkUrl);
303 return processMergeinfoProperty(keyString, revisionAsked, mergeinfoProperty.getValue(), trunkUrl, self);
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();
312 if (valueAsString.length() == 0) {
313 myPathMergedMap.put(pathWithRevisionNumber, Collections.<Long>emptySet());
314 return SvnMergeInfoCache.MergeCheckResult.NOT_MERGED;
317 final Map<String, SVNMergeRangeList> map;
319 map = SVNMergeInfoUtil.parseMergeInfo(new StringBuffer(value.getString()), null);
321 catch (SVNException e) {
323 return SvnMergeInfoCache.MergeCheckResult.NOT_MERGED;
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>();
331 final SVNMergeRangeList rangesList = map.get(key);
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);
341 if ((isInheritable || self) && inInterval) {
345 for (long i = startRevision; i <= endRevision; i++) {
349 nonInheritableRevisions.add(i);
353 myPathMergedMap.put(pathWithRevisionNumber, revisions);
354 if (! nonInheritableRevisions.isEmpty()) {
355 myNonInheritablePathMergedMap.put(pathWithRevisionNumber, nonInheritableRevisions);
358 return SvnMergeInfoCache.MergeCheckResult.getInstance(result);
361 myPathMergedMap.put(pathWithRevisionNumber, Collections.<Long>emptySet());
362 return SvnMergeInfoCache.MergeCheckResult.NOT_MERGED;
365 public boolean isMixedRevisionsFound() {
366 return myMixedRevisionsFound;