/* * 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.integrate; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.progress.Task; import com.intellij.openapi.ui.MessageType; import com.intellij.openapi.ui.Messages; import com.intellij.openapi.util.Ref; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vcs.*; import com.intellij.openapi.vcs.changes.Change; import com.intellij.openapi.vcs.changes.ChangeListManager; import com.intellij.openapi.vcs.changes.InvokeAfterUpdateMode; import com.intellij.openapi.vcs.changes.VcsDirtyScopeManager; import com.intellij.openapi.vcs.changes.ui.CommitChangeListDialog; import com.intellij.openapi.vcs.ex.ProjectLevelVcsManagerEx; import com.intellij.openapi.vcs.ui.VcsBalloonProblemNotifier; import com.intellij.openapi.vcs.update.*; import com.intellij.util.Consumer; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.idea.svn.SvnBundle; import org.jetbrains.idea.svn.SvnChangeProvider; import org.jetbrains.idea.svn.SvnUtil; import org.jetbrains.idea.svn.SvnVcs; import org.jetbrains.idea.svn.status.Status; import org.jetbrains.idea.svn.status.StatusType; import org.jetbrains.idea.svn.update.UpdateEventHandler; import org.tmatesoft.svn.core.SVNException; import org.tmatesoft.svn.core.SVNURL; import java.io.File; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedList; import java.util.List; public class SvnIntegrateChangesTask extends Task.Backgroundable { private final ProjectLevelVcsManagerEx myProjectLevelVcsManager; private final SvnVcs myVcs; private final WorkingCopyInfo myInfo; private final UpdatedFilesReverseSide myAccomulatedFiles; private UpdatedFiles myRecentlyUpdatedFiles; private final List myExceptions; private UpdateEventHandler myHandler; private IMerger myMerger; private ResolveWorker myResolveWorker; private FilePathImpl myMergeTarget; private final String myTitle; private final String myBranchName; 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, String branchName) { super(vcs.getProject(), title, true, VcsConfiguration.getInstance(vcs.getProject()).getUpdateOption()); myMergerFactory = mergerFactory; myCurrentBranchUrl = currentBranchUrl; myDryRun = dryRun; myTitle = title; myBranchName = branchName; myProjectLevelVcsManager = ProjectLevelVcsManagerEx.getInstanceEx(myProject); myVcs = vcs; myInfo = info; myAccomulatedFiles = new UpdatedFilesReverseSide(UpdatedFiles.create()); myExceptions = new ArrayList(); myHandler = new IntegrateEventHandler(myVcs, ProgressManager.getInstance().getProgressIndicator()); myMerger = myMergerFactory.createMerger(myVcs, new File(myInfo.getLocalPath()), myHandler, myCurrentBranchUrl, myBranchName); } private void indicatorOnStart() { final ProgressIndicator ind = ProgressManager.getInstance().getProgressIndicator(); if (ind != null) { ind.setIndeterminate(true); } if (ind != null) { ind.setText(SvnBundle.message("action.Subversion.integrate.changes.progress.integrating.text")); } } public void run(@NotNull final ProgressIndicator indicator) { myHandler.setProgressIndicator(ProgressManager.getInstance().getProgressIndicator()); myResolveWorker = new ResolveWorker(myInfo.isUnderProjectRoot(), myProject); BlockReloadingUtil.block(); myProjectLevelVcsManager.startBackgroundVcsOperation(); try { myRecentlyUpdatedFiles = UpdatedFiles.create(); myHandler.setUpdatedFiles(myRecentlyUpdatedFiles); indicatorOnStart(); // try to do multiple under single progress while (true) { doMerge(); RefreshVFsSynchronously.updateAllChanged(myRecentlyUpdatedFiles); indicator.setText(VcsBundle.message("progress.text.updating.done")); if (myResolveWorker.needsInteraction(myRecentlyUpdatedFiles) || (! myMerger.hasNext()) || (! myExceptions.isEmpty()) || UpdatedFilesReverseSide.containErrors(myRecentlyUpdatedFiles)) { break; } accomulate(); } } finally { myProjectLevelVcsManager.stopBackgroundVcsOperation(); } } private void createMessage(final boolean getLatest, final boolean warning, final String firstString) { final List messages = new ArrayList(); messages.add(firstString); myMerger.getInfo(new Consumer() { public void consume(final String s) { messages.add(s); } }, getLatest); final VcsException result = new VcsException(messages); result.setIsWarning(warning); myExceptions.add(result); } private void doMerge() { try { myMerger.mergeNext(); } catch (SVNException e) { createMessage(true, false, e.getMessage()); } catch (VcsException e) { createMessage(true, false, e.getMessage()); } } public void onCancel() { try { if (myProject.isDisposed()) return; afterExecution(true); } finally { BlockReloadingUtil.unblock(); } } public void onSuccess() { try { if (myProject.isDisposed()) return; afterExecution(false); } finally { BlockReloadingUtil.unblock(); } } private void accomulate() { myAccomulatedFiles.accomulateFiles(myRecentlyUpdatedFiles, UpdatedFilesReverseSide.DuplicateLevel.DUPLICATE_ERRORS); } private void afterExecution(final boolean wasCanceled) { if (! myRecentlyUpdatedFiles.isEmpty()) { myResolveWorker.execute(myRecentlyUpdatedFiles); } final boolean haveConflicts = ResolveWorker.haveUnresolvedConflicts(myRecentlyUpdatedFiles); accomulate(); if ((! myMerger.hasNext()) || haveConflicts || (! myExceptions.isEmpty()) || myAccomulatedFiles.containErrors() || wasCanceled) { initMergeTarget(); if (myAccomulatedFiles.isEmpty() && myExceptions.isEmpty() && (myMergeTarget == null) && (! wasCanceled)) { Messages.showMessageDialog(SvnBundle.message("action.Subversion.integrate.changes.message.files.up.to.date.text"), myTitle, Messages.getInformationIcon()); } else { if (haveConflicts) { final VcsException exception = new VcsException(SvnBundle.message("svn.integrate.changelist.warning.unresolved.conflicts.text")); exception.setIsWarning(true); myExceptions.add(exception); } if (wasCanceled) { final List details = new LinkedList(); details.add("Integration was canceled"); myMerger.getSkipped(new Consumer() { public void consume(String s) { if (! StringUtil.isEmptyOrSpaces(s)) { details.add(s); } } }); final VcsException exception = new VcsException(details); exception.setIsWarning(true); myExceptions.add(exception); } finishActions(wasCanceled); } myMerger.afterProcessing(); } else { stepToNextChangeList(); } } private void finishActions(final boolean wasCanceled) { if (! wasCanceled) { if (! ApplicationManager.getApplication().isUnitTestMode() && (! myDryRun) && (myExceptions.isEmpty()) && (! myAccomulatedFiles.containErrors()) && ((! myAccomulatedFiles.isEmpty()) || (myMergeTarget != null))) { if (myInfo.isUnderProjectRoot()) { showLocalCommit(); } else { showAlienCommit(); } return; } } final Collection files = gatherChangedPaths(); VcsDirtyScopeManager.getInstance(myProject).filePathsDirty(files, null); prepareAndShowResults(); } // no remote operations private void prepareAndShowResults() { // todo unite into one window?? if (! myAccomulatedFiles.isEmpty()) { showUpdateTree(); } if (! myExceptions.isEmpty()) { AbstractVcsHelper.getInstance(myProject).showErrors(myExceptions, VcsBundle.message("message.title.vcs.update.errors", myExceptions.size())); } } private void showUpdateTree() { RestoreUpdateTree restoreUpdateTree = RestoreUpdateTree.getInstance(myProject); // action info is actually NOT used restoreUpdateTree.registerUpdateInformation(myAccomulatedFiles.getUpdatedFiles(), ActionInfo.INTEGRATE); myProjectLevelVcsManager.showUpdateProjectInfo(myAccomulatedFiles.getUpdatedFiles(), myTitle, ActionInfo.INTEGRATE, false); } private void stepToNextChangeList() { ApplicationManager.getApplication().invokeLater(new Runnable() { public void run() { ProgressManager.getInstance().run(SvnIntegrateChangesTask.this); } }); } /** * folder that is going to keep merge info record should also be changed */ @Nullable private void initMergeTarget() { final File mergeInfoHolder = myMerger.getMergeInfoHolder(); if (mergeInfoHolder != null) { final Status svnStatus = SvnUtil.getStatus(myVcs, mergeInfoHolder); if ((svnStatus != null) && (StatusType.STATUS_MODIFIED.equals(svnStatus.getPropertiesStatus()))) { myMergeTarget = FilePathImpl.create(mergeInfoHolder, mergeInfoHolder.isDirectory()); } } } private void showLocalCommit() { final Collection files = gatherChangedPaths(); // for changes to be detected, we need switch to background change list manager update thread and back to dispatch thread // so callback is used; ok to be called after VCS update markup closed: no remote operations final ChangeListManager clManager = ChangeListManager.getInstance(myProject); clManager.invokeAfterUpdate(new Runnable() { public void run() { final Collection changes = new ArrayList(); for (FilePath file : files) { final Change change = clManager.getChange(file); if (change != null) { changes.add(change); } } CommitChangeListDialog.commitChanges(myProject, changes, null, null, myMerger.getComment()); prepareAndShowResults(); } }, InvokeAfterUpdateMode.SYNCHRONOUS_CANCELLABLE, myTitle, new Consumer() { public void consume(final VcsDirtyScopeManager vcsDirtyScopeManager) { vcsDirtyScopeManager.filePathsDirty(files, null); } }, null); } private Collection gatherChangedPaths() { final Collection files = new ArrayList(); UpdateFilesHelper.iterateFileGroupFiles(myAccomulatedFiles.getUpdatedFiles(), new UpdateFilesHelper.Callback() { public void onFile(final String filePath, final String groupId) { final FilePath file = FilePathImpl.create(new File(filePath)); files.add(file); } }); if (myMergeTarget != null) { files.add(myMergeTarget); } return files; } private void showAlienCommit() { final AlienDirtyScope dirtyScope = new AlienDirtyScope(); if (myMergeTarget != null) { dirtyScope.addDir(myMergeTarget); } else { UpdateFilesHelper.iterateFileGroupFiles(myAccomulatedFiles.getUpdatedFiles(), new UpdateFilesHelper.Callback() { public void onFile(final String filePath, final String groupId) { final FilePath file = FilePathImpl.create(new File(filePath)); dirtyScope.addFile(file); } }); } new Task.Backgroundable(myVcs.getProject(), SvnBundle.message("action.Subversion.integrate.changes.collecting.changes.to.commit.task.title")) { private final GatheringChangelistBuilder changesBuilder = new GatheringChangelistBuilder(myVcs, myAccomulatedFiles); private final Ref caughtError = new Ref(); @Override public void run(@NotNull ProgressIndicator indicator) { indicator.setIndeterminate(true); if (!myVcs.getProject().isDisposed()) { final SvnChangeProvider provider = new SvnChangeProvider(myVcs); try { provider.getChanges(dirtyScope, changesBuilder, indicator, null); } catch (VcsException e) { caughtError.set(SvnBundle.message("action.Subversion.integrate.changes.error.unable.to.collect.changes.text", e.getMessage())); } } } @Override public void onSuccess() { if (!caughtError.isNull()) { VcsBalloonProblemNotifier.showOverVersionControlView(myVcs.getProject(), caughtError.get(), MessageType.ERROR); } else if (!changesBuilder.getChanges().isEmpty()) { CommitChangeListDialog .commitAlienChanges(myProject, changesBuilder.getChanges(), myVcs, myMerger.getComment(), myMerger.getComment()); } } }.queue(); } }