org.community.intellij.plugins.communitycase.update.BaseRebaseProcess.java Source code

Java tutorial

Introduction

Here is the source code for org.community.intellij.plugins.communitycase.update.BaseRebaseProcess.java

Source

/*
 * 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);
            }
        }
    }
}