Java tutorial
/* * 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.community.intellij.plugins.communitycase.update; import com.intellij.openapi.application.ModalityState; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.project.Project; import com.intellij.openapi.project.ex.ProjectManagerEx; import com.intellij.openapi.ui.Messages; import com.intellij.openapi.util.Key; import com.intellij.openapi.util.Ref; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vcs.AbstractVcsHelper; import com.intellij.openapi.vcs.VcsException; import com.intellij.openapi.vcs.changes.*; import com.intellij.openapi.vcs.changes.shelf.ShelveChangesManager; import com.intellij.openapi.vcs.changes.shelf.ShelvedChangeList; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; import org.community.intellij.plugins.communitycase.Branch; import org.community.intellij.plugins.communitycase.Util; import org.community.intellij.plugins.communitycase.Vcs; import org.community.intellij.plugins.communitycase.changes.ChangeUtils; import org.community.intellij.plugins.communitycase.commands.Command; import org.community.intellij.plugins.communitycase.commands.HandlerUtil; import org.community.intellij.plugins.communitycase.commands.LineHandler; import org.community.intellij.plugins.communitycase.commands.LineHandlerAdapter; import org.community.intellij.plugins.communitycase.config.VcsSettings; import org.community.intellij.plugins.communitycase.i18n.Bundle; import org.community.intellij.plugins.communitycase.rebase.RebaseUtils; import org.community.intellij.plugins.communitycase.ui.ConvertFilesDialog; import org.community.intellij.plugins.communitycase.ui.UiUtil; import java.io.File; import java.util.*; import java.util.concurrent.atomic.AtomicBoolean; /** * Abstract class that implement rebase operation for several roots based on rebase operation (for example update operation) */ public abstract class BaseRebaseProcess { /** * The logger */ private static final Logger LOG = Logger.getInstance("#" + BaseRebaseProcess.class.getName()); /** * The context project */ protected Project myProject; /** * The vcs service */ protected Vcs myVcs; /** * The exception list */ protected List<VcsException> myExceptions; /** * Copy of local change list */ private List<LocalChangeList> myListsCopy; /** * The changes sorted by root */ private final Map<VirtualFile, List<Change>> mySortedChanges = new HashMap<VirtualFile, List<Change>>(); /** * The change list manager */ private final ChangeListManagerEx myChangeManager; /** * Roots to stash */ private final HashSet<VirtualFile> myRootsToStash = new HashSet<VirtualFile>(); /** * True if the stash was created (root local variable) */ private boolean stashCreated; /** * The stash message */ private String myStashMessage; /** * Shelve manager instance */ private ShelveChangesManager myShelveManager; /** * The shelved change list (used when {@code SHELVE} policy is selected) */ private ShelvedChangeList myShelvedChangeList; /** * Contains vcs roots for which commits were skipped */ private SortedMap<VirtualFile, List<RebaseUtils.CommitInfo>> mySkippedCommits = new TreeMap<VirtualFile, List<RebaseUtils.CommitInfo>>( Util.VIRTUAL_FILE_COMPARATOR); /** * The progress indicator to use */ private ProgressIndicator myProgressIndicator; public BaseRebaseProcess(final Vcs vcs, final Project project, List<VcsException> exceptions) { myVcs = vcs; myProject = project; myExceptions = exceptions; myChangeManager = (ChangeListManagerEx) ChangeListManagerEx.getInstance(myProject); } /** * Perform rebase operation * * @param progressIndicator the progress indicator to use * @param roots the vcs roots */ public void doUpdate(ProgressIndicator progressIndicator, Set<VirtualFile> roots) { ProjectManagerEx projectManager = ProjectManagerEx.getInstanceEx(); projectManager.blockReloadingProjectOnExternalChanges(); this.myProgressIndicator = progressIndicator; try { if (areRootsUnderRebase(roots)) return; if (!saveProjectChangesBeforeUpdate()) return; try { for (final VirtualFile root : roots) { List<RebaseUtils.CommitInfo> skippedCommits = null; try { // check if there is a remote for the branch final Branch branch = Branch.current(myProject, root); if (branch == null) { continue; } final String value = branch.getTrackedRemoteName(myProject, root); if (value == null || value.length() == 0) { continue; } final Ref<Boolean> cancelled = new Ref<Boolean>(false); final Ref<Throwable> ex = new Ref<Throwable>(); saveRootChangesBeforeUpdate(root); boolean hadAbortErrors = false; try { markStart(root); try { LineHandler h = makeStartHandler(root); RebaseConflictDetector rebaseConflictDetector = new RebaseConflictDetector(); h.addLineListener(rebaseConflictDetector); try { HandlerUtil.doSynchronouslyWithExceptions(h, progressIndicator, HandlerUtil.formatOperationName("Updating", root)); } finally { if (!rebaseConflictDetector.isRebaseConflict()) { myExceptions.addAll(h.errors()); } cleanupHandler(root, h); } while (rebaseConflictDetector.isRebaseConflict() && !cancelled.get() && !hadAbortErrors) { mergeFiles(root, cancelled, ex, true); //noinspection ThrowableResultOfMethodCallIgnored if (ex.get() != null) { //noinspection ThrowableResultOfMethodCallIgnored throw Util.rethrowVcsException(ex.get()); } checkLocallyModified(root, cancelled, ex); //noinspection ThrowableResultOfMethodCallIgnored if (ex.get() != null) { //noinspection ThrowableResultOfMethodCallIgnored throw Util.rethrowVcsException(ex.get()); } if (cancelled.get()) { break; } Collection<VcsException> exceptions = doRebase(progressIndicator, root, rebaseConflictDetector, "--continue"); while (rebaseConflictDetector.isNoChange() && !hasAbortExceptions(exceptions)) { if (skippedCommits == null) { skippedCommits = new ArrayList<RebaseUtils.CommitInfo>(); mySkippedCommits.put(root, skippedCommits); } skippedCommits.add(RebaseUtils.getCurrentRebaseCommit(root)); exceptions = doRebase(progressIndicator, root, rebaseConflictDetector, "--skip"); } hadAbortErrors = hasAbortExceptions(exceptions); } if (cancelled.get() || hadAbortErrors) { //noinspection ThrowableInstanceNeverThrown myExceptions.add(new VcsException( "The update process was " + (hadAbortErrors ? "aborted" : "cancelled") + " for " + root.getPresentableUrl())); doRebase(progressIndicator, root, rebaseConflictDetector, "--abort"); progressIndicator.setText2("Refreshing files for the root " + root.getPath()); root.refresh(false, true); } } finally { markEnd(root, cancelled.get()); } } finally { restoreRootChangesAfterUpdate(root, cancelled); } } catch (VcsException ex) { myExceptions.add(ex); } } } finally { restoreProjectChangesAfterUpdate(); } } finally { projectManager.unblockReloadingProjectOnExternalChanges(); } } /** * Check if the exceptions should cause an abort for the rebase process * * @param exceptions the exceptions to check (it should be result of single operation) * @return true if rebase process should be aborted */ private static boolean hasAbortExceptions(Collection<VcsException> exceptions) { if (exceptions.size() > 1) { return true; } if (exceptions.size() == 1) { @SuppressWarnings({ "ThrowableResultOfMethodCallIgnored" }) final VcsException ex = exceptions.iterator().next(); return !ex.getMessage().startsWith("Failed to merge in the changes"); } return false; } /** * Restore project changes after update */ private void restoreProjectChangesAfterUpdate() { if (mySkippedCommits.size() > 0) { SkippedCommits.showSkipped(myProject, mySkippedCommits); } if (getUpdatePolicy() == VcsSettings.UpdateChangesPolicy.SHELVE) { if (myShelvedChangeList != null) { myProgressIndicator.setText(Bundle.getString("update.unshelving.changes")); //StashUtils.doSystemUnshelve(myProject, myShelvedChangeList, myShelveManager, myChangeManager, myExceptions); //todo wc unshelve changes here } } // Move files back to theirs change lists if (getUpdatePolicy() == VcsSettings.UpdateChangesPolicy.SHELVE || getUpdatePolicy() == VcsSettings.UpdateChangesPolicy.STASH) { VcsDirtyScopeManager m = VcsDirtyScopeManager.getInstance(myProject); final boolean isStash = getUpdatePolicy() == VcsSettings.UpdateChangesPolicy.STASH; HashSet<File> filesToRefresh = isStash ? new HashSet<File>() : null; for (LocalChangeList changeList : myListsCopy) { for (Change c : changeList.getChanges()) { ContentRevision after = c.getAfterRevision(); if (after != null) { m.fileDirty(after.getFile()); if (isStash) { filesToRefresh.add(after.getFile().getIOFile()); } } ContentRevision before = c.getBeforeRevision(); if (before != null) { m.fileDirty(before.getFile()); if (isStash) { filesToRefresh.add(before.getFile().getIOFile()); } } } } if (isStash) { LocalFileSystem.getInstance().refreshIoFiles(filesToRefresh); } com.intellij.util.ui.UIUtil.invokeLaterIfNeeded(new Runnable() { public void run() { myChangeManager.invokeAfterUpdate(new Runnable() { public void run() { for (LocalChangeList changeList : myListsCopy) { final Collection<Change> changes = changeList.getChanges(); if (!changes.isEmpty()) { LOG.debug("After restoring files: moving " + changes.size() + " changes to '" + changeList.getName() + "'"); myChangeManager.moveChangesTo(changeList, changes.toArray(new Change[changes.size()])); } } } }, InvokeAfterUpdateMode.BACKGROUND_NOT_CANCELLABLE, Bundle.getString("update.restoring.change.lists"), ModalityState.NON_MODAL); } }); } } /** * Restore per-root changes after update * * @param root the just updated root * @param cancelled */ private void restoreRootChangesAfterUpdate(VirtualFile root, Ref<Boolean> cancelled) { final Ref<Throwable> ex = new Ref<Throwable>(); if (new File(root.getPath(), "MERGE_HEAD").exists()) { // in case of unfinished merge offer direct merging mergeFiles(root, cancelled, ex, false); //noinspection ThrowableResultOfMethodCallIgnored if (ex.get() != null) { //noinspection ThrowableResultOfMethodCallIgnored myExceptions.add(Util.rethrowVcsException(ex.get())); } } if (stashCreated && getUpdatePolicy() == VcsSettings.UpdateChangesPolicy.STASH) { myProgressIndicator.setText(HandlerUtil.formatOperationName("Unstashing changes to", root)); //unstash(root); // after unstash, offer reverse merge mergeFiles(root, cancelled, ex, true); //noinspection ThrowableResultOfMethodCallIgnored if (ex.get() != null) { //noinspection ThrowableResultOfMethodCallIgnored myExceptions.add(Util.rethrowVcsException(ex.get())); } } } /** * Save per-root changes before update * * @param root the root to save changes for * @throws VcsException if there is a problem with saving changes */ private void saveRootChangesBeforeUpdate(VirtualFile root) throws VcsException { if (getUpdatePolicy() == VcsSettings.UpdateChangesPolicy.STASH) { stashCreated = false; if (myRootsToStash.contains(root)) { myProgressIndicator.setText(HandlerUtil.formatOperationName("Stashing changes from", root)); //stashCreated = StashUtils.saveStash(myProject, root, myStashMessage); } } } /** * Do the project level work required to save the changes * * @return false, if update process needs to be aborted */ private boolean saveProjectChangesBeforeUpdate() { if (getUpdatePolicy() == VcsSettings.UpdateChangesPolicy.STASH || getUpdatePolicy() == VcsSettings.UpdateChangesPolicy.SHELVE) { myStashMessage = makeStashMessage(); myListsCopy = myChangeManager.getChangeListsCopy(); for (LocalChangeList l : myListsCopy) { final Collection<Change> changeCollection = l.getChanges(); LOG.debug("Stashing " + changeCollection.size() + " changes from '" + l.getName() + "'"); for (Change c : changeCollection) { ContentRevision after = c.getAfterRevision(); if (after != null) { VirtualFile r = Util.getRootOrNull(after.getFile()); if (r != null) { myRootsToStash.add(r); List<Change> changes = mySortedChanges.get(r); if (changes == null) { changes = new ArrayList<Change>(); mySortedChanges.put(r, changes); } changes.add(c); } } else { ContentRevision before = c.getBeforeRevision(); if (before != null) { VirtualFile r = Util.getRootOrNull(before.getFile()); if (r != null) { myRootsToStash.add(r); } } } } } } if (getUpdatePolicy() == VcsSettings.UpdateChangesPolicy.STASH) { VcsSettings settings = VcsSettings.getInstance(myProject); if (settings == null) { return false; } boolean result = ConvertFilesDialog.showDialogIfNeeded(myProject, settings, mySortedChanges, myExceptions); if (!result) { if (myExceptions.isEmpty()) { //noinspection ThrowableInstanceNeverThrown myExceptions.add(new VcsException("Conversion of line separators failed.")); } return false; } } if (getUpdatePolicy() == VcsSettings.UpdateChangesPolicy.SHELVE) { myShelveManager = ShelveChangesManager.getInstance(myProject); ArrayList<Change> changes = new ArrayList<Change>(); for (LocalChangeList l : myListsCopy) { changes.addAll(l.getChanges()); } if (changes.size() > 0) { myProgressIndicator.setText(Bundle.getString("update.shelving.changes")); //myShelvedChangeList = StashUtils.shelveChanges(myProject, myShelveManager, changes, myStashMessage, myExceptions); //todo wc shelve changes here if (myShelvedChangeList == null) { return false; } } } return true; } /** * Clean up the start handler * * @param root the root * @param h the handler */ @SuppressWarnings({ "UnusedDeclaration" }) protected void cleanupHandler(VirtualFile root, LineHandler h) { // do nothing by default } /** * Make handler that starts operation * * @param root the vcs root * @return the handler that starts rebase operation * @throws VcsException in if there is problem with running */ protected abstract LineHandler makeStartHandler(VirtualFile root) throws VcsException; /** * Unstash changes and restore them in change list * * @param root the vcs root */ /* private void unstash(VirtualFile root) { try { StashUtils.popLastStash(myProject, root); } catch (final VcsException ue) { myExceptions.add(ue); com.intellij.util.ui.UIUtil.invokeAndWaitIfNeeded(new Runnable() { public void run() { UiUtil.showOperationError(myProject, ue, "Auto-unstash"); } }); } } */ /** * Mark the start of the operation * * @param root the vcs root * @throws VcsException the exception */ protected void markStart(VirtualFile root) throws VcsException { } /** * Mark the end of the operation * * @param root the vcs operation * @param cancelled true if the operation was cancelled due to update operation */ protected void markEnd(VirtualFile root, boolean cancelled) { } /** * @return a stash message for the operation */ protected abstract String makeStashMessage(); /** * @return the policy of autosaving change */ protected abstract VcsSettings.UpdateChangesPolicy getUpdatePolicy(); /** * Check if some roots are under the rebase operation and show a message in this case * * @param roots the roots to check * @return true if some roots are being rebased */ private boolean areRootsUnderRebase(Set<VirtualFile> roots) { Set<VirtualFile> rebasingRoots = new TreeSet<VirtualFile>(Util.VIRTUAL_FILE_COMPARATOR); for (final VirtualFile root : roots) { if (RebaseUtils.isRebaseInTheProgress(root)) { rebasingRoots.add(root); } } if (!rebasingRoots.isEmpty()) { final StringBuilder files = new StringBuilder(); for (VirtualFile r : rebasingRoots) { files.append(Bundle.message("update.root.rebasing.item", r.getPresentableUrl())); //noinspection ThrowableInstanceNeverThrown myExceptions.add(new VcsException(Bundle.message("update.root.rebasing", r.getPresentableUrl()))); } com.intellij.util.ui.UIUtil.invokeAndWaitIfNeeded(new Runnable() { public void run() { Messages.showErrorDialog(myProject, Bundle.message("update.root.rebasing.message", files.toString()), Bundle.message("update.root.rebasing.title")); } }); return true; } return false; } /** * Merge files * * @param root the project root * @param cancelled the cancelled indicator * @param ex the exception holder * @param reverse if true, reverse merge provider will be used */ private void mergeFiles(final VirtualFile root, final Ref<Boolean> cancelled, final Ref<Throwable> ex, final boolean reverse) { com.intellij.util.ui.UIUtil.invokeAndWaitIfNeeded(new Runnable() { public void run() { try { List<VirtualFile> affectedFiles = ChangeUtils.unmergedFiles(myProject, root); while (affectedFiles.size() != 0) { AbstractVcsHelper.getInstance(myProject).showMergeDialog(affectedFiles, reverse ? myVcs.getReverseMergeProvider() : myVcs.getMergeProvider()); affectedFiles = ChangeUtils.unmergedFiles(myProject, root); if (affectedFiles.size() != 0) { int result = Messages.showYesNoDialog(myProject, Bundle.message("update.rebase.unmerged", StringUtil.escapeXml(root.getPresentableUrl())), Bundle.getString("update.rebase.unmerged.title"), Messages.getErrorIcon()); if (result != 0) { cancelled.set(true); return; } } } } catch (Throwable t) { ex.set(t); } } }); } /** * Check and process locally modified files * * @param root the project root * @param cancelled the cancelled indicator * @param ex the exception holder */ private void checkLocallyModified(final VirtualFile root, final Ref<Boolean> cancelled, final Ref<Throwable> ex) { com.intellij.util.ui.UIUtil.invokeAndWaitIfNeeded(new Runnable() { public void run() { try { if (!UpdateLocallyModifiedDialog.showIfNeeded(myProject, root)) { cancelled.set(true); } } catch (Throwable t) { ex.set(t); } } }); } /** * Do rebase operation as part of update operator * * @param progressIndicator the progress indicator for the update * @param root the vcs root * @param rebaseConflictDetector the detector of conflicts in rebase operation * @param action the rebase action to execute * @return collected exceptions */ private Collection<VcsException> doRebase(ProgressIndicator progressIndicator, VirtualFile root, RebaseConflictDetector rebaseConflictDetector, final String action) { LineHandler rh = new LineHandler(myProject, root, Command.REBASE); // ignore failure for abort rh.ignoreErrorCode(1); rh.addParameters(action); rebaseConflictDetector.reset(); rh.addLineListener(rebaseConflictDetector); if (!"--abort".equals(action)) { configureRebaseEditor(root, rh); } try { return HandlerUtil.doSynchronouslyWithExceptions(rh, progressIndicator, HandlerUtil.formatOperationName("Rebasing ", root)); } finally { cleanupHandler(root, rh); } } /** * Configure rebase editor * * @param root the vcs root * @param h the handler to configure */ @SuppressWarnings({ "UnusedDeclaration" }) protected void configureRebaseEditor(VirtualFile root, LineHandler h) { // do nothing by default } /** * The detector of conflict conditions for rebase operation */ static class RebaseConflictDetector extends LineHandlerAdapter { /** * The line that indicates that there is a rebase conflict. */ private final static String[] REBASE_CONFLICT_INDICATORS = { "When you have resolved this problem run \" rebase --continue\".", "Automatic cherry-pick failed. After resolving the conflicts," }; /** * The line that indicates "no change" condition. */ private static final String REBASE_NO_CHANGE_INDICATOR = "No changes - did you forget to use ' add'?"; /** * if true, the rebase conflict happened */ AtomicBoolean rebaseConflict = new AtomicBoolean(false); /** * if true, the no changes were detected in the rebase operations */ AtomicBoolean noChange = new AtomicBoolean(false); /** * Reset detector before new operation */ public void reset() { rebaseConflict.set(false); noChange.set(false); } /** * @return true if "no change" condition was detected during the operation */ public boolean isNoChange() { return noChange.get(); } /** * @return true if conflict during rebase was detected */ public boolean isRebaseConflict() { return rebaseConflict.get(); } /** * {@inheritDoc} */ @Override public void onLineAvailable(String line, Key outputType) { for (String i : REBASE_CONFLICT_INDICATORS) { if (line.startsWith(i)) { rebaseConflict.set(true); break; } } if (line.startsWith(REBASE_NO_CHANGE_INDICATOR)) { noChange.set(true); } } } }