/* * Copyright 2000-2012 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.integrate; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.progress.Task; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.vcs.actions.BackgroundTaskGroup; import com.intellij.util.concurrency.Semaphore; import org.jetbrains.annotations.CalledInAny; import org.jetbrains.annotations.CalledInAwt; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.idea.svn.NestedCopyType; import org.jetbrains.idea.svn.history.SvnChangeList; import org.tmatesoft.svn.core.SVNURL; import org.tmatesoft.svn.core.internal.util.SVNURLUtil; import java.io.File; import java.util.List; import static com.intellij.openapi.application.ApplicationManager.getApplication; import static org.jetbrains.idea.svn.SvnUtil.*; import static org.jetbrains.idea.svn.WorkingCopyFormat.ONE_DOT_EIGHT; import static org.jetbrains.idea.svn.integrate.SvnBranchPointsCalculator.WrapperInvertor; public class QuickMerge extends BackgroundTaskGroup { private static final Logger LOG = Logger.getInstance(QuickMerge.class); @NotNull private final MergeContext myMergeContext; @NotNull private final QuickMergeInteraction myInteraction; @NotNull private final Semaphore mySemaphore = new Semaphore(); public QuickMerge(@NotNull MergeContext mergeContext, @NotNull QuickMergeInteraction interaction) { super(mergeContext.getProject(), mergeContext.getTitle()); myMergeContext = mergeContext; myInteraction = interaction; } @NotNull public MergeContext getMergeContext() { return myMergeContext; } @NotNull public QuickMergeInteraction getInteraction() { return myInteraction; } @Override public void showErrors() { if (!myExceptions.isEmpty()) { myInteraction.showErrors(myMergeContext.getTitle(), myExceptions); } } @Override public void waitForTasksToFinish() { super.waitForTasksToFinish(); mySemaphore.waitFor(); } @Override public void end() { super.end(); mySemaphore.up(); } @CalledInAny public void end(@NotNull String message, boolean isError) { LOG.info((isError ? "Error: " : "Info: ") + message); clear(); getApplication().invokeLater(() -> myInteraction.showErrors(message, isError)); } public boolean is18() { return myMergeContext.getWcInfo().getFormat().isOrGreater(ONE_DOT_EIGHT); } @CalledInAwt public void execute() { FileDocumentManager.getInstance().saveAllDocuments(); mySemaphore.down(); runInEdt(() -> { if (areInSameHierarchy(createUrl(myMergeContext.getSourceUrl()), myMergeContext.getWcInfo().getUrl())) { end("Cannot merge from self", true); } else if (!hasSwitchedRoots() || myInteraction.shouldContinueSwitchedRootFound()) { runInBackground("Checking repository capabilities", indicator -> { if (supportsMergeInfo()) { runInEdt(this::selectMergeVariant); } else { mergeAll(false); } }); } }); } private void selectMergeVariant() { switch (myInteraction.selectMergeVariant()) { case all: mergeAll(true); break; case showLatest: runInBackground("Loading recent " + myMergeContext.getBranchName() + " revisions", new MergeCalculatorTask(this, null, task -> runInEdt(() -> selectRevisionsToMerge(task, false)))); break; case select: runInBackground("Looking for branch origin", new LookForBranchOriginTask(this, false, copyPoint -> runInBackground("Filtering " + myMergeContext.getBranchName() + " revisions", new MergeCalculatorTask(this, copyPoint, task -> runInEdt(() -> selectRevisionsToMerge(task, true)))))); case cancel: break; } } private void selectRevisionsToMerge(@NotNull MergeCalculatorTask task, boolean allStatusesCalculated) { SelectMergeItemsResult result = myInteraction.selectMergeItems(task.getChangeLists(), task.getMergeChecker(), allStatusesCalculated, task.areAllListsLoaded()); switch (result.getResultCode()) { case all: mergeAll(true); break; case select: case showLatest: merge(result.getSelectedLists()); break; case cancel: break; } } private void mergeAll(boolean supportsMergeInfo) { // merge info is not supported - branch copy point is used to make first sync merge successful (without unnecessary tree conflicts) // merge info is supported and svn client < 1.8 - branch copy point is used to determine if sync or reintegrate merge should be performed // merge info is supported and svn client >= 1.8 - branch copy point is not used - svn automatically detects if reintegrate is necessary if (supportsMergeInfo && is18()) { runInEdt(() -> checkReintegrateIsAllowedAndMergeAll(null, true)); } else { runInBackground("Looking for branch origin", new LookForBranchOriginTask(this, true, copyPoint -> runInEdt(() -> checkReintegrateIsAllowedAndMergeAll(copyPoint, supportsMergeInfo)))); } } private void checkReintegrateIsAllowedAndMergeAll(@Nullable WrapperInvertor copyPoint, boolean supportsMergeInfo) { boolean reintegrate = copyPoint != null && copyPoint.isInvertedSense(); if (!reintegrate || myInteraction.shouldReintegrate(copyPoint.inverted().getTarget())) { MergerFactory mergerFactory = createMergeAllFactory(reintegrate, copyPoint, supportsMergeInfo); String title = "Merging all from " + myMergeContext.getBranchName() + (reintegrate ? " (reintegrate)" : ""); merge(title, mergerFactory, null); } } private void merge(@NotNull List<SvnChangeList> changeLists) { if (!changeLists.isEmpty()) { ChangeListsMergerFactory mergerFactory = new ChangeListsMergerFactory(changeLists, false, false, true); merge(myMergeContext.getTitle(), mergerFactory, changeLists); } } private void merge(@NotNull String title, @NotNull MergerFactory mergerFactory, @Nullable List<SvnChangeList> changeLists) { runInEdt(new LocalChangesPromptTask(this, changeLists, () -> runInEdt(new MergeTask(this, () -> newIntegrateTask(title, mergerFactory).queue())))); } @NotNull private Task newIntegrateTask(@NotNull String title, @NotNull MergerFactory mergerFactory) { return new SvnIntegrateChangesTask(myMergeContext.getVcs(), new WorkingCopyInfo(myMergeContext.getWcInfo().getPath(), true), mergerFactory, parseUrl(myMergeContext.getSourceUrl()), title, false, myMergeContext.getBranchName()) { @Override public void onFinished() { super.onFinished(); mySemaphore.up(); } }; } private boolean hasSwitchedRoots() { File currentRoot = myMergeContext.getWcInfo().getRootInfo().getIoFile(); return myMergeContext.getVcs().getAllWcInfos().stream() .filter(info -> NestedCopyType.switched.equals(info.getType())) .anyMatch(info -> FileUtil.isAncestor(currentRoot, info.getRootInfo().getIoFile(), true)); } private boolean supportsMergeInfo() { return myMergeContext.getWcInfo().getFormat().supportsMergeInfo() && checkRepositoryVersion15(myMergeContext.getVcs(), myMergeContext.getSourceUrl()); } @NotNull private MergerFactory createMergeAllFactory(boolean reintegrate, @Nullable WrapperInvertor copyPoint, boolean supportsMergeInfo) { long revision = copyPoint != null ? reintegrate ? copyPoint.getWrapped().getTargetRevision() : copyPoint.getWrapped().getSourceRevision() : -1; return (vcs, target, handler, currentBranchUrl, branchName) -> new BranchMerger(vcs, currentBranchUrl, myMergeContext.getWcInfo().getPath(), handler, reintegrate, myMergeContext.getBranchName(), revision, supportsMergeInfo); } private static boolean areInSameHierarchy(@NotNull SVNURL url1, @NotNull SVNURL url2) { return SVNURLUtil.isAncestor(url1, url2) || SVNURLUtil.isAncestor(url2, url1); } }