diff --git a/platform/vcs-api/src/com/intellij/openapi/vcs/ConcurrentTasks.java b/platform/vcs-api/src/com/intellij/openapi/vcs/ConcurrentTasks.java new file mode 100644 index 000000000000..2255fd7beb96 --- /dev/null +++ b/platform/vcs-api/src/com/intellij/openapi/vcs/ConcurrentTasks.java @@ -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 { + private volatile boolean myResultKnown; + private final Semaphore mySemaphore; + private volatile T myResult; + private volatile int myCntAlive; + private final ProgressIndicator myParentIndicator; + private final Consumer>[] 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> futures = new LinkedList>(); + for (final Consumer> task : myTasks) { + if (myResultKnown) break; + final Runnable computableProxy = new Runnable() { + public void run() { + try { + task.consume(new Consumer() { + 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>... tasks) { + myParentIndicator = parentIndicator; + myTasks = tasks; + mySemaphore = new Semaphore(); + } +} diff --git a/plugins/svn4idea/src/org/jetbrains/idea/svn/dialogs/CopiesPanel.java b/plugins/svn4idea/src/org/jetbrains/idea/svn/dialogs/CopiesPanel.java index b0e488d1133b..547bba8336b7 100644 --- a/plugins/svn4idea/src/org/jetbrains/idea/svn/dialogs/CopiesPanel.java +++ b/plugins/svn4idea/src/org/jetbrains/idea/svn/dialogs/CopiesPanel.java @@ -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); } diff --git a/plugins/svn4idea/src/org/jetbrains/idea/svn/dialogs/QuickMerge.java b/plugins/svn4idea/src/org/jetbrains/idea/svn/dialogs/QuickMerge.java index 93fecb59feb4..16aa43e886ad 100644 --- a/plugins/svn4idea/src/org/jetbrains/idea/svn/dialogs/QuickMerge.java +++ b/plugins/svn4idea/src/org/jetbrains/idea/svn/dialogs/QuickMerge.java @@ -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) { diff --git a/plugins/svn4idea/src/org/jetbrains/idea/svn/dialogs/SvnBranchPointsCalculator.java b/plugins/svn4idea/src/org/jetbrains/idea/svn/dialogs/SvnBranchPointsCalculator.java index 70f46a5a3571..5080ce1dd61e 100644 --- a/plugins/svn4idea/src/org/jetbrains/idea/svn/dialogs/SvnBranchPointsCalculator.java +++ b/plugins/svn4idea/src/org/jetbrains/idea/svn/dialogs/SvnBranchPointsCalculator.java @@ -169,6 +169,10 @@ public class SvnBranchPointsCalculator { if (! excRef.isNull()) { throw excRef.get(); } + + for (String key : myForSearchMap.keySet()) { + Collections.sort((List) 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 list = (List)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(); } diff --git a/plugins/svn4idea/src/org/jetbrains/idea/svn/history/FirstInBranch.java b/plugins/svn4idea/src/org/jetbrains/idea/svn/history/FirstInBranch.java index 3a301876a849..7496e97f473d 100644 --- a/plugins/svn4idea/src/org/jetbrains/idea/svn/history/FirstInBranch.java +++ b/plugins/svn4idea/src/org/jetbrains/idea/svn/history/FirstInBranch.java @@ -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 myConsumer; - private CopyData myResult; - private final boolean myPrimary; public FirstInBranch(final SvnVcs vcs, final String repositoryRoot, final String branchUrl, final String trunkUrl, final Consumer consumer) { - this(vcs, repositoryRoot, branchUrl, trunkUrl, consumer, true); - } - - public FirstInBranch(final SvnVcs vcs, final String repositoryRoot, final String branchUrl, final String trunkUrl, final Consumer 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 tasks = + new ConcurrentTasks(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> createTask(final SVNURL branchURL) { + return new Consumer>() { + public void consume(final Consumer 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 myRevisionCandidate = new Ref(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 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)); + } + } + } + } + } } diff --git a/plugins/svn4idea/src/org/jetbrains/idea/svn/integrate/SvnIntegrateChangesTask.java b/plugins/svn4idea/src/org/jetbrains/idea/svn/integrate/SvnIntegrateChangesTask.java index a0ce583b920b..a2f81a3207e2 100644 --- a/plugins/svn4idea/src/org/jetbrains/idea/svn/integrate/SvnIntegrateChangesTask.java +++ b/plugins/svn4idea/src/org/jetbrains/idea/svn/integrate/SvnIntegrateChangesTask.java @@ -56,16 +56,20 @@ public class SvnIntegrateChangesTask extends Task.Backgroundable { private final List 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(); - - 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(); diff --git a/plugins/svn4idea/src/org/jetbrains/idea/svn/mergeinfo/BranchInfo.java b/plugins/svn4idea/src/org/jetbrains/idea/svn/mergeinfo/BranchInfo.java index 752b813a7aa9..0ffc7580dfa7 100644 --- a/plugins/svn4idea/src/org/jetbrains/idea/svn/mergeinfo/BranchInfo.java +++ b/plugins/svn4idea/src/org/jetbrains/idea/svn/mergeinfo/BranchInfo.java @@ -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();