From 3ee8e8adb595a64a213911ea1f2e959318673ff5 Mon Sep 17 00:00:00 2001 From: Kirill Likhodedov Date: Wed, 13 Jan 2016 19:43:01 +0300 Subject: [PATCH] Remember the ongoing rebase state and use it to resume from "continue rebase" and other actions * Keep GitRebaseParams just for parameters of the standard rebase; * Introduce separate methods for "continuing" rebase operations: abort, continue, skip. This fixes: IDEA-135299, IDEA-60413, IDEA-72282, IDEA-142648 --- plugins/git4idea/src/git4idea/GitUtil.java | 15 + .../actions/GitAbstractRebaseAction.java | 104 ++++ .../GitAbstractRebaseResumeAction.java | 96 --- .../src/git4idea/actions/GitRebaseAbort.java | 90 +-- .../git4idea/actions/GitRebaseContinue.java | 39 +- .../src/git4idea/actions/GitRebaseSkip.java | 39 +- .../src/git4idea/branch/GitRebaseParams.java | 66 +- .../git4idea/src/git4idea/commands/Git.java | 14 +- .../src/git4idea/commands/GitImpl.java | 68 ++- .../rebase/GitAbortRebaseProcess.java | 7 +- .../rebase/GitRebaseProblemDetector.java | 1 + .../src/git4idea/rebase/GitRebaseProcess.java | 562 ++++++++++-------- .../git4idea/rebase/GitRebaseResumeMode.java | 34 ++ .../src/git4idea/rebase/GitRebaseSpec.java | 224 +++++++ .../src/git4idea/rebase/GitRebaseStatus.java | 77 +++ .../src/git4idea/rebase/GitRebaseUtils.java | 94 ++- .../git4idea/rebase/GitSuccessfulRebase.java | 89 +++ .../git4idea/repo/GitRepositoryManager.java | 18 + .../git4idea/stash/GitShelveChangesSaver.java | 5 + .../git4idea/stash/GitStashChangesSaver.java | 5 + .../git4idea/rebase/GitMultiRepoRebaseTest.kt | 103 +++- .../git4idea/rebase/GitRebaseBaseTest.kt | 91 ++- .../rebase/GitSingleRepoRebaseTest.kt | 164 ++--- .../tests/git4idea/test/GitPlatformTest.kt | 1 + .../tests/git4idea/test/GitTestUtil.java | 6 +- .../git4idea/tests/git4idea/test/TestGit.kt | 34 +- 26 files changed, 1366 insertions(+), 680 deletions(-) create mode 100644 plugins/git4idea/src/git4idea/actions/GitAbstractRebaseAction.java delete mode 100644 plugins/git4idea/src/git4idea/actions/GitAbstractRebaseResumeAction.java create mode 100644 plugins/git4idea/src/git4idea/rebase/GitRebaseResumeMode.java create mode 100644 plugins/git4idea/src/git4idea/rebase/GitRebaseSpec.java create mode 100644 plugins/git4idea/src/git4idea/rebase/GitRebaseStatus.java create mode 100644 plugins/git4idea/src/git4idea/rebase/GitSuccessfulRebase.java diff --git a/plugins/git4idea/src/git4idea/GitUtil.java b/plugins/git4idea/src/git4idea/GitUtil.java index 90807660f1e3..622eb5b1200d 100644 --- a/plugins/git4idea/src/git4idea/GitUtil.java +++ b/plugins/git4idea/src/git4idea/GitUtil.java @@ -1027,4 +1027,19 @@ public class GitUtil { if (repositories.isEmpty()) return ""; return " in " + joinShortNames(repositories, limit); } + + public static void updateRepositories(@NotNull Collection repositories) { + for (GitRepository repository : repositories) { + repository.update(); + } + } + + public static boolean hasGitRepositories(@NotNull Project project) { + return !getRepositories(project).isEmpty(); + } + + @NotNull + public static Collection getRepositories(@NotNull Project project) { + return getRepositoryManager(project).getRepositories(); + } } diff --git a/plugins/git4idea/src/git4idea/actions/GitAbstractRebaseAction.java b/plugins/git4idea/src/git4idea/actions/GitAbstractRebaseAction.java new file mode 100644 index 000000000000..751186030aef --- /dev/null +++ b/plugins/git4idea/src/git4idea/actions/GitAbstractRebaseAction.java @@ -0,0 +1,104 @@ +/* + * Copyright 2000-2015 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 git4idea.actions; + +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.progress.ProgressManager; +import com.intellij.openapi.progress.Task; +import com.intellij.openapi.project.DumbAwareAction; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.util.containers.ContainerUtil; +import git4idea.rebase.GitRebaseActionDialog; +import git4idea.rebase.GitRebaseUtils; +import git4idea.repo.GitRepository; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collection; + +import static com.intellij.util.ObjectUtils.assertNotNull; +import static com.intellij.util.containers.ContainerUtil.newArrayList; +import static git4idea.GitUtil.*; + +abstract class GitAbstractRebaseAction extends DumbAwareAction { + + @Override + public void update(@NotNull AnActionEvent e) { + super.update(e); + Project project = e.getProject(); + if (project == null || !hasGitRepositories(project)) { + e.getPresentation().setVisible(false); + } + else { + e.getPresentation().setEnabledAndVisible(hasRebaseInProgress(project)); + } + } + + @Override + public final void actionPerformed(AnActionEvent e) { + final Project project = e.getRequiredData(CommonDataKeys.PROJECT); + ProgressManager progressManager = ProgressManager.getInstance(); + String progressTitle = getProgressTitle(); + if (getRepositoryManager(project).hasOngoingRebase()) { + progressManager.run(new Task.Backgroundable(project, progressTitle) { + @Override + public void run(@NotNull ProgressIndicator indicator) { + performActionForProject(project, indicator); + } + }); + } + else { + final GitRepository repositoryToOperate = chooseRepository(project, GitRebaseUtils.getRebasingRepositories(project)); + if (repositoryToOperate != null) { + progressManager.run(new Task.Backgroundable(project, progressTitle) { + @Override + public void run(@NotNull ProgressIndicator indicator) { + performActionForRepository(project, repositoryToOperate, indicator); + } + }); + } + } + } + + @NotNull + protected abstract String getProgressTitle(); + + protected abstract void performActionForProject(@NotNull Project project, @NotNull ProgressIndicator indicator); + + protected abstract void performActionForRepository(@NotNull Project project, + @NotNull GitRepository repository, + @NotNull ProgressIndicator indicator); + + private static boolean hasRebaseInProgress(@NotNull Project project) { + return !GitRebaseUtils.getRebasingRepositories(project).isEmpty(); + } + + @Nullable + private GitRepository chooseRepository(@NotNull Project project, @NotNull Collection repositories) { + GitRepository firstRepo = assertNotNull(ContainerUtil.getFirstItem(repositories)); + if (repositories.size() == 1) return firstRepo; + ArrayList roots = newArrayList(getRootsFromRepositories(repositories)); + GitRebaseActionDialog dialog = new GitRebaseActionDialog(project, getTemplatePresentation().getText(), roots, firstRepo.getRoot()); + dialog.show(); + VirtualFile root = dialog.selectRoot(); + if (root == null) return null; + return getRepositoryManager(project).getRepositoryForRootQuick(root); // TODO avoid root <-> GitRepository double conversion + } +} diff --git a/plugins/git4idea/src/git4idea/actions/GitAbstractRebaseResumeAction.java b/plugins/git4idea/src/git4idea/actions/GitAbstractRebaseResumeAction.java deleted file mode 100644 index ee03ae650793..000000000000 --- a/plugins/git4idea/src/git4idea/actions/GitAbstractRebaseResumeAction.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * 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 git4idea.actions; - -import com.intellij.openapi.actionSystem.AnActionEvent; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.ui.Messages; -import com.intellij.openapi.vfs.VirtualFile; -import git4idea.commands.GitCommand; -import git4idea.commands.GitLineHandler; -import git4idea.i18n.GitBundle; -import git4idea.rebase.GitInteractiveRebaseEditorHandler; -import git4idea.rebase.GitRebaseActionDialog; -import git4idea.rebase.GitRebaseUtils; -import org.jetbrains.annotations.NonNls; - -import java.util.Iterator; -import java.util.List; - -/** - * Base class for git rebase [--skip, --continue] actions and git rebase operation - */ -public abstract class GitAbstractRebaseResumeAction extends GitRebaseActionBase { - - /** - * {@inheritDoc} - */ - protected GitLineHandler createHandler(Project project, List gitRoots, VirtualFile defaultRoot) { - for (Iterator i = gitRoots.iterator(); i.hasNext();) { - if (!GitRebaseUtils.isRebaseInTheProgress(i.next())) { - i.remove(); - } - } - if (gitRoots.size() == 0) { - Messages.showErrorDialog(project, GitBundle.getString("rebase.action.no.root"), GitBundle.getString("rebase.action.error")); - return null; - } - final VirtualFile root; - if (gitRoots.size() == 1) { - root = gitRoots.get(0); - } - else { - if (!gitRoots.contains(defaultRoot)) { - defaultRoot = gitRoots.get(0); - } - GitRebaseActionDialog d = new GitRebaseActionDialog(project, getActionTitle(), gitRoots, defaultRoot); - d.show(); - - root = d.selectRoot(); - if (root == null) { - return null; - } - } - GitLineHandler h = new GitLineHandler(project, root, GitCommand.REBASE); - h.setStdoutSuppressed(false); - h.addParameters(getOptionName()); - return h; - } - - /** - * {@inheritDoc} - */ - @Override - protected void configureEditor(GitInteractiveRebaseEditorHandler editor) { - editor.setRebaseEditorShown(); - } - - /** - * @return title for rebase operation - */ - @NonNls - protected abstract String getOptionName(); - - /** - * @return title for root selection dialog - */ - protected abstract String getActionTitle(); - - @Override - protected boolean isEnabled(AnActionEvent e) { - return super.isEnabled(e) && isRebasing(e); - } -} diff --git a/plugins/git4idea/src/git4idea/actions/GitRebaseAbort.java b/plugins/git4idea/src/git4idea/actions/GitRebaseAbort.java index ab60b4f3320e..c76147a9cc95 100644 --- a/plugins/git4idea/src/git4idea/actions/GitRebaseAbort.java +++ b/plugins/git4idea/src/git4idea/actions/GitRebaseAbort.java @@ -15,88 +15,28 @@ */ package git4idea.actions; -import com.intellij.dvcs.DvcsUtil; -import com.intellij.openapi.actionSystem.AnActionEvent; -import com.intellij.openapi.application.AccessToken; +import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.project.Project; -import com.intellij.openapi.ui.Messages; -import com.intellij.openapi.vcs.VcsException; -import com.intellij.openapi.vfs.VfsUtil; -import com.intellij.openapi.vfs.VirtualFile; -import git4idea.commands.GitCommand; -import git4idea.commands.GitHandlerUtil; -import git4idea.commands.GitSimpleHandler; -import git4idea.i18n.GitBundle; -import git4idea.rebase.GitRebaseActionDialog; import git4idea.rebase.GitRebaseUtils; +import git4idea.repo.GitRepository; import org.jetbrains.annotations.NotNull; -import java.util.Iterator; -import java.util.List; -import java.util.Set; - -/** - * Rebase abort action - */ -public class GitRebaseAbort extends GitRepositoryAction { - - /** - * {@inheritDoc} - */ - protected void perform(@NotNull Project project, - @NotNull List gitRoots, - @NotNull VirtualFile defaultRoot, - Set affectedRoots, - List exceptions) throws VcsException { - // remote all roots where there are no rebase in progress - for (Iterator i = gitRoots.iterator(); i.hasNext();) { - if (!GitRebaseUtils.isRebaseInTheProgress(i.next())) { - i.remove(); - } - } - if (gitRoots.size() == 0) { - Messages.showErrorDialog(project, GitBundle.getString("rebase.action.no.root"), GitBundle.getString("rebase.action.error")); - return; - } - final VirtualFile root; - if (gitRoots.size() == 1) { - root = gitRoots.get(0); - } - else { - if (!gitRoots.contains(defaultRoot)) { - defaultRoot = gitRoots.get(0); - } - GitRebaseActionDialog d = new GitRebaseActionDialog(project, getActionName(), gitRoots, defaultRoot); - d.show(); - root = d.selectRoot(); - if (root == null) { - return; - } - } - affectedRoots.add(root); - GitSimpleHandler h = new GitSimpleHandler(project, root, GitCommand.REBASE); - h.setStdoutSuppressed(false); - h.addParameters("--abort"); - AccessToken token = DvcsUtil.workingTreeChangeStarted(project); - try { - GitHandlerUtil.doSynchronously(h, getActionName(), h.printableCommandLine()); - VfsUtil.markDirtyAndRefresh(true, true, false, root); - } - finally { - DvcsUtil.workingTreeChangeFinished(project, token); - } - } - - /** - * {@inheritDoc} - */ +public class GitRebaseAbort extends GitAbstractRebaseAction { @NotNull - protected String getActionName() { - return GitBundle.getString("rebase.abort.action.name"); + @Override + protected String getProgressTitle() { + return "Aborting Rebase Process..."; } @Override - protected boolean isEnabled(AnActionEvent e) { - return super.isEnabled(e) && isRebasing(e); + protected void performActionForProject(@NotNull Project project, @NotNull ProgressIndicator indicator) { + GitRebaseUtils.abort(project, indicator); + } + + @Override + protected void performActionForRepository(@NotNull Project project, + @NotNull GitRepository repository, + @NotNull ProgressIndicator indicator) { + GitRebaseUtils.abort(project, repository, indicator); } } diff --git a/plugins/git4idea/src/git4idea/actions/GitRebaseContinue.java b/plugins/git4idea/src/git4idea/actions/GitRebaseContinue.java index 1f4cb41fc1e4..3830a07bae27 100644 --- a/plugins/git4idea/src/git4idea/actions/GitRebaseContinue.java +++ b/plugins/git4idea/src/git4idea/actions/GitRebaseContinue.java @@ -15,35 +15,28 @@ */ package git4idea.actions; -import git4idea.i18n.GitBundle; -import org.jetbrains.annotations.NonNls; +import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.project.Project; +import git4idea.rebase.GitRebaseUtils; +import git4idea.repo.GitRepository; import org.jetbrains.annotations.NotNull; -/** - * Rebase abort action - */ -public class GitRebaseContinue extends GitAbstractRebaseResumeAction { - - /** - * {@inheritDoc} - */ +public class GitRebaseContinue extends GitAbstractRebaseAction { @NotNull - protected String getActionName() { - return GitBundle.getString("rebase.continue.action.name"); + @Override + protected String getProgressTitle() { + return GitRebaseUtils.CONTINUE_PROGRESS_TITLE; } - /** - * {@inheritDoc} - */ - @NonNls - protected String getOptionName() { - return "--continue"; + @Override + protected void performActionForProject(@NotNull Project project, @NotNull ProgressIndicator indicator) { + GitRebaseUtils.continueRebase(project); } - /** - * {@inheritDoc} - */ - protected String getActionTitle() { - return GitBundle.getString("rebase.continue.action.name"); + @Override + protected void performActionForRepository(@NotNull Project project, + @NotNull GitRepository repository, + @NotNull ProgressIndicator indicator) { + GitRebaseUtils.continueRebase(project, repository, indicator); } } \ No newline at end of file diff --git a/plugins/git4idea/src/git4idea/actions/GitRebaseSkip.java b/plugins/git4idea/src/git4idea/actions/GitRebaseSkip.java index 921f63010ff8..fb643ad11b45 100644 --- a/plugins/git4idea/src/git4idea/actions/GitRebaseSkip.java +++ b/plugins/git4idea/src/git4idea/actions/GitRebaseSkip.java @@ -15,35 +15,28 @@ */ package git4idea.actions; -import git4idea.i18n.GitBundle; -import org.jetbrains.annotations.NonNls; +import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.project.Project; +import git4idea.rebase.GitRebaseUtils; +import git4idea.repo.GitRepository; import org.jetbrains.annotations.NotNull; -/** - * Rebase abort action - */ -public class GitRebaseSkip extends GitAbstractRebaseResumeAction { - - /** - * {@inheritDoc} - */ +public class GitRebaseSkip extends GitAbstractRebaseAction { @NotNull - protected String getActionName() { - return GitBundle.getString("rebase.skip.action.name"); + @Override + protected String getProgressTitle() { + return "Skip Commit during Rebase..."; } - /** - * {@inheritDoc} - */ - @NonNls - protected String getOptionName() { - return "--skip"; + @Override + protected void performActionForProject(@NotNull Project project, @NotNull ProgressIndicator indicator) { + GitRebaseUtils.skipRebase(project); } - /** - * {@inheritDoc} - */ - protected String getActionTitle() { - return GitBundle.getString("rebase.skip.action.name"); + @Override + protected void performActionForRepository(@NotNull Project project, + @NotNull GitRepository repository, + @NotNull ProgressIndicator indicator) { + GitRebaseUtils.skipRebase(project, repository, indicator); } } \ No newline at end of file diff --git a/plugins/git4idea/src/git4idea/branch/GitRebaseParams.java b/plugins/git4idea/src/git4idea/branch/GitRebaseParams.java index c6a836cba78a..200f0ff6e4fb 100644 --- a/plugins/git4idea/src/git4idea/branch/GitRebaseParams.java +++ b/plugins/git4idea/src/git4idea/branch/GitRebaseParams.java @@ -18,67 +18,31 @@ package git4idea.branch; import com.intellij.openapi.util.text.StringUtil; import com.intellij.util.containers.ContainerUtil; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import java.util.List; -import static java.util.Collections.singletonList; - public class GitRebaseParams { - public enum Mode { - STANDARD, - CONTINUE, - SKIP, - ABORT; + @NotNull private final String myNewBase; - @NotNull - @SuppressWarnings("StringToUpperCaseOrToLowerCaseWithoutLocale") - String asArgument() { - if (this == STANDARD) return ""; - return "--" + this.name().toLowerCase(); - } - } - - @Nullable private final String myBase; - @NotNull private final Mode myMode; - - @NotNull - public static GitRebaseParams abort() { - return new GitRebaseParams(null, Mode.ABORT); - } - - public GitRebaseParams(@NotNull String base) { - this(base, Mode.STANDARD); - } - - private GitRebaseParams(@Nullable String base, @NotNull Mode mode) { - myBase = base; - myMode = mode; + public GitRebaseParams(@NotNull String newBase) { + myNewBase = newBase; } @NotNull - public GitRebaseParams withMode(@NotNull Mode mode) { - return mode == myMode ? this : new GitRebaseParams(myBase, mode); - } - - @NotNull - public String getBase() { - return StringUtil.notNullize(myBase); - } - - @NotNull - public Mode getMode() { - return myMode; - } - - @NotNull - public List getCommandLineArguments() { - if (myMode != Mode.STANDARD) { - return singletonList(myMode.asArgument()); - } + public List asCommandLineArguments() { List args = ContainerUtil.newArrayList(); - args.add(myBase); + args.add(myNewBase); return args; } + + @NotNull + public String getNewBase() { + return myNewBase; + } + + @Override + public String toString() { + return StringUtil.join(asCommandLineArguments(), " "); + } } diff --git a/plugins/git4idea/src/git4idea/commands/Git.java b/plugins/git4idea/src/git4idea/commands/Git.java index 011f70a4fbc0..79ae680a02b0 100644 --- a/plugins/git4idea/src/git4idea/commands/Git.java +++ b/plugins/git4idea/src/git4idea/commands/Git.java @@ -171,5 +171,17 @@ public interface Git { GitCommandResult remotePrune(@NotNull GitRepository repository, @NotNull GitRemote remote); @NotNull - GitCommandResult rebase(@NotNull GitRepository repository, @NotNull GitRebaseParams params, @NotNull GitLineHandlerListener... listeners); + GitCommandResult rebase(@NotNull GitRepository repository, + @NotNull GitRebaseParams parameters, + @NotNull GitLineHandlerListener... listeners); + + @NotNull + GitCommandResult rebaseAbort(@NotNull GitRepository repository, @NotNull GitLineHandlerListener... listeners); + + @NotNull + GitCommandResult rebaseContinue(@NotNull GitRepository repository, @NotNull GitLineHandlerListener... listeners); + + @NotNull + GitCommandResult rebaseSkip(@NotNull GitRepository repository, @NotNull GitLineHandlerListener... listeners); + } diff --git a/plugins/git4idea/src/git4idea/commands/GitImpl.java b/plugins/git4idea/src/git4idea/commands/GitImpl.java index 4fa2b1ac02af..b8cac21d0954 100644 --- a/plugins/git4idea/src/git4idea/commands/GitImpl.java +++ b/plugins/git4idea/src/git4idea/commands/GitImpl.java @@ -34,6 +34,9 @@ import git4idea.GitVcs; import git4idea.branch.GitRebaseParams; import git4idea.config.GitVersionSpecialty; import git4idea.history.GitHistoryUtils; +import git4idea.rebase.GitInteractiveRebaseEditorHandler; +import git4idea.rebase.GitRebaseEditorService; +import git4idea.rebase.GitRebaseResumeMode; import git4idea.repo.GitRemote; import git4idea.repo.GitRepository; import git4idea.reset.GitResetMode; @@ -562,14 +565,75 @@ public class GitImpl implements Git { @NotNull @Override public GitCommandResult rebase(@NotNull GitRepository repository, - @NotNull GitRebaseParams params, + @NotNull GitRebaseParams parameters, @NotNull GitLineHandlerListener... listeners) { GitLineHandler handler = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.REBASE); - handler.addParameters(params.getCommandLineArguments()); + handler.addParameters(parameters.asCommandLineArguments()); addListeners(handler, listeners); return run(handler); } + @NotNull + @Override + public GitCommandResult rebaseAbort(@NotNull GitRepository repository, @NotNull GitLineHandlerListener... listeners) { + GitLineHandler handler = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.REBASE); + handler.addParameters("--abort"); + addListeners(handler, listeners); + return run(handler); + } + + @NotNull + @Override + public GitCommandResult rebaseContinue(@NotNull GitRepository repository, @NotNull GitLineHandlerListener... listeners) { + return rebaseResume(repository, GitRebaseResumeMode.CONTINUE, listeners); + } + + @NotNull + @Override + public GitCommandResult rebaseSkip(@NotNull GitRepository repository, @NotNull GitLineHandlerListener... listeners) { + return rebaseResume(repository, GitRebaseResumeMode.SKIP, listeners); + } + + @NotNull + private static GitCommandResult rebaseResume(@NotNull GitRepository repository, + @NotNull GitRebaseResumeMode rebaseMode, + @NotNull GitLineHandlerListener[] listeners) { + Project project = repository.getProject(); + VirtualFile root = repository.getRoot(); + GitLineHandler handler = new GitLineHandler(project, root, GitCommand.REBASE); + handler.addParameters(rebaseMode.asCommandLineArgument()); + addListeners(handler, listeners); + return runWithEditor(project, root, handler, false); + } + + @NotNull + private static GitCommandResult runWithEditor(@NotNull Project project, + @NotNull VirtualFile root, + @NotNull GitLineHandler handler, + boolean commitListAware) { + GitInteractiveRebaseEditorHandler editor = configureEditor(project, root, handler, commitListAware); + try { + return run(handler); + } + finally { + editor.close(); + } + } + + @NotNull + private static GitInteractiveRebaseEditorHandler configureEditor(@NotNull Project project, + @NotNull VirtualFile root, + @NotNull GitLineHandler handler, + boolean commitListAware) { + GitRebaseEditorService service = GitRebaseEditorService.getInstance(); + GitInteractiveRebaseEditorHandler editor = new GitInteractiveRebaseEditorHandler(service, project, root, handler); + if (!commitListAware) { + editor.setRebaseEditorShown(); + } + service.configureHandler(handler, editor.getHandlerNo()); + return editor; + } + @NotNull private static GitCommandResult doLsRemote(@NotNull final Project project, @NotNull final File workingDir, diff --git a/plugins/git4idea/src/git4idea/rebase/GitAbortRebaseProcess.java b/plugins/git4idea/src/git4idea/rebase/GitAbortRebaseProcess.java index b8b70bd64939..983187b6a250 100644 --- a/plugins/git4idea/src/git4idea/rebase/GitAbortRebaseProcess.java +++ b/plugins/git4idea/src/git4idea/rebase/GitAbortRebaseProcess.java @@ -30,7 +30,6 @@ import com.intellij.util.containers.ContainerUtil; import git4idea.DialogManager; import git4idea.GitPlatformFacade; import git4idea.GitUtil; -import git4idea.branch.GitRebaseParams; import git4idea.commands.Git; import git4idea.commands.GitCommandResult; import git4idea.repo.GitRepository; @@ -79,7 +78,7 @@ class GitAbortRebaseProcess { } void abortWithConfirmation() { - LOG.debug("Abort rebase. " + (myRepositoryToAbort == null ? "Nothing to abort" : getShortRepositoryName(myRepositoryToAbort)) + + LOG.info("Abort rebase. " + (myRepositoryToAbort == null ? "Nothing to abort" : getShortRepositoryName(myRepositoryToAbort)) + ". Roots to rollback: " + DvcsUtil.joinShortNames(myRepositoriesToRollback.keySet())); final Ref ref = Ref.create(); ApplicationManager.getApplication().invokeAndWait(new Runnable() { @@ -89,7 +88,7 @@ class GitAbortRebaseProcess { } }, ModalityState.defaultModalityState()); - LOG.debug("User choice: " + ref.get()); + LOG.info("User choice: " + ref.get()); if (ref.get() == AbortChoice.ROLLBACK_AND_ABORT) { doAbort(true); } @@ -151,7 +150,7 @@ class GitAbortRebaseProcess { try { if (myRepositoryToAbort != null) { myIndicator.setText2("git rebase --abort" + GitUtil.mention(myRepositoryToAbort)); - GitCommandResult result = myGit.rebase(myRepositoryToAbort, GitRebaseParams.abort()); + GitCommandResult result = myGit.rebaseAbort(myRepositoryToAbort); repositoriesToRefresh.add(myRepositoryToAbort); if (!result.success()) { myNotifier.notifyError("Rebase Abort Failed", diff --git a/plugins/git4idea/src/git4idea/rebase/GitRebaseProblemDetector.java b/plugins/git4idea/src/git4idea/rebase/GitRebaseProblemDetector.java index 3cc65961598c..87144ab69f18 100644 --- a/plugins/git4idea/src/git4idea/rebase/GitRebaseProblemDetector.java +++ b/plugins/git4idea/src/git4idea/rebase/GitRebaseProblemDetector.java @@ -34,6 +34,7 @@ public class GitRebaseProblemDetector extends GitLineHandlerAdapter { private final static String[] REBASE_CONFLICT_INDICATORS = { "Merge conflict in", "hint: after resolving the conflicts, mark the corrected paths", + "You must edit all merge conflicts", "Failed to merge in the changes", "could not apply"}; private static final String REBASE_NO_CHANGE_INDICATOR = "No changes - did you forget to use 'git add'?"; diff --git a/plugins/git4idea/src/git4idea/rebase/GitRebaseProcess.java b/plugins/git4idea/src/git4idea/rebase/GitRebaseProcess.java index d04f9941de66..1fb3c24c39eb 100644 --- a/plugins/git4idea/src/git4idea/rebase/GitRebaseProcess.java +++ b/plugins/git4idea/src/git4idea/rebase/GitRebaseProcess.java @@ -23,30 +23,32 @@ import com.intellij.openapi.application.AccessToken; import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.progress.Task; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Condition; -import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vcs.VcsException; import com.intellij.openapi.vcs.VcsNotifier; import com.intellij.openapi.vcs.changes.ChangeListManager; import com.intellij.openapi.vfs.VfsUtil; import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.util.ExceptionUtil; import com.intellij.util.Function; import com.intellij.util.ThreeState; -import com.intellij.util.containers.ContainerUtil; import com.intellij.util.containers.MultiMap; import git4idea.GitPlatformFacade; import git4idea.GitUtil; import git4idea.branch.GitRebaseParams; import git4idea.commands.Git; import git4idea.commands.GitCommandResult; +import git4idea.commands.GitLineHandlerListener; import git4idea.commands.GitUntrackedFilesOverwrittenByOperationDetector; import git4idea.merge.GitConflictResolver; +import git4idea.rebase.GitSuccessfulRebase.SuccessType; import git4idea.repo.GitRepository; +import git4idea.repo.GitRepositoryManager; import git4idea.stash.GitChangesSaver; -import git4idea.stash.GitStashChangesSaver; import git4idea.util.GitFreezingProcess; import git4idea.util.GitUntrackedFilesHelper; import org.jetbrains.annotations.CalledInBackground; @@ -55,12 +57,19 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.event.HyperlinkEvent; -import java.util.*; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; import static com.intellij.dvcs.DvcsUtil.getShortRepositoryName; import static com.intellij.openapi.vfs.VfsUtilCore.toVirtualFileArray; +import static com.intellij.util.ObjectUtils.assertNotNull; +import static com.intellij.util.ObjectUtils.notNull; +import static com.intellij.util.containers.ContainerUtil.*; +import static com.intellij.util.containers.ContainerUtilRt.newArrayList; +import static com.intellij.util.containers.ContainerUtilRt.newLinkedHashSet; import static git4idea.GitUtil.getRootsFromRepositories; -import static git4idea.GitUtil.mention; import static java.util.Collections.singleton; public class GitRebaseProcess { @@ -72,45 +81,28 @@ public class GitRebaseProcess { @NotNull private final ChangeListManager myChangeListManager; @NotNull private final VcsNotifier myNotifier; @NotNull private final GitPlatformFacade myFacade; + @NotNull private final GitRepositoryManager myRepositoryManager; - @NotNull private final List myAllRepositories; - @NotNull private final GitRebaseParams myParams; + @NotNull private final GitRebaseSpec myRebaseSpec; + @Nullable private final GitRebaseResumeMode myCustomMode; @NotNull private final GitChangesSaver mySaver; - @NotNull private final Map mySuccessfulRepositories; - @NotNull private final Map myInitialHeadPositions; + @NotNull private final ProgressManager myProgressManager; - @NotNull private final MultiMap mySkippedCommits; - - protected GitRebaseProcess(@NotNull Project project, - @NotNull List repositories, - @NotNull GitRebaseParams params, - @NotNull ProgressIndicator indicator) { - this(project, repositories, params, newSaver(project, indicator), - MultiMap.create(), Collections.emptyMap()); - } - - private GitRebaseProcess(@NotNull Project project, - @NotNull List allRepositories, - @NotNull GitRebaseParams params, - @NotNull GitChangesSaver saver, - @NotNull MultiMap skippedCommits, - @NotNull Map successfulRepositories) { + public GitRebaseProcess(@NotNull Project project, @NotNull GitRebaseSpec rebaseSpec, @Nullable GitRebaseResumeMode customMode) { myProject = project; - myAllRepositories = allRepositories; - myParams = params; - mySaver = saver; - mySuccessfulRepositories = successfulRepositories; + myRebaseSpec = rebaseSpec; + myCustomMode = customMode; + mySaver = rebaseSpec.getSaver(); myGit = ServiceManager.getService(Git.class); myChangeListManager = ChangeListManager.getInstance(myProject); myNotifier = VcsNotifier.getInstance(myProject); myFacade = ServiceManager.getService(GitPlatformFacade.class); - - myInitialHeadPositions = readInitialHeadPositions(myAllRepositories); - mySkippedCommits = skippedCommits; + myRepositoryManager = GitUtil.getRepositoryManager(myProject); + myProgressManager = ProgressManager.getInstance(); } - void rebase() { + public void rebase() { new GitFreezingProcess(myProject, myFacade, "rebase", new Runnable() { public void run() { doRebase(); @@ -118,115 +110,173 @@ public class GitRebaseProcess { }).execute(); } - void abort(@Nullable GitRepository repositoryToAbort, - @NotNull final Collection successfulRepositories, - @NotNull ProgressIndicator indicator) { - new GitAbortRebaseProcess(myProject, repositoryToAbort, ContainerUtil.filter(myInitialHeadPositions, new Condition() { - @Override - public boolean value(GitRepository repository) { - return successfulRepositories.contains(repository); - } - }), indicator, mySaver).abortWithConfirmation(); - } - - void retry(final boolean continueRebase) { - new GitFreezingProcess(myProject, myFacade, "rebase", new Runnable() { - public void run() { - GitRebaseParams params = continueRebase ? myParams.withMode(GitRebaseParams.Mode.CONTINUE) : myParams; - new GitRebaseProcess(myProject, myAllRepositories, params, mySaver, mySkippedCommits, mySuccessfulRepositories).doRebase(); - } - }).execute(); - } - + /** + * Given a GitRebaseSpec this method either starts, or continues the ongoing rebase in multiple repositories. + *
    + *
  • It does nothing with "already successfully rebased repositories" (the ones which have {@link GitRebaseStatus} == SUCCESSFUL, + * and just remembers them to use in the resulting notification.
  • + *
  • If there is a repository with rebase in progress, it calls `git rebase --continue` (or `--skip`). + * It is assumed that there is only one such repository.
  • + *
  • For all remaining repositories rebase on which didn't start yet, it calls {@code git rebase }
  • + *
+ */ private void doRebase() { - LOG.debug("Started rebase."); - Map successfulWithTypes = ContainerUtil.newLinkedHashMap(mySuccessfulRepositories); - List repositories = ContainerUtil.newArrayList(myAllRepositories); - repositories.removeAll(successfulWithTypes.keySet()); - ListIterator iterator = repositories.listIterator(); + LOG.info("Started rebase"); + LOG.debug("Started rebase with the following spec: " + myRebaseSpec); + Map statuses = newLinkedHashMap(myRebaseSpec.getStatuses()); + Collection toRefresh = newLinkedHashSet(); + List repositoriesToRebase = myRebaseSpec.getIncompleteRepositories(); AccessToken token = DvcsUtil.workingTreeChangeStarted(myProject); try { - if (!saveDirtyRootsInitially(repositories)) return; + if (!saveDirtyRootsInitially(repositoriesToRebase)) return; - GitRepository facedDirtyError = null; - Map customModes = ContainerUtil.newLinkedHashMap(); - while (iterator.hasNext()) { - GitRepository repository = iterator.next(); - VirtualFile root = repository.getRoot(); - String repoName = getShortRepositoryName(repository); - LOG.debug("Rebase iteration. Root: " + repoName); - Collection successful = successfulWithTypes.keySet(); + GitRepository failed = null; + for (GitRepository repository : repositoriesToRebase) { + GitRebaseResumeMode customMode = null; + if (repository == myRebaseSpec.getOngoingRebase()) { + customMode = myCustomMode == null ? GitRebaseResumeMode.CONTINUE : myCustomMode; + } - GitRebaseProblemDetector rebaseDetector = new GitRebaseProblemDetector(); - GitUntrackedFilesOverwrittenByOperationDetector untrackedDetector = new GitUntrackedFilesOverwrittenByOperationDetector(root); - GitRebaseLineListener progressListener = new GitRebaseLineListener(); + GitRebaseStatus rebaseStatus = rebaseSingleRoot(repository, customMode, getSuccessfulRepositories(statuses)); + statuses.put(repository, rebaseStatus); + if (shouldBeRefreshed(rebaseStatus)) { + toRefresh.add(repository); + } + if (rebaseStatus.getType() != GitRebaseStatus.Type.SUCCESS) { + failed = repository; + break; + } + } - GitRebaseParams.Mode customMode = customModes.get(repository); - GitRebaseParams rebaseParams = customMode != null ? myParams.withMode(customMode) : myParams; - GitCommandResult result = myGit.rebase(repository, rebaseParams, rebaseDetector, untrackedDetector, progressListener); - boolean nonStandardMode = rebaseParams.getMode() != GitRebaseParams.Mode.STANDARD; - boolean somethingRebased = nonStandardMode || progressListener.getResult().current > 1; + if (failed == null) { + LOG.debug("Rebase completed successfully."); + mySaver.load(); + } + refresh(toRefresh); + if (failed == null) { + notifySuccess(getSuccessfulRepositories(statuses), getSkippedCommits(statuses)); + } - if (result.success()) { - LOG.debug("Successfully rebased " + repoName); - successfulWithTypes.put(repository, SuccessType.fromOutput(result.getOutput())); - } - else if (rebaseDetector.isDirtyTree() && !nonStandardMode && repository != facedDirtyError) { - // if the initial dirty tree check doesn't find all local changes, we are still ready to stash-on-demand, - // but only once per repository (if the error happens again, that means that the previous stash attempt failed for some reason), - // and not in the case of --continue (where all local changes are expected to be committed) or --skip. - - LOG.debug("Dirty tree detected in " + repoName); - String saveError = saveLocalChanges(singleton(repository.getRoot())); - if (saveError == null) { - iterator.previous(); // try same repository again - facedDirtyError = repository; - } - else { - LOG.warn("Couldn't " + mySaver.getOperationName() + " root " + repository.getRoot() + ": " + saveError); - showFatalError(saveError, repository, somethingRebased, successful); - return; - } - } - else if (rebaseDetector.isMergeConflict()) { - LOG.debug("Merge conflict in " + repoName); - boolean allResolved = showConflictResolver(repository, false); - if (allResolved) { - iterator.previous(); // continue with the same repository - customModes.put(repository, GitRebaseParams.Mode.CONTINUE); - } - else { - notifyNotAllConflictsResolved(repository, successful); - return; - } - } - else if (rebaseDetector.isNoChangeError()) { - LOG.info("'No changes' situation detected in " + repoName); - mySkippedCommits.putValue(repository, GitRebaseUtils.getCurrentRebaseCommit(root)); - iterator.previous(); - customModes.put(repository, GitRebaseParams.Mode.SKIP); - } - else if (untrackedDetector.wasMessageDetected()) { - LOG.debug("Untracked files detected in " + repoName); - showUntrackedFilesError(untrackedDetector.getRelativeFilePaths(), repository, somethingRebased, successful); - return; - } - else { - LOG.info("Error rebasing root " + repoName + ": " + result.getErrorOutputAsJoinedString()); - showFatalError(result.getErrorOutputAsHtmlString(), repository, somethingRebased, successful); - return; - } - } // while - - LOG.debug("Rebase completed successfully."); - mySaver.load(); + saveUpdatedSpec(statuses); + } + catch(Throwable e) { + myRepositoryManager.setOngoingRebaseSpec(null); + ExceptionUtil.rethrowUnchecked(e); } finally { - refresh(getRepositoriesToRefresh(successfulWithTypes, iterator.previous())); DvcsUtil.workingTreeChangeFinished(myProject, token); } - notifySuccess(successfulWithTypes); // refresh _before_ showing the notification + } + + private void saveUpdatedSpec(@NotNull Map statuses) { + if (myRebaseSpec.shouldBeSaved()) { + GitRebaseSpec newRebaseInfo = myRebaseSpec.cloneWithNewStatuses(statuses); + myRepositoryManager.setOngoingRebaseSpec(newRebaseInfo); + } + else { + myRepositoryManager.setOngoingRebaseSpec(null); + } + } + + @NotNull + private GitRebaseStatus rebaseSingleRoot(@NotNull GitRepository repository, + @Nullable GitRebaseResumeMode customMode, + @NotNull Map alreadyRebased) { + VirtualFile root = repository.getRoot(); + String repoName = getShortRepositoryName(repository); + LOG.info("Rebasing root " + repoName + ", mode: " + notNull(customMode, "standard")); + + Collection skippedCommits = newArrayList(); + MultiMap allSkippedCommits = getSkippedCommits(alreadyRebased); + boolean retryWhenDirty = false; + + while (true) { + GitRebaseProblemDetector rebaseDetector = new GitRebaseProblemDetector(); + GitUntrackedFilesOverwrittenByOperationDetector untrackedDetector = new GitUntrackedFilesOverwrittenByOperationDetector(root); + GitRebaseLineListener progressListener = new GitRebaseLineListener(); + GitCommandResult result = callRebase(repository, customMode, rebaseDetector, untrackedDetector, progressListener); + + boolean somethingRebased = customMode != null || progressListener.getResult().current > 1; + + if (result.success()) { + LOG.debug("Successfully rebased " + repoName); + return GitSuccessfulRebase.parseFromOutput(result.getOutput(), skippedCommits); + } + else if (rebaseDetector.isDirtyTree() && customMode == null && !retryWhenDirty) { + // if the initial dirty tree check doesn't find all local changes, we are still ready to stash-on-demand, + // but only once per repository (if the error happens again, that means that the previous stash attempt failed for some reason), + // and not in the case of --continue (where all local changes are expected to be committed) or --skip. + LOG.debug("Dirty tree detected in " + repoName); + String saveError = saveLocalChanges(singleton(repository.getRoot())); + if (saveError == null) { + retryWhenDirty = true; // try same repository again + } + else { + LOG.warn("Couldn't " + mySaver.getOperationName() + " root " + repository.getRoot() + ": " + saveError); + showFatalError(saveError, repository, somethingRebased, alreadyRebased.keySet(), allSkippedCommits); + GitRebaseStatus.Type type = somethingRebased ? GitRebaseStatus.Type.SUSPENDED : GitRebaseStatus.Type.ERROR; + return new GitRebaseStatus(type, skippedCommits); + } + } + else if (untrackedDetector.wasMessageDetected()) { + LOG.info("Untracked files detected in " + repoName); + showUntrackedFilesError(untrackedDetector.getRelativeFilePaths(), repository, somethingRebased, alreadyRebased.keySet(), + allSkippedCommits); + GitRebaseStatus.Type type = somethingRebased ? GitRebaseStatus.Type.SUSPENDED : GitRebaseStatus.Type.ERROR; + return new GitRebaseStatus(type, skippedCommits); + } + else if (rebaseDetector.isNoChangeError()) { + LOG.info("'No changes' situation detected in " + repoName); + GitRebaseUtils.CommitInfo currentRebaseCommit = GitRebaseUtils.getCurrentRebaseCommit(root); + if (currentRebaseCommit != null) skippedCommits.add(currentRebaseCommit); + customMode = GitRebaseResumeMode.SKIP; + } + else if (rebaseDetector.isMergeConflict()) { + LOG.info("Merge conflict in " + repoName); + ResolveConflictResult resolveResult = showConflictResolver(repository, false); + if (resolveResult == ResolveConflictResult.ALL_RESOLVED) { + customMode = GitRebaseResumeMode.CONTINUE; + } + else if (resolveResult == ResolveConflictResult.NOTHING_TO_MERGE) { + // the output is the same for the cases: + // (1) "unresolved conflicts" + // (2) "manual editing of a file not followed by `git add` + // => we check if there are any unresolved conflicts, and if not, then it is the case #2 which we are not handling + LOG.info("Unmerged changes while rebasing root " + repoName + ": " + result.getErrorOutputAsJoinedString()); + showFatalError(result.getErrorOutputAsHtmlString(), repository, somethingRebased, alreadyRebased.keySet(), allSkippedCommits); + GitRebaseStatus.Type type = somethingRebased ? GitRebaseStatus.Type.SUSPENDED : GitRebaseStatus.Type.ERROR; + return new GitRebaseStatus(type, skippedCommits); + } + else { + notifyNotAllConflictsResolved(repository, allSkippedCommits); + return new GitRebaseStatus(GitRebaseStatus.Type.SUSPENDED, skippedCommits); + } + } + else { + LOG.info("Error rebasing root " + repoName + ": " + result.getErrorOutputAsJoinedString()); + showFatalError(result.getErrorOutputAsHtmlString(), repository, somethingRebased, alreadyRebased.keySet(), allSkippedCommits); + GitRebaseStatus.Type type = somethingRebased ? GitRebaseStatus.Type.SUSPENDED : GitRebaseStatus.Type.ERROR; + return new GitRebaseStatus(type, skippedCommits); + } + } + } + + @NotNull + private GitCommandResult callRebase(@NotNull GitRepository repository, + @Nullable GitRebaseResumeMode mode, + @NotNull GitLineHandlerListener... listeners) { + if (mode == null) { + GitRebaseParams params = assertNotNull(myRebaseSpec.getParams()); + return myGit.rebase(repository, params, listeners); + } + else if (mode == GitRebaseResumeMode.SKIP) { + return myGit.rebaseSkip(repository, listeners); + } + else { + LOG.assertTrue(mode == GitRebaseResumeMode.CONTINUE, "Unexpected rebase mode: " + mode); + return myGit.rebaseContinue(repository, listeners); + } } @VisibleForTesting @@ -235,57 +285,26 @@ public class GitRebaseProcess { return findRootsWithLocalChanges(repositories); } - @NotNull - private static Map readInitialHeadPositions(@NotNull Collection repositories) { - updateRepositoriesInfo(repositories); - return ContainerUtil.map2Map(repositories, new Function>() { - @Override - public Pair fun(@NotNull GitRepository repository) { - String currentRevision = repository.getCurrentRevision(); - LOG.debug("Current revision in [" + repository.getRoot().getName() + "] is [" + currentRevision + "]"); - return Pair.create(repository, currentRevision); - } - }); - } - - @NotNull - private static Collection getRepositoriesToRefresh(@NotNull final Map successfulWithTypes, - @NotNull GitRepository latestProcessed) { - Collection toRefresh = ContainerUtil.newHashSet(ContainerUtil.filter(successfulWithTypes.keySet(), - new Condition() { - @Override - public boolean value(GitRepository repository) { - return successfulWithTypes.get(repository) != SuccessType.UP_TO_DATE; - } - })); - toRefresh.add(latestProcessed); - return toRefresh; + private static boolean shouldBeRefreshed(@NotNull GitRebaseStatus rebaseStatus) { + return rebaseStatus.getType() != GitRebaseStatus.Type.SUCCESS || + ((GitSuccessfulRebase)rebaseStatus).getSuccessType() != SuccessType.UP_TO_DATE; } private static void refresh(@NotNull Collection repositories) { - updateRepositoriesInfo(repositories); + GitUtil.updateRepositories(repositories); + // TODO use --diff-stat, and refresh only what's needed VfsUtil.markDirtyAndRefresh(false, true, false, toVirtualFileArray(getRootsFromRepositories(repositories))); } - private static void updateRepositoriesInfo(@NotNull Collection repositories) { - for (GitRepository repository : repositories) { - repository.update(); - } - } - - @NotNull - private static GitStashChangesSaver newSaver(@NotNull Project project, @NotNull ProgressIndicator indicator) { - Git git = ServiceManager.getService(Git.class); - GitPlatformFacade facade = ServiceManager.getService(GitPlatformFacade.class); - return new GitStashChangesSaver(project, facade, git, indicator, "Uncommitted changes before rebase"); - } - private boolean saveDirtyRootsInitially(@NotNull List repositories) { - if (myParams.getMode() != GitRebaseParams.Mode.STANDARD) { - LOG.debug("No need to pre-save dirty roots in the " + myParams.getMode() + " mode"); - return true; - } - Collection rootsToSave = getRootsFromRepositories(getDirtyRoots(repositories)); + Collection repositoriesToSave = filter(repositories, new Condition() { + @Override + public boolean value(GitRepository repository) { + return !repository.equals(myRebaseSpec.getOngoingRebase()); // no need to save anything when --continue/--skip is to be called + } + }); + if (repositoriesToSave.isEmpty()) return true; + Collection rootsToSave = getRootsFromRepositories(getDirtyRoots(repositoriesToSave)); String error = saveLocalChanges(rootsToSave); if (error != null) { myNotifier.notifyError("Rebase not Started", error); @@ -307,7 +326,7 @@ public class GitRebaseProcess { } private Collection findRootsWithLocalChanges(@NotNull Collection repositories) { - return ContainerUtil.filter(repositories, new Condition() { + return filter(repositories, new Condition() { @Override public boolean value(GitRepository repository) { return myChangeListManager.haveChangesUnder(repository.getRoot()) != ThreeState.NO; @@ -315,22 +334,31 @@ public class GitRebaseProcess { }); } - private void notifySuccess(@NotNull Map successful) { - String rebasedBranch = getCommonCurrentBranchNameIfAllTheSame(myAllRepositories); - SuccessType commonType = getItemIfAllTheSame(successful.values(), SuccessType.REBASED); - String message = commonType.formatMessage(rebasedBranch, myParams.getBase()); - message += mentionSkippedCommits(); + private void notifySuccess(@NotNull Map successful, + final MultiMap skippedCommits) { + String rebasedBranch = getCommonCurrentBranchNameIfAllTheSame(myRebaseSpec.getAllRepositories()); + List successTypes = map(successful.values(), + new Function() { + @Override + public SuccessType fun(@NotNull GitSuccessfulRebase rebase) { + return rebase.getSuccessType(); + } + }); + SuccessType commonType = getItemIfAllTheSame(successTypes, SuccessType.REBASED); + GitRebaseParams params = myRebaseSpec.getParams(); + String message = commonType.formatMessage(rebasedBranch, params == null ? null : params.getNewBase()); + message += mentionSkippedCommits(skippedCommits); myNotifier.notifyMinorInfo("Rebase Successful", message, new NotificationListener.Adapter() { @Override protected void hyperlinkActivated(@NotNull Notification notification, @NotNull HyperlinkEvent e) { - handlePossibleCommitLinks(e.getDescription()); + handlePossibleCommitLinks(e.getDescription(), skippedCommits); } }); } @Nullable private static String getCommonCurrentBranchNameIfAllTheSame(@NotNull Collection repositories) { - return getItemIfAllTheSame(ContainerUtil.map(repositories, new Function() { + return getItemIfAllTheSame(map(repositories, new Function() { @Override public String fun(@NotNull GitRepository repository) { return repository.getCurrentBranchName(); @@ -340,43 +368,51 @@ public class GitRebaseProcess { @Contract("_, !null -> !null") private static T getItemIfAllTheSame(@NotNull Collection collection, @Nullable T defaultItem) { - return ContainerUtil.newHashSet(collection).size() == 1 ? ContainerUtil.getFirstItem(collection) : defaultItem; + return newHashSet(collection).size() == 1 ? getFirstItem(collection) : defaultItem; } - private void notifyNotAllConflictsResolved(@NotNull GitRepository conflictingRepository, @NotNull Collection successful) { + private void notifyNotAllConflictsResolved(@NotNull GitRepository conflictingRepository, + MultiMap skippedCommits) { String description = "You have to resolve the conflicts and continue rebase.
" + "If you want to start from the beginning, you can abort rebase."; description += GitRebaseUtils.mentionLocalChangesRemainingInStash(mySaver); myNotifier.notifyImportantWarning("Rebase Suspended", description, - new RebaseNotificationListener(conflictingRepository, true, successful)); + new RebaseNotificationListener(conflictingRepository, skippedCommits)); } - private boolean showConflictResolver(@NotNull GitRepository conflicting, boolean calledFromNotification) { + @NotNull + private ResolveConflictResult showConflictResolver(@NotNull GitRepository conflicting, boolean calledFromNotification) { GitConflictResolver.Params params = new GitConflictResolver.Params().setReverse(true); RebaseConflictResolver conflictResolver = new RebaseConflictResolver(myProject, myGit, myFacade, conflicting, params, calledFromNotification); - return conflictResolver.merge(); + boolean allResolved = conflictResolver.merge(); + if (conflictResolver.myWasNothingToMerge) return ResolveConflictResult.NOTHING_TO_MERGE; + if (allResolved) return ResolveConflictResult.ALL_RESOLVED; + return ResolveConflictResult.UNRESOLVED_REMAIN; } private void showFatalError(@NotNull final String error, @NotNull final GitRepository currentRepository, boolean somethingWasRebased, - @NotNull final Collection successful) { - String description = "Rebase failed with error" + mention(currentRepository) + ": " + error + "
" + + @NotNull final Collection successful, + @NotNull MultiMap skippedCommits) { + String repo = myRepositoryManager.moreThanOneRoot() ? getShortRepositoryName(currentRepository) + ": " : ""; + String description = repo + error + "
" + mentionRetryAndAbort(somethingWasRebased, successful) + - mentionSkippedCommits() + + mentionSkippedCommits(skippedCommits) + GitRebaseUtils.mentionLocalChangesRemainingInStash(mySaver); - myNotifier.notifyError("Rebase Failed", description, - new RebaseNotificationListener(currentRepository, somethingWasRebased, successful)); + String title = myRebaseSpec.getOngoingRebase() == null ? "Rebase Failed" : "Continue Rebase Failed"; + myNotifier.notifyError(title, description, new RebaseNotificationListener(currentRepository, skippedCommits)); } private void showUntrackedFilesError(@NotNull Set untrackedPaths, @NotNull GitRepository currentRepository, boolean somethingWasRebased, - @NotNull Collection successful) { + @NotNull Collection successful, + MultiMap skippedCommits) { String message = GitUntrackedFilesHelper.createUntrackedFilesOverwrittenDescription("rebase", true) + mentionRetryAndAbort(somethingWasRebased, successful) + - mentionSkippedCommits() + + mentionSkippedCommits(skippedCommits) + GitRebaseUtils.mentionLocalChangesRemainingInStash(mySaver); GitUntrackedFilesHelper.notifyUntrackedFilesOverwrittenBy(myProject, currentRepository.getRoot(), untrackedPaths, "rebase", message); } @@ -389,16 +425,16 @@ public class GitRebaseProcess { } @NotNull - private String mentionSkippedCommits() { - if (mySkippedCommits.isEmpty()) return ""; + private static String mentionSkippedCommits(@NotNull MultiMap skippedCommits) { + if (skippedCommits.isEmpty()) return ""; String message = "
"; - if (mySkippedCommits.values().size() == 1) { + if (skippedCommits.values().size() == 1) { message += "The following commit was skipped during rebase:
"; } else { message += "The following commits were skipped during rebase:
"; } - message += StringUtil.join(mySkippedCommits.values(), new Function() { + message += StringUtil.join(skippedCommits.values(), new Function() { @Override public String fun(@NotNull GitRebaseUtils.CommitInfo commitInfo) { String commitMessage = StringUtil.shortenPathWithEllipsis(commitInfo.subject, 72, true); @@ -410,53 +446,28 @@ public class GitRebaseProcess { return message; } - private enum SuccessType { - REBASED { - @NotNull - @Override - public String formatMessage(@Nullable String currentBranch, @NotNull String baseBranch) { - return "Rebased" + notNullize(currentBranch) + " on " + baseBranch; - } - }, - UP_TO_DATE { - @NotNull - @Override - public String formatMessage(@Nullable String currentBranch, @NotNull String baseBranch) { - return currentBranch != null ? currentBranch + " is up-to-date with " + baseBranch : "Up-to-date with " + baseBranch; - } - }, - FAST_FORWARDED { - @NotNull - @Override - public String formatMessage(@Nullable String currentBranch, @NotNull String baseBranch) { - return "Fast-forwarded" + notNullize(currentBranch) + " to " + baseBranch; - } - }; - - @NotNull - private static String notNullize(@Nullable String currentBranch) { - return currentBranch != null ? " " + currentBranch : ""; + @NotNull + private static MultiMap getSkippedCommits(@NotNull Map statuses) { + MultiMap map = MultiMap.create(); + for (GitRepository repository : statuses.keySet()) { + map.put(repository, statuses.get(repository).getSkippedCommits()); } + return map; + } - @NotNull - abstract String formatMessage(@Nullable String currentBranch, @NotNull String baseBranch); - - @NotNull - static SuccessType fromOutput(@NotNull List output) { - for (String line : output) { - if (StringUtil.containsIgnoreCase(line, "Fast-forwarded")) { - return FAST_FORWARDED; - } - if (StringUtil.containsIgnoreCase(line, "is up to date")) { - return UP_TO_DATE; - } - } - return REBASED; + @NotNull + private static Map getSuccessfulRepositories(@NotNull Map statuses) { + Map map = newLinkedHashMap(); + for (GitRepository repository : statuses.keySet()) { + GitRebaseStatus status = statuses.get(repository); + if (status instanceof GitSuccessfulRebase) map.put(repository, (GitSuccessfulRebase)status); } + return map; } private class RebaseConflictResolver extends GitConflictResolver { private final boolean myCalledFromNotification; + private boolean myWasNothingToMerge; RebaseConflictResolver(@NotNull Project project, @NotNull Git git, @@ -475,42 +486,46 @@ public class GitRebaseProcess { @CalledInBackground @Override protected boolean proceedAfterAllMerged() throws VcsException { - if (myCalledFromNotification) retry(true); + if (myCalledFromNotification) { + retry(GitRebaseUtils.CONTINUE_PROGRESS_TITLE); + } + return true; + } + + @Override + protected boolean proceedIfNothingToMerge() throws VcsException { + myWasNothingToMerge = true; return true; } } + private enum ResolveConflictResult { + ALL_RESOLVED, + NOTHING_TO_MERGE, + UNRESOLVED_REMAIN + } + private class RebaseNotificationListener extends NotificationListener.Adapter { @NotNull private final GitRepository myCurrentRepository; - private final boolean mySomethingWasRebased; - @NotNull private final Collection mySuccessful; + private final MultiMap mySkippedCommits; RebaseNotificationListener(@NotNull GitRepository currentRepository, - boolean somethingWasRebased, - @NotNull Collection successful) { + MultiMap skippedCommits) { myCurrentRepository = currentRepository; - mySomethingWasRebased = somethingWasRebased; - mySuccessful = successful; + mySkippedCommits = skippedCommits; } @Override protected void hyperlinkActivated(@NotNull Notification notification, @NotNull final HyperlinkEvent e) { final String href = e.getDescription(); if ("abort".equals(href)) { - new Task.Backgroundable(myProject, "Aborting Rebase...") { - @Override - public void run(@NotNull ProgressIndicator indicator) { - abort(mySomethingWasRebased ? myCurrentRepository : null, mySuccessful, indicator); - } - }.queue(); + abort(); } - else if ("retry".equals(href) || "continue".equals(href)) { - new Task.Backgroundable(myProject, "Rebasing...") { - @Override - public void run(@NotNull ProgressIndicator indicator) { - retry(mySomethingWasRebased); - } - }.queue(); + else if ("continue".equals(href)) { + retry(GitRebaseUtils.CONTINUE_PROGRESS_TITLE); + } + else if ("retry".equals(href)) { + retry("Retry Rebase Process..."); } else if ("resolve".equals(href)) { showConflictResolver(myCurrentRepository, true); @@ -519,24 +534,43 @@ public class GitRebaseProcess { mySaver.showSavedChanges(); } else { - handlePossibleCommitLinks(href); + handlePossibleCommitLinks(href, mySkippedCommits); } } } - private void handlePossibleCommitLinks(@NotNull String href) { - GitRepository repository = findRootBySkippedCommit(href); + private void abort() { + myProgressManager.run(new Task.Backgroundable(myProject, "Aborting Rebase Process...") { + @Override + public void run(@NotNull ProgressIndicator indicator) { + GitRebaseUtils.abort(myProject, indicator); + } + }); + } + + private void retry(@NotNull final String processTitle) { + myProgressManager.run(new Task.Backgroundable(myProject, processTitle, true) { + @Override + public void run(@NotNull ProgressIndicator indicator) { + GitRebaseUtils.continueRebase(myProject); + } + }); + } + + private void handlePossibleCommitLinks(@NotNull String href, MultiMap skippedCommits) { + GitRepository repository = findRootBySkippedCommit(href, skippedCommits); if (repository != null) { GitUtil.showSubmittedFiles(myProject, href, repository.getRoot(), true, false); } } @Nullable - private GitRepository findRootBySkippedCommit(@NotNull final String hash) { - return ContainerUtil.find(mySkippedCommits.keySet(), new Condition() { + private static GitRepository findRootBySkippedCommit(@NotNull final String hash, + final MultiMap skippedCommits) { + return find(skippedCommits.keySet(), new Condition() { @Override public boolean value(GitRepository repository) { - return ContainerUtil.exists(mySkippedCommits.get(repository), new Condition() { + return exists(skippedCommits.get(repository), new Condition() { @Override public boolean value(GitRebaseUtils.CommitInfo info) { return info.revision.asString().equals(hash); diff --git a/plugins/git4idea/src/git4idea/rebase/GitRebaseResumeMode.java b/plugins/git4idea/src/git4idea/rebase/GitRebaseResumeMode.java new file mode 100644 index 000000000000..f34c6b18bd8e --- /dev/null +++ b/plugins/git4idea/src/git4idea/rebase/GitRebaseResumeMode.java @@ -0,0 +1,34 @@ +/* + * Copyright 2000-2015 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 git4idea.rebase; + +import org.jetbrains.annotations.NotNull; + +public enum GitRebaseResumeMode { + CONTINUE("--continue"), + SKIP("--skip"); + + @NotNull private final String myCommandLineArgument; + + GitRebaseResumeMode(@NotNull String argument) { + myCommandLineArgument = argument; + } + + @NotNull + public String asCommandLineArgument() { + return myCommandLineArgument; + } +} diff --git a/plugins/git4idea/src/git4idea/rebase/GitRebaseSpec.java b/plugins/git4idea/src/git4idea/rebase/GitRebaseSpec.java new file mode 100644 index 000000000000..68dfa746930c --- /dev/null +++ b/plugins/git4idea/src/git4idea/rebase/GitRebaseSpec.java @@ -0,0 +1,224 @@ +/* + * Copyright 2000-2015 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 git4idea.rebase; + +import com.intellij.dvcs.DvcsUtil; +import com.intellij.openapi.components.ServiceManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Condition; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.util.Function; +import com.intellij.util.containers.ContainerUtil; +import git4idea.GitPlatformFacade; +import git4idea.GitUtil; +import git4idea.branch.GitRebaseParams; +import git4idea.commands.Git; +import git4idea.repo.GitRepository; +import git4idea.stash.GitChangesSaver; +import git4idea.stash.GitStashChangesSaver; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; + +import static com.intellij.dvcs.DvcsUtil.getShortNames; + +public class GitRebaseSpec { + + private static final Logger LOG = Logger.getInstance(GitRebaseSpec.class); + + @Nullable private final GitRebaseParams myParams; + @NotNull private final Map myStatuses; + @NotNull private final Map myInitialHeadPositions; + @NotNull private final GitChangesSaver mySaver; + private final boolean myShouldBeSaved; + + public GitRebaseSpec(@Nullable GitRebaseParams params, + @NotNull Map statuses, + @NotNull Map initialHeadPositions, + @NotNull GitChangesSaver saver, + boolean shouldBeSaved) { + myParams = params; + myStatuses = statuses; + myInitialHeadPositions = initialHeadPositions; + mySaver = saver; + myShouldBeSaved = shouldBeSaved; + } + + @NotNull + public static GitRebaseSpec forNewRebase(@NotNull Project project, + @NotNull GitRebaseParams params, + @NotNull Collection repositories, + @NotNull ProgressIndicator indicator) { + Map initialHeadPositions = readInitialHeadPositions(repositories); + Map initialStatusMap = new TreeMap(DvcsUtil.REPOSITORY_COMPARATOR); + for (GitRepository repository : repositories) { + initialStatusMap.put(repository, GitRebaseStatus.notStarted()); + } + return new GitRebaseSpec(params, initialStatusMap, initialHeadPositions, newSaver(project, indicator), true); + } + + @Nullable + public static GitRebaseSpec forResumeInSingleRepository(@NotNull Project project, + @NotNull GitRepository repository, + @NotNull ProgressIndicator indicator) { + if (!repository.isRebaseInProgress()) return null; + GitRebaseStatus suspended = new GitRebaseStatus(GitRebaseStatus.Type.SUSPENDED, Collections.emptyList()); + return new GitRebaseSpec(null, Collections.singletonMap(repository, suspended), + Collections.emptyMap(), newSaver(project, indicator), false); + } + + public boolean isValid() { + return singleOngoingRebase() && rebaseStatusesMatch(); + } + + @NotNull + public GitChangesSaver getSaver() { + return mySaver; + } + + @NotNull + public Collection getAllRepositories() { + return myStatuses.keySet(); + } + + @Nullable + public GitRepository getOngoingRebase() { + return ContainerUtil.getFirstItem(getOngoingRebases()); + } + + @Nullable + public GitRebaseParams getParams() { + return myParams; + } + + @NotNull + public Map getStatuses() { + return Collections.unmodifiableMap(myStatuses); + } + + @NotNull + public Map getHeadPositionsToRollback() { + return ContainerUtil.filter(myInitialHeadPositions, new Condition() { + @Override + public boolean value(GitRepository repository) { + return myStatuses.get(repository).getType() == GitRebaseStatus.Type.SUCCESS; + } + }); + } + + @NotNull + public GitRebaseSpec cloneWithNewStatuses(@NotNull Map statuses) { + return new GitRebaseSpec(myParams, statuses, myInitialHeadPositions, mySaver, true); + } + + public boolean shouldBeSaved() { + return myShouldBeSaved; + } + + /** + * Returns repositories for which rebase is in progress, has failed and we want to retry, or didn't start yet.
+ * It is guaranteed that if there is a rebase in progress (returned by {@link #getOngoingRebase()}, it will be the first in the list. + */ + @NotNull + public List getIncompleteRepositories() { + List incompleteRepositories = ContainerUtil.newArrayList(); + final GitRepository ongoingRebase = getOngoingRebase(); + if (ongoingRebase != null) incompleteRepositories.add(ongoingRebase); + incompleteRepositories.addAll(DvcsUtil.sortRepositories(ContainerUtil.filter(myStatuses.keySet(), new Condition() { + @Override + public boolean value(@NotNull GitRepository repository) { + return !repository.equals(ongoingRebase) && myStatuses.get(repository).getType() != GitRebaseStatus.Type.SUCCESS; + } + }))); + return incompleteRepositories; + } + + @NotNull + private static GitStashChangesSaver newSaver(@NotNull Project project, @NotNull ProgressIndicator indicator) { + Git git = ServiceManager.getService(Git.class); + GitPlatformFacade facade = ServiceManager.getService(GitPlatformFacade.class); + return new GitStashChangesSaver(project, facade, git, indicator, "Uncommitted changes before rebase"); + } + + @NotNull + private static Map readInitialHeadPositions(@NotNull Collection repositories) { + GitUtil.updateRepositories(repositories); + return ContainerUtil.map2Map(repositories, new Function>() { + @Override + public Pair fun(@NotNull GitRepository repository) { + String currentRevision = repository.getCurrentRevision(); + LOG.debug("Current revision in [" + repository.getRoot().getName() + "] is [" + currentRevision + "]"); + return Pair.create(repository, currentRevision); + } + }); + } + + @NotNull + private Collection getOngoingRebases() { + return ContainerUtil.filter(myStatuses.keySet(), new Condition() { + @Override + public boolean value(@NotNull GitRepository repository) { + return myStatuses.get(repository).getType() == GitRebaseStatus.Type.SUSPENDED; + } + }); + } + + private boolean singleOngoingRebase() { + Collection ongoingRebases = getOngoingRebases(); + if (ongoingRebases.size() > 1) { + LOG.warn("Invalid rebase spec: rebase is in progress in " + getShortNames(ongoingRebases)); + return false; + } + return true; + } + + private boolean rebaseStatusesMatch() { + for (GitRepository repository : myStatuses.keySet()) { + GitRebaseStatus.Type savedStatus = myStatuses.get(repository).getType(); + if (repository.isRebaseInProgress() && savedStatus != GitRebaseStatus.Type.SUSPENDED) { + LOG.warn("Invalid rebase spec: rebase is in progress in " + + DvcsUtil.getShortRepositoryName(repository) + ", but it is saved as " + savedStatus); + return false; + } + else if (!repository.isRebaseInProgress() && savedStatus == GitRebaseStatus.Type.SUSPENDED) { + LOG.warn("Invalid rebase spec: rebase is not in progress in " + DvcsUtil.getShortRepositoryName(repository)); + return false; + } + } + return true; + } + + @Override + public String toString() { + String initialHeadPositions = StringUtil.join(myInitialHeadPositions.keySet(), new Function() { + @Override + public String fun(@NotNull GitRepository repository) { + return DvcsUtil.getShortRepositoryName(repository) + ": " + myInitialHeadPositions.get(repository); + } + }, ", "); + String statuses = StringUtil.join(myStatuses.keySet(), new Function() { + @Override + public String fun(GitRepository repository) { + return DvcsUtil.getShortRepositoryName(repository) + ": " + myStatuses.get(repository); + } + }, ", "); + return String.format("{Params: [%s].\nInitial positions: %s.\nStatuses: %s.\nSaver: %s}", myParams, initialHeadPositions, statuses, mySaver); + } +} diff --git a/plugins/git4idea/src/git4idea/rebase/GitRebaseStatus.java b/plugins/git4idea/src/git4idea/rebase/GitRebaseStatus.java new file mode 100644 index 000000000000..7bfbd9acb74f --- /dev/null +++ b/plugins/git4idea/src/git4idea/rebase/GitRebaseStatus.java @@ -0,0 +1,77 @@ +/* + * Copyright 2000-2015 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 git4idea.rebase; + +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; +import java.util.Collections; + +class GitRebaseStatus { + + enum Type { + /** + * Rebase has completed successfully. + */ + SUCCESS, + /** + * Rebase started, and some commits were already applied, + * but then rebase stopped because of conflicts, or because of an error.
+ * Such rebase can be retried/continued by calling `git rebase --continue/--skip`, or + * it can be aborted by calling `git rebase --abort`. + */ + SUSPENDED, + /** + * Rebase started, but immediately stopped because of an error at the very beginning. + * As opposed to {@link #SUSPENDED}, no commits have been applied yet.
+ * Retrying such rebase requires calling `git rebase ` again, + * there is nothing to abort. + */ + ERROR, + /** + * Rebase hasn't started yet. + */ + NOT_STARTED + } + + @NotNull private final Type myType; + @NotNull private final Collection mySkippedCommits; + + @NotNull + static GitRebaseStatus notStarted() { + return new GitRebaseStatus(Type.NOT_STARTED, Collections.emptyList()); + } + + GitRebaseStatus(@NotNull Type type, @NotNull Collection skippedCommits) { + myType = type; + mySkippedCommits = skippedCommits; + } + + @NotNull + Collection getSkippedCommits() { + return mySkippedCommits; + } + + @NotNull + Type getType() { + return myType; + } + + @Override + public String toString() { + return myType.toString(); + } +} diff --git a/plugins/git4idea/src/git4idea/rebase/GitRebaseUtils.java b/plugins/git4idea/src/git4idea/rebase/GitRebaseUtils.java index 5d4963afc52c..7fbe984bb898 100644 --- a/plugins/git4idea/src/git4idea/rebase/GitRebaseUtils.java +++ b/plugins/git4idea/src/git4idea/rebase/GitRebaseUtils.java @@ -19,11 +19,13 @@ import com.intellij.dvcs.repo.Repository; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Condition; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.vcs.VcsNotifier; import com.intellij.openapi.vfs.CharsetToolkit; import com.intellij.openapi.vfs.VfsUtilCore; import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.util.containers.ContainerUtil; import git4idea.GitRevisionNumber; import git4idea.GitUtil; import git4idea.branch.GitRebaseParams; @@ -38,13 +40,14 @@ import java.io.File; import java.io.FileInputStream; import java.io.InputStreamReader; import java.util.Collection; +import java.util.Collections; import java.util.List; -import java.util.Map; /** * The utilities related to rebase functionality */ public class GitRebaseUtils { + public static final String CONTINUE_PROGRESS_TITLE = "Continue Rebase Process..."; /** * The logger instance */ @@ -61,23 +64,76 @@ public class GitRebaseUtils { @NotNull final GitRebaseParams params, @NotNull final ProgressIndicator indicator) { if (!isRebaseAllowed(project, repositories)) return; // TODO maybe move to the outside - new GitRebaseProcess(project, repositories, params, indicator).rebase(); + new GitRebaseProcess(project, GitRebaseSpec.forNewRebase(project, params, repositories, indicator), null).rebase(); + } + + public static void continueRebase(@NotNull Project project) { + GitRebaseSpec spec = GitUtil.getRepositoryManager(project).getOngoingRebaseSpec(); + if (spec != null) { + new GitRebaseProcess(project, spec, GitRebaseResumeMode.CONTINUE).rebase(); + } + else { + LOG.warn("Refusing to continue: no rebase spec"); + VcsNotifier.getInstance(project).notifyError("Can't Continue Rebase", "No rebase in progress"); + } + } + + public static void continueRebase(@NotNull Project project, @NotNull GitRepository repository, @NotNull ProgressIndicator indicator) { + GitRebaseSpec spec = GitRebaseSpec.forResumeInSingleRepository(project, repository, indicator); + if (spec != null) { + new GitRebaseProcess(project, spec, GitRebaseResumeMode.CONTINUE).rebase(); + } + else { + LOG.warn("Refusing to continue: no rebase spec"); + VcsNotifier.getInstance(project).notifyError("Can't Continue Rebase", "No rebase in progress"); + } + } + + public static void skipRebase(@NotNull Project project) { + GitRebaseSpec spec = GitUtil.getRepositoryManager(project).getOngoingRebaseSpec(); + if (spec != null) { + new GitRebaseProcess(project, spec, GitRebaseResumeMode.SKIP).rebase(); + } + else { + LOG.warn("Refusing to skip: no rebase spec"); + VcsNotifier.getInstance(project).notifyError("Can't Continue Rebase", "No rebase in progress"); + } + } + + public static void skipRebase(@NotNull Project project, @NotNull GitRepository repository, @NotNull ProgressIndicator indicator) { + GitRebaseSpec spec = GitRebaseSpec.forResumeInSingleRepository(project, repository, indicator); + if (spec != null) { + new GitRebaseProcess(project, spec, GitRebaseResumeMode.SKIP).rebase(); + } + else { + LOG.warn("Refusing to skip: no rebase spec"); + VcsNotifier.getInstance(project).notifyError("Can't Continue Rebase", "No rebase in progress"); + } } /** - * Abort the ongoing rebase process in the {@code repositoryToAbort}, - * and optionally rollback rebase which has already successfully completed in some other repositories. - * - * @param repositoryToAbort Repository to perform {@code git rebase --abort}. - * @param repositoriesToRollback Repositories to rollback the successful rebase, together with commit hashes which were HEAD revisions - * before that successful rebase started - these are the revisions which the method will rollback to - * via {@code git reset --keep}. + * Automatically detects the ongoing rebase process in the project and abort it. + * Optionally rollbacks repositories which were already rebased during that detected multi-root rebase process. + *

+ * Does nothing if no information about ongoing rebase is available, or if this information has become obsolete. */ - public static void abort(@NotNull final Project project, - @Nullable final GitRepository repositoryToAbort, - @NotNull final Map repositoriesToRollback, - @NotNull ProgressIndicator progressIndicator) { - new GitAbortRebaseProcess(project, repositoryToAbort, repositoriesToRollback, progressIndicator, null).abortWithConfirmation(); + public static void abort(@NotNull Project project, @NotNull ProgressIndicator indicator) { + GitRebaseSpec spec = GitUtil.getRepositoryManager(project).getOngoingRebaseSpec(); + if (spec != null) { + new GitAbortRebaseProcess(project, spec.getOngoingRebase(), + spec.getHeadPositionsToRollback(), indicator, spec.getSaver()).abortWithConfirmation(); + } + else { + LOG.warn("Refusing to abort: no rebase spec"); + VcsNotifier.getInstance(project).notifyError("Can't Abort Rebase", "No rebase in progress"); + } + } + + /** + * Abort the ongoing rebase process in the given repository. + */ + public static void abort(@NotNull final Project project, @Nullable final GitRepository repository, @NotNull ProgressIndicator indicator) { + new GitAbortRebaseProcess(project, repository, Collections.emptyMap(), indicator, null).abortWithConfirmation(); } private static boolean isRebaseAllowed(@NotNull Project project, @NotNull Collection repositories) { @@ -215,6 +271,16 @@ public class GitRebaseUtils { return word.endsWith("e") ? word + "d" : word + "ed"; } + @NotNull + public static Collection getRebasingRepositories(@NotNull Project project) { + return ContainerUtil.filter(GitUtil.getRepositories(project), new Condition() { + @Override + public boolean value(@NotNull GitRepository repository) { + return repository.getState() == Repository.State.REBASING; + } + }); + } + /** * Short commit info */ diff --git a/plugins/git4idea/src/git4idea/rebase/GitSuccessfulRebase.java b/plugins/git4idea/src/git4idea/rebase/GitSuccessfulRebase.java new file mode 100644 index 000000000000..6ffda6b88aa1 --- /dev/null +++ b/plugins/git4idea/src/git4idea/rebase/GitSuccessfulRebase.java @@ -0,0 +1,89 @@ +/* + * Copyright 2000-2015 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 git4idea.rebase; + +import com.intellij.openapi.util.text.StringUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.List; + +class GitSuccessfulRebase extends GitRebaseStatus { + private final SuccessType mySuccessType; + + private GitSuccessfulRebase(@NotNull SuccessType successType, @NotNull Collection skippedCommits) { + super(Type.SUCCESS, skippedCommits); + mySuccessType = successType; + } + + @NotNull + public SuccessType getSuccessType() { + return mySuccessType; + } + + @NotNull + static GitSuccessfulRebase parseFromOutput(@NotNull List output, @NotNull Collection skippedCommits) { + return new GitSuccessfulRebase(SuccessType.fromOutput(output), skippedCommits); + } + + enum SuccessType { + REBASED { + @NotNull + @Override + public String formatMessage(@Nullable String currentBranch, @Nullable String baseBranch) { + return "Rebased" + mention(currentBranch) + (baseBranch == null ? "" : " on " + baseBranch); + } + }, + UP_TO_DATE { + @NotNull + @Override + public String formatMessage(@Nullable String currentBranch, @Nullable String baseBranch) { + String msg = currentBranch != null ? currentBranch + " is up-to-date" : "Up-to-date"; + if (baseBranch != null) msg += " with " + baseBranch; + return msg; + } + }, + FAST_FORWARDED { + @NotNull + @Override + public String formatMessage(@Nullable String currentBranch, @Nullable String baseBranch) { + return "Fast-forwarded" + mention(currentBranch) + (baseBranch == null ? "" : " to " + baseBranch); + } + }; + + @NotNull + private static String mention(@Nullable String currentBranch) { + return currentBranch != null ? " " + currentBranch : ""; + } + + @NotNull + abstract String formatMessage(@Nullable String currentBranch, @Nullable String baseBranch); + + @NotNull + public static SuccessType fromOutput(@NotNull List output) { + for (String line : output) { + if (StringUtil.containsIgnoreCase(line, "Fast-forwarded")) { + return FAST_FORWARDED; + } + if (StringUtil.containsIgnoreCase(line, "is up to date")) { + return UP_TO_DATE; + } + } + return REBASED; + } + } +} diff --git a/plugins/git4idea/src/git4idea/repo/GitRepositoryManager.java b/plugins/git4idea/src/git4idea/repo/GitRepositoryManager.java index b581f9f00ea5..e72f8b8bb2c1 100644 --- a/plugins/git4idea/src/git4idea/repo/GitRepositoryManager.java +++ b/plugins/git4idea/src/git4idea/repo/GitRepositoryManager.java @@ -21,8 +21,10 @@ import com.intellij.dvcs.repo.VcsRepositoryManager; import com.intellij.openapi.project.Project; import git4idea.GitPlatformFacade; import git4idea.GitUtil; +import git4idea.rebase.GitRebaseSpec; import git4idea.ui.branch.GitMultiRootBranchConfig; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.List; @@ -31,6 +33,8 @@ public class GitRepositoryManager extends AbstractRepositoryManager getRepositories() { return getRepositories(GitRepository.class); } + + @Nullable + public GitRebaseSpec getOngoingRebaseSpec() { + GitRebaseSpec rebaseSpec = myOngoingRebaseSpec; + return rebaseSpec != null && rebaseSpec.isValid() ? rebaseSpec : null; + } + + public boolean hasOngoingRebase() { + return getOngoingRebaseSpec() != null; + } + + public void setOngoingRebaseSpec(@Nullable GitRebaseSpec ongoingRebaseSpec) { + myOngoingRebaseSpec = ongoingRebaseSpec != null && ongoingRebaseSpec.isValid() ? ongoingRebaseSpec : null; + } } diff --git a/plugins/git4idea/src/git4idea/stash/GitShelveChangesSaver.java b/plugins/git4idea/src/git4idea/stash/GitShelveChangesSaver.java index bb31c0656715..0a489433befe 100644 --- a/plugins/git4idea/src/git4idea/stash/GitShelveChangesSaver.java +++ b/plugins/git4idea/src/git4idea/stash/GitShelveChangesSaver.java @@ -118,4 +118,9 @@ public class GitShelveChangesSaver extends GitChangesSaver { public void showSavedChanges() { myShelveViewManager.activateView(myShelvedLists.get(myShelvedLists.keySet().iterator().next())); } + + @Override + public String toString() { + return "ShelveChangesSaver. Lists: " + myShelvedLists; + } } diff --git a/plugins/git4idea/src/git4idea/stash/GitStashChangesSaver.java b/plugins/git4idea/src/git4idea/stash/GitStashChangesSaver.java index 06a32772cefb..0d91e388d846 100644 --- a/plugins/git4idea/src/git4idea/stash/GitStashChangesSaver.java +++ b/plugins/git4idea/src/git4idea/stash/GitStashChangesSaver.java @@ -161,6 +161,11 @@ public class GitStashChangesSaver extends GitChangesSaver { } } + @Override + public String toString() { + return "StashChangesSaver. Roots: " + myStashedRoots; + } + private static class UnstashConflictResolver extends GitConflictResolver { private final Set myStashedRoots; diff --git a/plugins/git4idea/tests/git4idea/rebase/GitMultiRepoRebaseTest.kt b/plugins/git4idea/tests/git4idea/rebase/GitMultiRepoRebaseTest.kt index 48f44204970f..686aab9ceb72 100644 --- a/plugins/git4idea/tests/git4idea/rebase/GitMultiRepoRebaseTest.kt +++ b/plugins/git4idea/tests/git4idea/rebase/GitMultiRepoRebaseTest.kt @@ -66,12 +66,11 @@ class GitMultiRepoRebaseTest : GitRebaseBaseTest() { fun `test abort from critical error during rebasing 2nd root, before any commits were applied`() { val localChange = LocalChange(myUltimate, "new.txt", "Some content") - val rebaseProcess = `fail with critical error while rebasing 2nd root`(localChange) + `fail with critical error while rebasing 2nd root`(localChange) assertErrorNotification("Rebase Failed", """ - Rebase failed with error in community:
- $UNKNOWN_ERROR_TEXT + community: $UNKNOWN_ERROR_TEXT
You can retry or abort rebase. $LOCAL_CHANGES_WARNING """) @@ -88,7 +87,7 @@ class GitMultiRepoRebaseTest : GitRebaseBaseTest() { Messages.YES; } - rebaseProcess.abort(null, listOf(myUltimate), EmptyProgressIndicator()) + abortOngoingRebase() assertNotNull(confirmation, "Abort confirmation message was not shown") assertEquals("Incorrect confirmation message text", @@ -102,7 +101,7 @@ class GitMultiRepoRebaseTest : GitRebaseBaseTest() { fun `test abort from critical error while rebasing 2nd root, after some commits were applied`() { val localChange = LocalChange(myUltimate, "new.txt", "Some content") - val rebaseProcess = `fail with critical error while rebasing 2nd root after some commits are applied`(localChange) + `fail with critical error while rebasing 2nd root after some commits are applied`(localChange) myVcsNotifier.lastNotification @@ -112,7 +111,7 @@ class GitMultiRepoRebaseTest : GitRebaseBaseTest() { Messages.YES; } - rebaseProcess.abort(myCommunity, listOf(myUltimate), EmptyProgressIndicator()) + abortOngoingRebase() assertNotNull(confirmation, "Abort confirmation message was not shown") assertEquals("Incorrect confirmation message text", @@ -129,28 +128,28 @@ class GitMultiRepoRebaseTest : GitRebaseBaseTest() { myCommunity.`prepare simple conflict`() myContrib.`diverge feature and master`() - var conflictInUltimate = false - var conflictInCommunity = false + var facedConflictInUltimate = false + var facedConflictInCommunity = false myVcsHelper.onMerge({ - if (!conflictInUltimate) { - myUltimate.assertConflict("c.txt") + assertFalse(facedConflictInCommunity && facedConflictInUltimate) + if (myUltimate.hasConflict("c.txt")) { + assertFalse(facedConflictInUltimate) + facedConflictInUltimate = true assertNoRebaseInProgress(myCommunity) resolveConflicts(myUltimate) - conflictInUltimate = true } - else { - assertFalse(conflictInCommunity) - myCommunity.assertConflict("c.txt") + else if (myCommunity.hasConflict("c.txt")) { + assertFalse(facedConflictInCommunity) + facedConflictInCommunity = true assertNoRebaseInProgress(myUltimate) resolveConflicts(myCommunity) - conflictInCommunity = true } }) rebase("master") - assertTrue(conflictInUltimate) - assertTrue(conflictInCommunity) + assertTrue(facedConflictInUltimate) + assertTrue(facedConflictInCommunity) myAllRepositories.forEach { it.`assert feature rebased on master`() assertNoRebaseInProgress(it) @@ -159,42 +158,80 @@ class GitMultiRepoRebaseTest : GitRebaseBaseTest() { } fun `test retry doesn't touch successful repositories`() { - val rebaseProcess = `fail with critical error while rebasing 2nd root`() + `fail with critical error while rebasing 2nd root`() - rebaseProcess.retry(false) + GitRebaseUtils.continueRebase(myProject) assertSuccessfulNotification("Rebased feature on master") assertAllRebased() assertNoRebaseInProgress(myAllRepositories) } - private fun `fail with critical error while rebasing 2nd root`(localChange: LocalChange? = null): GitRebaseProcess { + public fun `test continue rebase shouldn't attempt to stash`() { + myUltimate.`diverge feature and master`() + myCommunity.`prepare simple conflict`() + myContrib.`diverge feature and master`() + + `do nothing on merge`() + rebase("master") + GitRebaseUtils.continueRebase(myProject) + + `assert conflict not resolved notification`() + assertNotRebased("feature", "master", myCommunity) + } + + public fun `test continue rebase with unresolved conflicts should show merge dialog`() { + myUltimate.`diverge feature and master`() + myCommunity.`prepare simple conflict`() + myContrib.`diverge feature and master`() + + `do nothing on merge`() + rebase("master") + + var mergeDialogShown = false + myVcsHelper.onMerge { + mergeDialogShown = true + resolveConflicts(myCommunity) + } + GitRebaseUtils.continueRebase(myProject) + + assertTrue("Merge dialog was not shown", mergeDialogShown) + assertAllRebased() + } + + private fun `fail with critical error while rebasing 2nd root`(localChange: LocalChange? = null) { myAllRepositories.forEach { it.`diverge feature and master`() } localChange?.generate() myGit.setShouldRebaseFail { it == myCommunity } - val rebaseProcess = rebase("master") - - myGit.setShouldRebaseFail { false } - return rebaseProcess + try { + rebase("master") + } + finally { + myGit.setShouldRebaseFail { false } + } } - private fun `fail with critical error while rebasing 2nd root after some commits are applied`(localChange: LocalChange? = null): GitRebaseProcess { + private fun `fail with critical error while rebasing 2nd root after some commits are applied`(localChange: LocalChange? = null) { myUltimate.`diverge feature and master`() myCommunity.`make rebase fail on 2nd commit`() myContrib.`diverge feature and master`() localChange?.generate() - val rebaseProcess = rebase("master") - - myGit.setShouldRebaseFail { false } - return rebaseProcess + try { + rebase("master") + } + finally { + myGit.setShouldRebaseFail { false } + } } - private fun rebase(onto: String): GitRebaseProcess { - val rebaseProcess = GitRebaseProcess(myProject, myAllRepositories, GitRebaseParams(onto), EmptyProgressIndicator()) - rebaseProcess.rebase() - return rebaseProcess + private fun rebase(onto: String) { + GitTestingRebaseProcess(myProject, GitRebaseParams(onto), myAllRepositories).rebase() + } + + private fun abortOngoingRebase() { + GitRebaseUtils.abort(myProject, EmptyProgressIndicator()) } private fun assertAllRebased() { diff --git a/plugins/git4idea/tests/git4idea/rebase/GitRebaseBaseTest.kt b/plugins/git4idea/tests/git4idea/rebase/GitRebaseBaseTest.kt index b8ec15cb1319..2108cd3f498e 100644 --- a/plugins/git4idea/tests/git4idea/rebase/GitRebaseBaseTest.kt +++ b/plugins/git4idea/tests/git4idea/rebase/GitRebaseBaseTest.kt @@ -16,9 +16,12 @@ package git4idea.rebase import com.intellij.dvcs.repo.Repository +import com.intellij.openapi.progress.EmptyProgressIndicator +import com.intellij.openapi.project.Project import com.intellij.openapi.vcs.AbstractVcsHelper import com.intellij.openapi.vcs.Executor import git4idea.GitUtil +import git4idea.branch.GitRebaseParams import git4idea.repo.GitRepository import git4idea.test.* import git4idea.test.GitExecutor.cd @@ -38,7 +41,7 @@ abstract class GitRebaseBaseTest : GitPlatformTest() { override fun createRepository(rootDir: String) = GitTestUtil.createRepository(myProject, rootDir, false) - override fun getDebugLogCategories() = listOf("#" + GitRebaseProcess::class.java.name) + override fun getDebugLogCategories() = listOf("#git4idea.rebase") protected fun GitRepository.`diverge feature and master`() { build(this) { @@ -123,6 +126,10 @@ abstract class GitRebaseBaseTest : GitPlatformTest() { git("add -u .") } + protected fun `do nothing on merge`() { + myVcsHelper.onMerge{} + } + protected fun GitRepository.`assert feature rebased on master`() { assertRebased(this, "feature", "master") } @@ -133,7 +140,7 @@ abstract class GitRebaseBaseTest : GitPlatformTest() { protected fun assertRebased(repository: GitRepository, feature: String, master: String) { cd(repository) - assertEquals(git("rev-parse " + master), git("merge-base $feature $master")) + assertEquals("$feature is not rebased on $master!", git("rev-parse " + master), git("merge-base $feature $master")) } protected fun assertNotRebased(feature: String, master: String, repository: GitRepository) { @@ -160,14 +167,66 @@ abstract class GitRebaseBaseTest : GitPlatformTest() { assertEquals("There should be no local changes!", "", gitStatus()) } - protected fun GitRepository.isDirty(): Boolean { - return !gitStatus().isEmpty(); + protected fun GitRepository.hasConflict(file: String) : Boolean { + return ("UU " + file).equals(git(this, "status --porcelain")); } - private fun GitRepository.gitStatus() = git(this, "status --porcelain").trim() - protected fun GitRepository.assertConflict(file: String) { - assertEquals("UU " + file, git(this, "status --porcelain")) + assertTrue("Conflict was expected for " + file + ", but git status doesn't show it: \n${git(this, "status --porcelain")}", + hasConflict(file)) + } + + protected fun `assert conflict not resolved notification`() { + assertWarningNotification("Rebase Suspended", + """ + You have to resolve the conflicts and continue rebase.
+ If you want to start from the beginning, you can abort rebase. + """) + } + + protected fun `assert conflict not resolved notification with link to stash`() { + assertWarningNotification("Rebase Suspended", + """ + You have to resolve the conflicts and continue rebase.
+ If you want to start from the beginning, you can abort rebase.
+ $LOCAL_CHANGES_WARNING + """) + } + + protected fun `assert unknown error notification`() { + assertErrorNotification("Rebase Failed", + """ + $UNKNOWN_ERROR_TEXT
+ Retry. + """) + } + + protected fun `assert unknown error notification with link to abort`(afterContinue : Boolean = false) { + val expectedTitle = if (afterContinue) "Continue Rebase Failed" else "Rebase Failed"; + assertErrorNotification(expectedTitle, + """ + $UNKNOWN_ERROR_TEXT
+ You can retry or abort rebase. + """) + } + + protected fun `assert unknown error notification with link to stash`() { + assertErrorNotification("Rebase Failed", + """ + $UNKNOWN_ERROR_TEXT
+ Retry.
+ $LOCAL_CHANGES_WARNING + """) + } + + protected fun `assert error about unstaged file before continue rebase`(file : String) { + assertErrorNotification("Continue Rebase Failed", + """ + $file: needs update + You must edit all merge conflicts + and then mark them as resolved using git add + You can retry or abort rebase. + """) } class LocalChange(val repository: GitRepository, val filePath: String, val content: String = "Some content") { @@ -187,4 +246,22 @@ abstract class GitRebaseBaseTest : GitPlatformTest() { assertEquals("Incorrect content of the file [$file]", content, Executor.cat(file)) } } + + protected open class GitTestingRebaseProcess(project: Project, params: GitRebaseParams, val repositories: Collection) : + GitRebaseProcess(project, GitRebaseSpec.forNewRebase(project, params, repositories, EmptyProgressIndicator()), null) { + + constructor(project: Project, params: GitRebaseParams, repository: GitRepository) : this(project, params, listOf(repository)) + + override fun getDirtyRoots(repositories: Collection): Collection { + return repositories.filter { it.isDirty() } + } + + protected fun GitRepository.isDirty(): Boolean { + return !gitStatus().isEmpty(); + } + } } + +private fun GitRepository.gitStatus() = git(this, "status --porcelain").trim() + + diff --git a/plugins/git4idea/tests/git4idea/rebase/GitSingleRepoRebaseTest.kt b/plugins/git4idea/tests/git4idea/rebase/GitSingleRepoRebaseTest.kt index 28a045293b93..66aca707a3c5 100644 --- a/plugins/git4idea/tests/git4idea/rebase/GitSingleRepoRebaseTest.kt +++ b/plugins/git4idea/tests/git4idea/rebase/GitSingleRepoRebaseTest.kt @@ -131,6 +131,19 @@ class GitSingleRepoRebaseTest : GitRebaseBaseTest() { myRepo.assertRebaseInProgress() } + fun `test skip if user decides to skip`() { + myRepo.`prepare simple conflict`() + `do nothing on merge`() + + rebaseOnMaster() + + GitRebaseUtils.skipRebase(myProject) + + assertSuccessfulNotification("Rebased feature on master") + myRepo.`assert feature rebased on master`() + assertNoRebaseInProgress(myRepo) + } + fun `test rebase failed for unknown reason`() { myRepo.`diverge feature and master`() myGit.setShouldRebaseFail { true } @@ -149,19 +162,23 @@ class GitSingleRepoRebaseTest : GitRebaseBaseTest() { resolveConflicts(myRepo) myGit.setShouldRebaseFail { true } - val continueRebase = GitRebaseParams("master").withMode(GitRebaseParams.Mode.CONTINUE) - GitTestingRebaseProcess(continueRebase).rebase() - `assert unknown error notification with link to abort`() + GitRebaseUtils.continueRebase(myProject) + + `assert unknown error notification with link to abort`(true) myRepo.`assert feature not rebased on master`() myRepo.assertRebaseInProgress() } - fun `test local changes auto-saved`() { + fun `test local changes auto-saved initially`() { myRepo.`diverge feature and master`() val localChange = LocalChange(myRepo, "new.txt").generate() - rebaseOnMaster() + object : GitTestingRebaseProcess(myProject, GitRebaseParams("master"), myRepo) { + override fun getDirtyRoots(repositories: Collection): Collection { + return listOf(myRepo) + } + }.rebase() assertSuccessfulNotification("Rebased feature on master") assertRebased(myRepo, "feature", "master") @@ -173,7 +190,7 @@ class GitSingleRepoRebaseTest : GitRebaseBaseTest() { myRepo.`diverge feature and master`() val localChange = LocalChange(myRepo, "new.txt").generate() - object : GitTestingRebaseProcess(GitRebaseParams("master")) { + object : GitTestingRebaseProcess(myProject, GitRebaseParams("master"), myRepo) { override fun getDirtyRoots(repositories: Collection): Collection { return emptyList() } @@ -195,7 +212,7 @@ class GitSingleRepoRebaseTest : GitRebaseBaseTest() { assertErrorNotification("Rebase Failed", """ - Rebase failed with error: $UNKNOWN_ERROR_TEXT
+ $UNKNOWN_ERROR_TEXT
Retry.
Note that some local changes were stashed before rebase. """) @@ -222,12 +239,12 @@ class GitSingleRepoRebaseTest : GitRebaseBaseTest() { var attempt = 0 myGit.setShouldRebaseFail { attempt == 0 } - val rebaseProcess = rebaseOnMaster() + rebaseOnMaster() attempt++ myVcsNotifier.lastNotification - rebaseProcess.retry(false) + GitRebaseUtils.continueRebase(myProject) assertNoRebaseInProgress(myRepo) myRepo.`assert feature rebased on master`() @@ -240,11 +257,11 @@ class GitSingleRepoRebaseTest : GitRebaseBaseTest() { myVcsHelper.onMerge {} myDialogManager.onMessage { Messages.YES } - val rebaseProcess = rebaseOnMaster() + rebaseOnMaster() `assert conflict not resolved notification with link to stash`() - rebaseProcess.abort(myRepo, emptyList(), EmptyProgressIndicator()) + GitRebaseUtils.abort(myProject, EmptyProgressIndicator()) assertNoRebaseInProgress(myRepo) myRepo.`assert feature not rebased on master`() @@ -257,12 +274,12 @@ class GitSingleRepoRebaseTest : GitRebaseBaseTest() { myVcsHelper.onMerge {} myDialogManager.onMessage { Messages.YES } - val rebaseProcess = rebaseOnMaster() + rebaseOnMaster() `assert conflict not resolved notification with link to stash`() myGit.setShouldRebaseFail { true } - rebaseProcess.abort(myRepo, emptyList(), EmptyProgressIndicator()) + GitRebaseUtils.abort(myProject, EmptyProgressIndicator()) myRepo.assertRebaseInProgress() myRepo.`assert feature not rebased on master`() @@ -276,7 +293,7 @@ class GitSingleRepoRebaseTest : GitRebaseBaseTest() { // git rebase --continue should be either called from a commit dialog, either from the GitRebaseProcess. // both should prepare the working tree themselves by adding all necessary changes to the index. - fun `test local changes should not be saved when continue rebase`() { + fun `test local changes in the conflicting file should lead to error on continue rebase`() { myRepo.`prepare simple conflict`() `do nothing on merge`() @@ -288,21 +305,61 @@ class GitSingleRepoRebaseTest : GitRebaseBaseTest() { file("c.txt").append("more changes after resolving") // forget to git add afterwards - val continueRebase = GitRebaseParams("master").withMode(GitRebaseParams.Mode.CONTINUE) - GitTestingRebaseProcess(continueRebase).rebase() + GitRebaseUtils.continueRebase(myProject) - assertErrorNotification("Rebase Failed", - """ - Rebase failed with error: c.txt: needs update - You must edit all merge conflicts - and then mark them as resolved using git add - You can retry or abort rebase. - """) + `assert error about unstaged file before continue rebase`("c.txt") + myRepo.`assert feature not rebased on master`() + myRepo.assertRebaseInProgress() + } + + fun `test local changes in some other file should lead to error on continue rebase`() { + build { + master { + 0("d.txt") + 1("c.txt") + 2("c.txt") + } + feature(1) { + 3("c.txt") + } + } + + `do nothing on merge`() + + rebaseOnMaster() + myRepo.assertConflict("c.txt") + + //manually resolve conflicts + resolveConflicts(myRepo) + // add more changes to some other file + + file("d.txt").append("more changes after resolving") + + GitRebaseUtils.continueRebase(myProject) + + `assert error about unstaged file before continue rebase`("d.txt") myRepo.`assert feature not rebased on master`() myRepo.assertRebaseInProgress() } + fun `test unresolved conflict should lead to conflict resolver with continue rebase`() { + myRepo.`prepare simple conflict`() + `do nothing on merge`() + + rebaseOnMaster() + myRepo.assertConflict("c.txt") + + myVcsHelper.onMerge { + resolveConflicts(myRepo) + } + GitRebaseUtils.continueRebase(myProject) + + assertSuccessfulNotification("Rebased feature on master") + myRepo.`assert feature rebased on master`() + assertNoRebaseInProgress(myRepo) + } + fun `test skipped commit`() { build { master { @@ -315,7 +372,7 @@ class GitSingleRepoRebaseTest : GitRebaseBaseTest() { } } - val hash2skip = DvcsUtil.getShortHash(git("log -2 --pretty=%H").lines().get(1)) + val hash2skip = DvcsUtil.getShortHash(git("log -2 --pretty=%H").lines()[1]) myVcsHelper.onMerge { file("c.txt").write("base\nmaster") @@ -339,63 +396,8 @@ class GitSingleRepoRebaseTest : GitRebaseBaseTest() { build(myRepo, f) } - private fun `do nothing on merge`() { - myVcsHelper.onMerge{} - } - - private open inner class GitTestingRebaseProcess(params: GitRebaseParams) : - GitRebaseProcess(myProject, listOf(myRepo), params, EmptyProgressIndicator()) { - override fun getDirtyRoots(repositories: Collection): Collection { - return if (myRepo.isDirty()) listOf(myRepo) else emptyList() - } - } - - private fun rebaseOnMaster() : GitRebaseProcess { - val rebaseProcess = GitTestingRebaseProcess(GitRebaseParams("master")) - rebaseProcess.rebase() - return rebaseProcess - } - - private fun `assert conflict not resolved notification`() { - assertWarningNotification("Rebase Suspended", - """ - You have to resolve the conflicts and continue rebase.
- If you want to start from the beginning, you can abort rebase. - """) - } - - private fun `assert conflict not resolved notification with link to stash`() { - assertWarningNotification("Rebase Suspended", - """ - You have to resolve the conflicts and continue rebase.
- If you want to start from the beginning, you can abort rebase.
- $LOCAL_CHANGES_WARNING - """) - } - - private fun `assert unknown error notification`() { - assertErrorNotification("Rebase Failed", - """ - Rebase failed with error: $UNKNOWN_ERROR_TEXT
- Retry. - """) - } - - private fun `assert unknown error notification with link to abort`() { - assertErrorNotification("Rebase Failed", - """ - Rebase failed with error: $UNKNOWN_ERROR_TEXT
- You can retry or abort rebase. - """) - } - - private fun `assert unknown error notification with link to stash`() { - assertErrorNotification("Rebase Failed", - """ - Rebase failed with error: $UNKNOWN_ERROR_TEXT
- Retry.
- $LOCAL_CHANGES_WARNING - """) + private fun rebaseOnMaster() { + GitTestingRebaseProcess(myProject, GitRebaseParams("master"), myRepo).rebase() } } diff --git a/plugins/git4idea/tests/git4idea/test/GitPlatformTest.kt b/plugins/git4idea/tests/git4idea/test/GitPlatformTest.kt index 3dda78912d64..278ec57a717c 100644 --- a/plugins/git4idea/tests/git4idea/test/GitPlatformTest.kt +++ b/plugins/git4idea/tests/git4idea/test/GitPlatformTest.kt @@ -226,6 +226,7 @@ abstract class GitPlatformTest : PlatformTestCase() { protected fun assertErrorNotification(title: String, message: String) : Notification { val notification = myVcsNotifier.lastNotification + assertNotNull("No notification was shown", notification) GitTestUtil.assertNotification(NotificationType.ERROR, title, message, notification) return notification } diff --git a/plugins/git4idea/tests/git4idea/test/GitTestUtil.java b/plugins/git4idea/tests/git4idea/test/GitTestUtil.java index 4244d9410e86..4e17719bd23c 100644 --- a/plugins/git4idea/tests/git4idea/test/GitTestUtil.java +++ b/plugins/git4idea/tests/git4idea/test/GitTestUtil.java @@ -168,9 +168,9 @@ public class GitTestUtil { @NotNull String title, @NotNull String content, @NotNull Notification actual) { - assertEquals("Incorrect type of notification: " + tos(actual), type, actual.getType()); - assertEquals(title, actual.getTitle()); - assertEquals(cleanupForAssertion(content), cleanupForAssertion(actual.getContent())); + assertEquals("Incorrect notification type: " + tos(actual), type, actual.getType()); + assertEquals("Incorrect notification title: " + tos(actual), title, actual.getTitle()); + assertEquals("Incorrect notification content: " + tos(actual), cleanupForAssertion(content), cleanupForAssertion(actual.getContent())); } @NotNull diff --git a/plugins/git4idea/tests/git4idea/test/TestGit.kt b/plugins/git4idea/tests/git4idea/test/TestGit.kt index 11c582178e1b..14a7fa297d11 100644 --- a/plugins/git4idea/tests/git4idea/test/TestGit.kt +++ b/plugins/git4idea/tests/git4idea/test/TestGit.kt @@ -44,10 +44,27 @@ class TestGitImpl : GitImpl() { } override fun rebase(repository: GitRepository, params: GitRebaseParams, vararg listeners: GitLineHandlerListener): GitCommandResult { - return if (myRebaseShouldFail(repository)) - GitCommandResult(false, 128, listOf("fatal: error: $UNKNOWN_ERROR_TEXT"), emptyList(), null) - else + return failOrCall(repository) { super.rebase(repository, params, *listeners) + } + } + + override fun rebaseAbort(repository: GitRepository, vararg listeners: GitLineHandlerListener?): GitCommandResult { + return failOrCall(repository) { + super.rebaseAbort(repository, *listeners) + } + } + + override fun rebaseContinue(repository: GitRepository, vararg listeners: GitLineHandlerListener?): GitCommandResult { + return failOrCall(repository) { + super.rebaseContinue(repository, *listeners) + } + } + + override fun rebaseSkip(repository: GitRepository, vararg listeners: GitLineHandlerListener?): GitCommandResult { + return failOrCall(repository) { + super.rebaseSkip(repository, *listeners) + } } fun setShouldRebaseFail(shouldFail: (GitRepository) -> Boolean) { @@ -62,6 +79,17 @@ class TestGitImpl : GitImpl() { myRebaseShouldFail = { false } myPushHandler = { null } } + + private fun failOrCall(repository: GitRepository, delegate: () -> GitCommandResult): GitCommandResult { + return if (myRebaseShouldFail(repository)) { + fatalResult() + } + else { + delegate() + } + } + + private fun fatalResult() = GitCommandResult(false, 128, listOf("fatal: error: $UNKNOWN_ERROR_TEXT"), emptyList(), null) }