blob: f69aac4a4133a4f068b974191abaadd27169602b [file] [log] [blame]
/*
* Copyright 2000-2009 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.idea.svn.mergeinfo;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.util.containers.MultiMap;
import org.jetbrains.idea.svn.SvnPropertyKeys;
import org.jetbrains.idea.svn.SvnVcs;
import org.jetbrains.idea.svn.history.SvnChangeList;
import org.jetbrains.idea.svn.info.Info;
import org.jetbrains.idea.svn.properties.PropertyValue;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.SVNMergeRange;
import org.tmatesoft.svn.core.SVNMergeRangeList;
import org.tmatesoft.svn.core.SVNURL;
import org.tmatesoft.svn.core.internal.util.SVNMergeInfoUtil;
import org.tmatesoft.svn.core.internal.util.SVNPathUtil;
import org.tmatesoft.svn.core.wc.SVNRevision;
import org.tmatesoft.svn.core.wc2.SvnTarget;
import java.io.File;
import java.util.*;
public class BranchInfo {
private final static Logger LOG = Logger.getInstance("#org.jetbrains.idea.svn.mergeinfo.BranchInfo");
// repo path in branch in format path@revision -> merged revisions
private final Map<String, Set<Long>> myPathMergedMap;
private final Map<String, Set<Long>> myNonInheritablePathMergedMap;
private boolean myMixedRevisionsFound;
// revision in trunk -> whether merged into branch
private final Map<Long, SvnMergeInfoCache.MergeCheckResult> myAlreadyCalculatedMap;
private final Object myCalculatedLock = new Object();
private final String myRepositoryRoot;
private final String myBranchUrl;
private final String myTrunkUrl;
private final String myTrunkCorrected;
private final SvnVcs myVcs;
private SvnMergeInfoCache.CopyRevison myCopyRevison;
private final MultiMap<Long, String> myPartlyMerged;
public BranchInfo(final SvnVcs vcs, final String repositoryRoot, final String branchUrl, final String trunkUrl,
final String trunkCorrected) {
myVcs = vcs;
myRepositoryRoot = repositoryRoot;
myBranchUrl = branchUrl;
myTrunkUrl = trunkUrl;
myTrunkCorrected = trunkCorrected;
myPathMergedMap = new HashMap<String, Set<Long>>();
myPartlyMerged = new MultiMap<Long, String>();
myNonInheritablePathMergedMap = new HashMap<String, Set<Long>>();
myAlreadyCalculatedMap = new HashMap<Long, SvnMergeInfoCache.MergeCheckResult>();
}
private long calculateCopyRevision(final String branchPath) {
if (myCopyRevison != null && Comparing.equal(myCopyRevison.getPath(), branchPath)) {
return myCopyRevison.getRevision();
}
myCopyRevison = new SvnMergeInfoCache.CopyRevison(myVcs, branchPath, myRepositoryRoot, myBranchUrl, myTrunkUrl);
return -1;
}
public void clear() {
myPathMergedMap.clear();
synchronized (myCalculatedLock) {
myAlreadyCalculatedMap.clear();
}
myMixedRevisionsFound = false;
}
public void halfClear(final long listNumber) {
myPathMergedMap.clear();
synchronized (myCalculatedLock) {
myAlreadyCalculatedMap.remove(listNumber);
}
myMixedRevisionsFound = false;
}
public MergeinfoCached getCached() {
synchronized (myCalculatedLock) {
final long revision;
if (myCopyRevison != null && myCopyRevison.getRevision() != -1) {
revision = myCopyRevison.getRevision();
} else {
revision = -1;
}
return new MergeinfoCached(Collections.unmodifiableMap(myAlreadyCalculatedMap), revision);
}
}
public SvnMergeInfoCache.MergeCheckResult checkList(final SvnChangeList list, final String branchPath) {
synchronized (myCalculatedLock) {
final long revision = calculateCopyRevision(branchPath);
if (revision != -1 && revision >= list.getNumber()) {
return SvnMergeInfoCache.MergeCheckResult.COMMON;
}
final SvnMergeInfoCache.MergeCheckResult calculated = myAlreadyCalculatedMap.get(list.getNumber());
if (calculated != null) {
return calculated;
}
final SvnMergeInfoCache.MergeCheckResult result = checkAlive(list, branchPath);
myAlreadyCalculatedMap.put(list.getNumber(), result);
return result;
}
}
private SvnMergeInfoCache.MergeCheckResult checkAlive(final SvnChangeList list, final String branchPath) {
final Info info = getInfo(new File(branchPath));
if (info == null || info.getURL() == null || (! SVNPathUtil.isAncestor(myBranchUrl, info.getURL().toString()))) {
return SvnMergeInfoCache.MergeCheckResult.NOT_MERGED;
}
final String subPathUnderBranch = SVNPathUtil.getRelativePath(myBranchUrl, info.getURL().toString());
final MultiMap<SvnMergeInfoCache.MergeCheckResult, String> result = new MultiMap<SvnMergeInfoCache.MergeCheckResult, String>();
checkPaths(list.getNumber(), list.getAddedPaths(), branchPath, subPathUnderBranch, result);
if (result.containsKey(SvnMergeInfoCache.MergeCheckResult.NOT_EXISTS)) {
return SvnMergeInfoCache.MergeCheckResult.NOT_EXISTS;
}
checkPaths(list.getNumber(), list.getDeletedPaths(), branchPath, subPathUnderBranch, result);
if (result.containsKey(SvnMergeInfoCache.MergeCheckResult.NOT_EXISTS)) {
return SvnMergeInfoCache.MergeCheckResult.NOT_EXISTS;
}
checkPaths(list.getNumber(), list.getChangedPaths(), branchPath, subPathUnderBranch, result);
if (result.containsKey(SvnMergeInfoCache.MergeCheckResult.NOT_EXISTS)) {
return SvnMergeInfoCache.MergeCheckResult.NOT_EXISTS;
} else if (result.containsKey(SvnMergeInfoCache.MergeCheckResult.NOT_MERGED)) {
myPartlyMerged.put(list.getNumber(), result.get(SvnMergeInfoCache.MergeCheckResult.NOT_MERGED));
return SvnMergeInfoCache.MergeCheckResult.NOT_MERGED;
}
return SvnMergeInfoCache.MergeCheckResult.MERGED;
}
private void checkPaths(final long number, final Collection<String> paths, final String branchPath, final String subPathUnderBranch,
final MultiMap<SvnMergeInfoCache.MergeCheckResult, String> result) {
final String myTrunkPathCorrespondingToLocalBranchPath = SVNPathUtil.append(myTrunkCorrected, subPathUnderBranch);
for (String path : paths) {
final String absoluteInTrunkPath = SVNPathUtil.append(myRepositoryRoot, path);
if (! absoluteInTrunkPath.startsWith(myTrunkPathCorrespondingToLocalBranchPath)) {
result.putValue(SvnMergeInfoCache.MergeCheckResult.NOT_EXISTS, path);
continue;
}
final String relativeToTrunkPath = absoluteInTrunkPath.substring(myTrunkPathCorrespondingToLocalBranchPath.length());
final String localPathInBranch = new File(branchPath, relativeToTrunkPath).getAbsolutePath();
final SvnMergeInfoCache.MergeCheckResult pathResult = checkPathGoingUp(number, -1, branchPath, localPathInBranch, path, true);
result.putValue(pathResult, path);
}
}
private SvnMergeInfoCache.MergeCheckResult goUp(final long revisionAsked, final long targetRevision, final String branchRootPath,
final String path, final String trunkUrl) {
final String newTrunkUrl = SVNPathUtil.removeTail(trunkUrl).trim();
if (newTrunkUrl.length() == 0 || "/".equals(newTrunkUrl)) {
return SvnMergeInfoCache.MergeCheckResult.NOT_MERGED;
}
final String newPath = new File(path).getParent();
if (newPath.length() < branchRootPath.length()) {
// we are higher than WC root -> go into repo only
if (targetRevision == -1) {
// no paths in local copy
return SvnMergeInfoCache.MergeCheckResult.NOT_EXISTS;
}
final Info svnInfo = getInfo(new File(branchRootPath));
if (svnInfo == null || svnInfo.getRevision() == null || svnInfo.getURL() == null) {
return SvnMergeInfoCache.MergeCheckResult.NOT_MERGED;
}
try {
return goUpInRepo(revisionAsked, targetRevision, svnInfo.getURL().removePathTail(), newTrunkUrl);
}
catch (SVNException e) {
LOG.info(e);
return SvnMergeInfoCache.MergeCheckResult.NOT_MERGED;
}
}
return checkPathGoingUp(revisionAsked, targetRevision, branchRootPath, newPath, newTrunkUrl, false);
}
private SvnMergeInfoCache.MergeCheckResult goUpInRepo(final long revisionAsked, final long targetRevision, final SVNURL branchUrl,
final String trunkUrl) {
final String branchAsString = branchUrl.toString();
final String keyString = branchAsString + "@" + targetRevision;
final Set<Long> mergeInfo = myPathMergedMap.get(keyString);
if (mergeInfo != null) {
// take from self or first parent with info; do not go further
return SvnMergeInfoCache.MergeCheckResult.getInstance(mergeInfo.contains(revisionAsked));
}
final PropertyValue mergeinfoProperty;
SvnTarget target = SvnTarget.fromURL(branchUrl);
try {
mergeinfoProperty = myVcs.getFactory(target).createPropertyClient().getProperty(target, SvnPropertyKeys.MERGE_INFO, false,
SVNRevision.create(targetRevision));
}
catch (VcsException e) {
LOG.info(e);
return SvnMergeInfoCache.MergeCheckResult.NOT_MERGED;
}
if (mergeinfoProperty == null) {
final String newTrunkUrl = SVNPathUtil.removeTail(trunkUrl).trim();
final SVNURL newBranchUrl;
try {
newBranchUrl = branchUrl.removePathTail();
}
catch (SVNException e) {
LOG.info(e);
return SvnMergeInfoCache.MergeCheckResult.NOT_MERGED;
}
final String absoluteTrunk = SVNPathUtil.append(myRepositoryRoot, newTrunkUrl);
if ((1 >= newTrunkUrl.length()) || (myRepositoryRoot.length() >= newBranchUrl.toString().length()) ||
(newBranchUrl.toString().equals(absoluteTrunk))) {
return SvnMergeInfoCache.MergeCheckResult.NOT_MERGED;
}
// go up
return goUpInRepo(revisionAsked, targetRevision, newBranchUrl, newTrunkUrl);
}
// process
return processMergeinfoProperty(keyString, revisionAsked, mergeinfoProperty, trunkUrl, false);
}
private Info getInfo(final File pathFile) {
return myVcs.getInfo(pathFile);
}
private SvnMergeInfoCache.MergeCheckResult checkPathGoingUp(final long revisionAsked, final long targetRevision, final String branchRootPath,
final String path, final String trunkUrl, final boolean self) {
final File pathFile = new File(path);
if (targetRevision == -1) {
// we didn't find existing item on the path jet
// check whether we locally have path
if (! pathFile.exists()) {
// go into parent
return goUp(revisionAsked, targetRevision, branchRootPath, path, trunkUrl);
}
}
final Info svnInfo = getInfo(pathFile);
if (svnInfo == null || svnInfo.getRevision() == null || svnInfo.getURL() == null) {
LOG.info("Svninfo for " + pathFile + " is null or not full.");
return SvnMergeInfoCache.MergeCheckResult.NOT_MERGED;
}
final long actualRevision = svnInfo.getRevision().getNumber();
final long targetRevisionCorrected = (targetRevision == -1) ? actualRevision : targetRevision;
// here we know local URL and revision
// check existing info
final String keyString = path + "@" + targetRevisionCorrected;
final Set<Long> selfInfo = self ? myNonInheritablePathMergedMap.get(keyString) : null;
final Set<Long> mergeInfo = myPathMergedMap.get(keyString);
if (mergeInfo != null || selfInfo != null) {
final boolean merged = ((mergeInfo != null) && mergeInfo.contains(revisionAsked)) ||
((selfInfo != null) && selfInfo.contains(revisionAsked));
// take from self or first parent with info; do not go further
return SvnMergeInfoCache.MergeCheckResult.getInstance(merged);
}
final PropertyValue mergeinfoProperty;
try {
if (actualRevision == targetRevisionCorrected) {
// look in WC
SvnTarget target = SvnTarget.fromFile(pathFile, SVNRevision.WORKING);
mergeinfoProperty =
myVcs.getFactory(target).createPropertyClient().getProperty(target, SvnPropertyKeys.MERGE_INFO, false, SVNRevision.WORKING);
} else {
// in repo
myMixedRevisionsFound = true;
SvnTarget target = SvnTarget.fromURL(svnInfo.getURL());
mergeinfoProperty = myVcs.getFactory(target).createPropertyClient()
.getProperty(target, SvnPropertyKeys.MERGE_INFO, false, SVNRevision.create(targetRevisionCorrected));
}
}
catch (VcsException e) {
LOG.info(e);
return SvnMergeInfoCache.MergeCheckResult.NOT_MERGED;
}
if (mergeinfoProperty == null) {
// go up
return goUp(revisionAsked, targetRevisionCorrected, branchRootPath, path, trunkUrl);
}
// process
return processMergeinfoProperty(keyString, revisionAsked, mergeinfoProperty, trunkUrl, self);
}
private SvnMergeInfoCache.MergeCheckResult processMergeinfoProperty(final String pathWithRevisionNumber, final long revisionAsked,
final PropertyValue value, final String trunkRelativeUrl,
final boolean self) {
final String valueAsString = value.toString().trim();
// empty mergeinfo
if (valueAsString.length() == 0) {
myPathMergedMap.put(pathWithRevisionNumber, Collections.<Long>emptySet());
return SvnMergeInfoCache.MergeCheckResult.NOT_MERGED;
}
final Map<String, SVNMergeRangeList> map;
try {
map = SVNMergeInfoUtil.parseMergeInfo(new StringBuffer(replaceSeparators(value.toString())), null);
}
catch (SVNException e) {
LOG.info(e);
return SvnMergeInfoCache.MergeCheckResult.NOT_MERGED;
}
for (String key : map.keySet()) {
if ((key != null) && (trunkRelativeUrl.startsWith(key))) {
final Set<Long> revisions = new HashSet<Long>();
final Set<Long> nonInheritableRevisions = new HashSet<Long>();
final SVNMergeRangeList rangesList = map.get(key);
boolean result = false;
for (SVNMergeRange range : rangesList.getRanges()) {
// SVN does not include start revision in range
final long startRevision = range.getStartRevision() + 1;
final long endRevision = range.getEndRevision();
final boolean isInheritable = range.isInheritable();
final boolean inInterval = (revisionAsked >= startRevision) && (revisionAsked <= endRevision);
if ((isInheritable || self) && inInterval) {
result = true;
}
for (long i = startRevision; i <= endRevision; i++) {
if (isInheritable) {
revisions.add(i);
} else {
nonInheritableRevisions.add(i);
}
}
}
myPathMergedMap.put(pathWithRevisionNumber, revisions);
if (! nonInheritableRevisions.isEmpty()) {
myNonInheritablePathMergedMap.put(pathWithRevisionNumber, nonInheritableRevisions);
}
return SvnMergeInfoCache.MergeCheckResult.getInstance(result);
}
}
myPathMergedMap.put(pathWithRevisionNumber, Collections.<Long>emptySet());
return SvnMergeInfoCache.MergeCheckResult.NOT_MERGED;
}
private String replaceSeparators(final String s) {
return s.replace('\r', '\n').replace("\n\n", "\n");
}
public boolean isMixedRevisionsFound() {
return myMixedRevisionsFound;
}
// if nothing, maybe all not merged or merged: here only partly not merged
public Collection<String> getNotMergedPaths(final long number) {
return myPartlyMerged.get(number);
}
public static class MyMergeCheckerWrapper implements MergeChecker {
private final BranchInfo myInfo;
private final String myBranchPath;
public MyMergeCheckerWrapper(String branchPath, BranchInfo info) {
myBranchPath = branchPath;
myInfo = info;
}
public SvnMergeInfoCache.MergeCheckResult checkList(SvnChangeList list) {
return myInfo.checkList(list, myBranchPath);
}
public Collection<String> getNotMergedPaths(long number) {
return myInfo.getNotMergedPaths(number);
}
}
}