SVN: one-click merge: correct source url (actual for the case when branch point is above the wc root) + fork-join to find copy point

This commit is contained in:
irengrig
2010-05-20 16:12:10 +04:00
parent 79262ced7f
commit 104d8c7d90
7 changed files with 243 additions and 55 deletions
@@ -0,0 +1,110 @@
/*
* Copyright 2000-2010 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 com.intellij.openapi.vcs;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.progress.EmptyProgressIndicator;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.util.Consumer;
import com.intellij.util.concurrency.Semaphore;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.Future;
// competes to calculate The Result
public class ConcurrentTasks<T> {
private volatile boolean myResultKnown;
private final Semaphore mySemaphore;
private volatile T myResult;
private volatile int myCntAlive;
private final ProgressIndicator myParentIndicator;
private final Consumer<Consumer<T>>[] myTasks;
public void compute() {
final EmptyProgressIndicator pi = new EmptyProgressIndicator() {
@Override
public void checkCanceled() {
if (myResultKnown || (myParentIndicator != null) && myParentIndicator.isCanceled()) {
super.cancel();
}
super.checkCanceled();
}
};
myCntAlive = myTasks.length;
mySemaphore.down();
final List<Future<?>> futures = new LinkedList<Future<?>>();
for (final Consumer<Consumer<T>> task : myTasks) {
if (myResultKnown) break;
final Runnable computableProxy = new Runnable() {
public void run() {
try {
task.consume(new Consumer<T>() {
public void consume(T t) {
myResult = t;
myResultKnown = true;
-- myCntAlive;
mySemaphore.up();
}
});
}
catch (ProcessCanceledException e) {
-- myCntAlive;
if (myCntAlive == 0) {
mySemaphore.up();
}
throw e;
}
}
};
final Future<?> future = ApplicationManager.getApplication().executeOnPooledThread(new Runnable() {
public void run() {
ProgressManager.getInstance().runProcess(computableProxy, pi);
}
});
futures.add(future);
}
while (true) {
mySemaphore.waitFor(1000);
if (myResultKnown) break;
pi.checkCanceled();
}
// in it possible to even interrupt() threads involved, but at the moment it's better for tasks themselves to check cancel status
for (Future<?> future : futures) {
if ((! future.isCancelled() && (! future.isDone()))) {
future.cancel(true);
}
}
}
public boolean isResultKnown() {
return myResultKnown;
}
public T getResult() {
return myResult;
}
public ConcurrentTasks(final ProgressIndicator parentIndicator, final Consumer<Consumer<T>>... tasks) {
myParentIndicator = parentIndicator;
myTasks = tasks;
mySemaphore = new Semaphore();
}
}
@@ -262,7 +262,7 @@ public class CopiesPanel {
private void mergeFrom(final WCInfo wcInfo, final VirtualFile root, final LinkLabel mergeLabel) {
SelectBranchPopup.showForBranchRoot(myProject, root, new SelectBranchPopup.BranchSelectedCallback() {
public void branchSelected(Project project, SvnBranchConfigurationNew configuration, String url, long revision) {
new QuickMerge(project, url, wcInfo, configuration.getBaseName(url)).execute();
new QuickMerge(project, url, wcInfo, configuration.getBaseName(url), configuration).execute();
}
}, "Select branch", mergeLabel);
}
@@ -34,6 +34,7 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.idea.svn.SvnVcs;
import org.jetbrains.idea.svn.actions.ChangeListsMergerFactory;
import org.jetbrains.idea.svn.branchConfig.SvnBranchConfigurationNew;
import org.jetbrains.idea.svn.history.CopyData;
import org.jetbrains.idea.svn.history.FirstInBranch;
import org.jetbrains.idea.svn.history.SvnChangeList;
@@ -47,6 +48,7 @@ import org.jetbrains.idea.svn.mergeinfo.SvnMergeInfoCache;
import org.jetbrains.idea.svn.update.UpdateEventHandler;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.SVNURL;
import org.tmatesoft.svn.core.internal.util.SVNPathUtil;
import java.io.File;
import java.util.Collections;
@@ -56,31 +58,58 @@ import java.util.List;
public class QuickMerge {
private final Project myProject;
private final String myBranchName;
private final SvnBranchConfigurationNew myConfiguration;
private final WCInfo myWcInfo;
private final String mySourceUrl;
private String mySourceUrl;
private SvnVcs myVcs;
private final String myTitle;
public QuickMerge(Project project, String sourceUrl, WCInfo wcInfo, final String branchName) {
public QuickMerge(Project project, String sourceUrl, WCInfo wcInfo, final String branchName, final SvnBranchConfigurationNew configuration) {
myProject = project;
myBranchName = branchName;
myConfiguration = configuration;
myVcs = SvnVcs.getInstance(project);
mySourceUrl = sourceUrl;
myWcInfo = wcInfo;
myTitle = "Merge from " + myBranchName;
}
private void correctSourceUrl() throws SVNException {
final SVNURL branch = myConfiguration.getWorkingBranch(myWcInfo.getUrl());
if (branch != null && (! myWcInfo.getUrl().equals(branch))) {
final String branchString = branch.toString();
if (SVNPathUtil.isAncestor(branchString, myWcInfo.getRootUrl())) {
final String subPath = SVNPathUtil.getRelativePath(branchString, myWcInfo.getRootUrl());
mySourceUrl = SVNPathUtil.append(mySourceUrl, subPath);
}
}
}
private boolean prompt(final String question) {
return Messages.showOkCancelDialog(myProject, question, myTitle, Messages.getQuestionIcon()) == 0;
}
@CalledInAwt
public void execute() {
if (mySourceUrl.equals(myWcInfo.getRootUrl())) {
if (SVNPathUtil.isAncestor(mySourceUrl, myWcInfo.getRootUrl()) || SVNPathUtil.isAncestor(myWcInfo.getRootUrl(), mySourceUrl)) {
showErrorBalloon("Cannot merge from self");
return;
}
try {
correctSourceUrl();
}
catch (SVNException e) {
showErrorBalloon(e.getMessage());
return;
}
if (! myWcInfo.getFormat().supportsMergeInfo()) {
mergeAll();
return;
}
final int result = Messages.showDialog(myProject, "Merge all?", "Merge from " + myBranchName,
final int result = Messages.showDialog(myProject, "Merge all?", myTitle,
new String[]{"Merge &all", "&Select revisions to merge", "Cancel"}, 0, Messages.getQuestionIcon());
if (result == 2) return;
if (result == 0) {
@@ -169,6 +169,10 @@ public class SvnBranchPointsCalculator {
if (! excRef.isNull()) {
throw excRef.get();
}
for (String key : myForSearchMap.keySet()) {
Collections.sort((List<String>) myForSearchMap.get(key));
}
}
public void put(final String uid, final String target, final BranchCopyData data) throws IOException {
@@ -180,7 +184,16 @@ public class SvnBranchPointsCalculator {
}
map.put(target, data);
myPersistentMap.put(uid, map);
myForSearchMap.putValue(uid, target);
if (myForSearchMap.containsKey(uid)) {
final List<String> list = (List<String>)myForSearchMap.get(uid);
final int idx = Collections.binarySearch(list, target);
if (idx < 0) {
final int insertionIdx = - idx - 1;
list.add(insertionIdx, target);
}
} else {
myForSearchMap.putValue(uid, target);
}
}
myPersistentMap.force();
}
@@ -16,6 +16,8 @@
package org.jetbrains.idea.svn.history;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.vcs.ConcurrentTasks;
import com.intellij.util.Consumer;
import org.jetbrains.idea.svn.SvnVcs;
import org.tmatesoft.svn.core.*;
@@ -27,27 +29,16 @@ import java.util.Map;
public class FirstInBranch implements Runnable {
private final SvnVcs myVcs;
private final String myRepositoryRoot;
private final String myFullBranchUrl;
private final String myFullTrunkUrl;
private final String myBranchUrl;
private final String myTrunkUrl;
private final Consumer<CopyData> myConsumer;
private CopyData myResult;
private final boolean myPrimary;
public FirstInBranch(final SvnVcs vcs, final String repositoryRoot, final String branchUrl, final String trunkUrl, final Consumer<CopyData> consumer) {
this(vcs, repositoryRoot, branchUrl, trunkUrl, consumer, true);
}
public FirstInBranch(final SvnVcs vcs, final String repositoryRoot, final String branchUrl, final String trunkUrl, final Consumer<CopyData> consumer, final boolean primary) {
myPrimary = primary;
myVcs = vcs;
myRepositoryRoot = repositoryRoot;
myConsumer = consumer;
myResult = null;
myFullBranchUrl = branchUrl;
myFullTrunkUrl = trunkUrl;
myBranchUrl = relativePath(repositoryRoot, branchUrl);
@@ -60,44 +51,84 @@ public class FirstInBranch implements Runnable {
}
public void run() {
final SVNLogClient logClient = myVcs.createLogClient();
final SVNURL branchURL;
final SVNURL trunkURL;
try {
logClient.doLog(SVNURL.parseURIEncoded(myFullBranchUrl), null, SVNRevision.UNDEFINED, SVNRevision.HEAD, SVNRevision.create(0), true, true, 0,
new ISVNLogEntryHandler() {
public void handleLogEntry(final SVNLogEntry logEntry) throws SVNException {
ProgressManager.checkCanceled();
final Map map = logEntry.getChangedPaths();
for (Object o : map.values()) {
final SVNLogEntryPath path = (SVNLogEntryPath) o;
final String localPath = path.getPath();
final String copyPath = path.getCopyPath();
if ('A' == path.getType() &&
(myBranchUrl.equals(localPath) || SVNPathUtil.isAncestor(localPath, myBranchUrl)) &&
(myTrunkUrl.equals(copyPath)) || SVNPathUtil.isAncestor(copyPath, myTrunkUrl)) {
myResult = new CopyData(path.getCopyRevision(), logEntry.getRevision(), myPrimary);
throw new MockException();
}
}
}
});
}
catch (MockException e) {
myConsumer.consume(myResult);
return;
branchURL = SVNURL.parseURIEncoded(myFullBranchUrl);
trunkURL = SVNURL.parseURIEncoded(myFullTrunkUrl);
}
catch (SVNException e) {
myConsumer.consume(myResult);
myConsumer.consume(null);
return;
}
if (myPrimary) {
new FirstInBranch(myVcs, myRepositoryRoot, myFullTrunkUrl, myFullBranchUrl, myConsumer, false).run();
final ConcurrentTasks<CopyData> tasks =
new ConcurrentTasks<CopyData>(ProgressManager.getInstance().getProgressIndicator(), createTask(branchURL), createTask(trunkURL));
tasks.compute();
if (tasks.isResultKnown()) {
myConsumer.consume(tasks.getResult());
} else {
myConsumer.consume(myResult);
myConsumer.consume(null);
}
}
private static class MockException extends RuntimeException {}
private Consumer<Consumer<CopyData>> createTask(final SVNURL branchURL) {
return new Consumer<Consumer<CopyData>>() {
public void consume(final Consumer<CopyData> copyDataConsumer) {
final SVNLogClient logClient = myVcs.createLogClient();
final long start1 = getStart(logClient, branchURL);
if (start1 > 0) {
final SVNRevision start1Rev = SVNRevision.create(start1);
try {
logClient.doLog(branchURL, null, SVNRevision.UNDEFINED, start1Rev, start1Rev, true, true, 1, new ISVNLogEntryHandler() {
public void handleLogEntry(SVNLogEntry logEntry) throws SVNException {
checkForCopy(logEntry, copyDataConsumer);
}
});
} catch (SVNException e) {
//
}
}
}
};
}
private static long getStart(final SVNLogClient logClient, final SVNURL url) {
final Ref<Long> myRevisionCandidate = new Ref<Long>(0L);
try {
logClient.doLog(url, null, SVNRevision.UNDEFINED, SVNRevision.HEAD, SVNRevision.create(0),
true, false, 0, new ISVNLogEntryHandler() {
public void handleLogEntry(SVNLogEntry logEntry) throws SVNException {
ProgressManager.checkCanceled();
myRevisionCandidate.set(logEntry.getRevision());
}
});
}
catch (SVNException e) {
//
}
return myRevisionCandidate.get();
}
private void checkForCopy(final SVNLogEntry logEntry, final Consumer<CopyData> result) {
final Map map = logEntry.getChangedPaths();
for (Object o : map.values()) {
final SVNLogEntryPath path = (SVNLogEntryPath) o;
final String localPath = path.getPath();
final String copyPath = path.getCopyPath();
if ('A' == path.getType()) {
if ((myBranchUrl.equals(localPath) || SVNPathUtil.isAncestor(localPath, myBranchUrl)) &&
(myTrunkUrl.equals(copyPath)) || SVNPathUtil.isAncestor(copyPath, myTrunkUrl)) {
result.consume(new CopyData(path.getCopyRevision(), logEntry.getRevision(), true));
} else {
if ((myBranchUrl.equals(copyPath) || SVNPathUtil.isAncestor(copyPath, myBranchUrl)) &&
(myTrunkUrl.equals(localPath)) || SVNPathUtil.isAncestor(localPath, myTrunkUrl)) {
result.consume(new CopyData(path.getCopyRevision(), logEntry.getRevision(), false));
}
}
}
}
}
}
@@ -56,16 +56,20 @@ public class SvnIntegrateChangesTask extends Task.Backgroundable {
private final List<VcsException> myExceptions;
private final UpdateEventHandler myHandler;
private final IMerger myMerger;
private final ResolveWorker myResolveWorker;
private UpdateEventHandler myHandler;
private IMerger myMerger;
private ResolveWorker myResolveWorker;
private FilePathImpl myMergeTarget;
private final String myTitle;
private final MergerFactory myMergerFactory;
private final SVNURL myCurrentBranchUrl;
private boolean myDryRun;
public SvnIntegrateChangesTask(final SvnVcs vcs, final WorkingCopyInfo info, final MergerFactory mergerFactory,
final SVNURL currentBranchUrl, final String title, final boolean dryRun) {
super(vcs.getProject(), title, true, VcsConfiguration.getInstance(vcs.getProject()).getUpdateOption());
myMergerFactory = mergerFactory;
myCurrentBranchUrl = currentBranchUrl;
myDryRun = dryRun;
myTitle = title;
@@ -76,10 +80,6 @@ public class SvnIntegrateChangesTask extends Task.Backgroundable {
myAccomulatedFiles = new UpdatedFilesReverseSide(UpdatedFiles.create());
myExceptions = new ArrayList<VcsException>();
myHandler = new IntegrateEventHandler(myVcs, ProgressManager.getInstance().getProgressIndicator());
myMerger = mergerFactory.createMerger(myVcs, new File(info.getLocalPath()), myHandler, currentBranchUrl);
myResolveWorker = new ResolveWorker(myInfo.isUnderProjectRoot(), myProject);
}
private void indicatorOnStart() {
@@ -93,6 +93,10 @@ public class SvnIntegrateChangesTask extends Task.Backgroundable {
}
public void run(@NotNull final ProgressIndicator indicator) {
myHandler = new IntegrateEventHandler(myVcs, ProgressManager.getInstance().getProgressIndicator());
myMerger = myMergerFactory.createMerger(myVcs, new File(myInfo.getLocalPath()), myHandler, myCurrentBranchUrl);
myResolveWorker = new ResolveWorker(myInfo.isUnderProjectRoot(), myProject);
BlockReloadingUtil.block();
myProjectLevelVcsManager.startBackgroundVcsOperation();
@@ -157,6 +157,7 @@ public class BranchInfo {
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();