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
This commit is contained in:
Kirill Likhodedov
2016-01-21 18:53:14 +03:00
parent 4b65d4c70e
commit 3ee8e8adb5
26 changed files with 1366 additions and 680 deletions
@@ -1027,4 +1027,19 @@ public class GitUtil {
if (repositories.isEmpty()) return "";
return " in " + joinShortNames(repositories, limit);
}
public static void updateRepositories(@NotNull Collection<GitRepository> repositories) {
for (GitRepository repository : repositories) {
repository.update();
}
}
public static boolean hasGitRepositories(@NotNull Project project) {
return !getRepositories(project).isEmpty();
}
@NotNull
public static Collection<GitRepository> getRepositories(@NotNull Project project) {
return getRepositoryManager(project).getRepositories();
}
}
@@ -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<GitRepository> repositories) {
GitRepository firstRepo = assertNotNull(ContainerUtil.getFirstItem(repositories));
if (repositories.size() == 1) return firstRepo;
ArrayList<VirtualFile> 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
}
}
@@ -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<VirtualFile> gitRoots, VirtualFile defaultRoot) {
for (Iterator<VirtualFile> 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);
}
}
@@ -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<VirtualFile> gitRoots,
@NotNull VirtualFile defaultRoot,
Set<VirtualFile> affectedRoots,
List<VcsException> exceptions) throws VcsException {
// remote all roots where there are no rebase in progress
for (Iterator<VirtualFile> 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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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<String> getCommandLineArguments() {
if (myMode != Mode.STANDARD) {
return singletonList(myMode.asArgument());
}
public List<String> asCommandLineArguments() {
List<String> 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(), " ");
}
}
@@ -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);
}
@@ -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,
@@ -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<AbortChoice> 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",
@@ -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'?";
@@ -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<GitRepository> 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<GitRepository, SuccessType> mySuccessfulRepositories;
@NotNull private final Map<GitRepository, String> myInitialHeadPositions;
@NotNull private final ProgressManager myProgressManager;
@NotNull private final MultiMap<GitRepository, GitRebaseUtils.CommitInfo> mySkippedCommits;
protected GitRebaseProcess(@NotNull Project project,
@NotNull List<GitRepository> repositories,
@NotNull GitRebaseParams params,
@NotNull ProgressIndicator indicator) {
this(project, repositories, params, newSaver(project, indicator),
MultiMap.<GitRepository, GitRebaseUtils.CommitInfo>create(), Collections.<GitRepository, SuccessType>emptyMap());
}
private GitRebaseProcess(@NotNull Project project,
@NotNull List<GitRepository> allRepositories,
@NotNull GitRebaseParams params,
@NotNull GitChangesSaver saver,
@NotNull MultiMap<GitRepository, GitRebaseUtils.CommitInfo> skippedCommits,
@NotNull Map<GitRepository, SuccessType> 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<GitRepository> successfulRepositories,
@NotNull ProgressIndicator indicator) {
new GitAbortRebaseProcess(myProject, repositoryToAbort, ContainerUtil.filter(myInitialHeadPositions, new Condition<GitRepository>() {
@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.
* <ul>
* <li>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.</li>
* <li>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.</li>
* <li>For all remaining repositories rebase on which didn't start yet, it calls {@code git rebase <original parameters>}</li>
* </ul>
*/
private void doRebase() {
LOG.debug("Started rebase.");
Map<GitRepository, SuccessType> successfulWithTypes = ContainerUtil.newLinkedHashMap(mySuccessfulRepositories);
List<GitRepository> repositories = ContainerUtil.newArrayList(myAllRepositories);
repositories.removeAll(successfulWithTypes.keySet());
ListIterator<GitRepository> iterator = repositories.listIterator();
LOG.info("Started rebase");
LOG.debug("Started rebase with the following spec: " + myRebaseSpec);
Map<GitRepository, GitRebaseStatus> statuses = newLinkedHashMap(myRebaseSpec.getStatuses());
Collection<GitRepository> toRefresh = newLinkedHashSet();
List<GitRepository> repositoriesToRebase = myRebaseSpec.getIncompleteRepositories();
AccessToken token = DvcsUtil.workingTreeChangeStarted(myProject);
try {
if (!saveDirtyRootsInitially(repositories)) return;
if (!saveDirtyRootsInitially(repositoriesToRebase)) return;
GitRepository facedDirtyError = null;
Map<GitRepository, GitRebaseParams.Mode> 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<GitRepository> 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<GitRepository, GitRebaseStatus> 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<GitRepository, GitSuccessfulRebase> alreadyRebased) {
VirtualFile root = repository.getRoot();
String repoName = getShortRepositoryName(repository);
LOG.info("Rebasing root " + repoName + ", mode: " + notNull(customMode, "standard"));
Collection<GitRebaseUtils.CommitInfo> skippedCommits = newArrayList();
MultiMap<GitRepository, GitRebaseUtils.CommitInfo> 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<GitRepository, String> readInitialHeadPositions(@NotNull Collection<GitRepository> repositories) {
updateRepositoriesInfo(repositories);
return ContainerUtil.map2Map(repositories, new Function<GitRepository, Pair<GitRepository, String>>() {
@Override
public Pair<GitRepository, String> 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<GitRepository> getRepositoriesToRefresh(@NotNull final Map<GitRepository, SuccessType> successfulWithTypes,
@NotNull GitRepository latestProcessed) {
Collection<GitRepository> toRefresh = ContainerUtil.newHashSet(ContainerUtil.filter(successfulWithTypes.keySet(),
new Condition<GitRepository>() {
@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<GitRepository> 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<GitRepository> 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<GitRepository> repositories) {
if (myParams.getMode() != GitRebaseParams.Mode.STANDARD) {
LOG.debug("No need to pre-save dirty roots in the " + myParams.getMode() + " mode");
return true;
}
Collection<VirtualFile> rootsToSave = getRootsFromRepositories(getDirtyRoots(repositories));
Collection<GitRepository> repositoriesToSave = filter(repositories, new Condition<GitRepository>() {
@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<VirtualFile> 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<GitRepository> findRootsWithLocalChanges(@NotNull Collection<GitRepository> repositories) {
return ContainerUtil.filter(repositories, new Condition<GitRepository>() {
return filter(repositories, new Condition<GitRepository>() {
@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<GitRepository, SuccessType> 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<GitRepository, GitSuccessfulRebase> successful,
final MultiMap<GitRepository, GitRebaseUtils.CommitInfo> skippedCommits) {
String rebasedBranch = getCommonCurrentBranchNameIfAllTheSame(myRebaseSpec.getAllRepositories());
List<SuccessType> successTypes = map(successful.values(),
new Function<GitSuccessfulRebase, SuccessType>() {
@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<GitRepository> repositories) {
return getItemIfAllTheSame(ContainerUtil.map(repositories, new Function<GitRepository, String>() {
return getItemIfAllTheSame(map(repositories, new Function<GitRepository, String>() {
@Override
public String fun(@NotNull GitRepository repository) {
return repository.getCurrentBranchName();
@@ -340,43 +368,51 @@ public class GitRebaseProcess {
@Contract("_, !null -> !null")
private static <T> T getItemIfAllTheSame(@NotNull Collection<T> 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<GitRepository> successful) {
private void notifyNotAllConflictsResolved(@NotNull GitRepository conflictingRepository,
MultiMap<GitRepository, GitRebaseUtils.CommitInfo> skippedCommits) {
String description = "You have to <a href='resolve'>resolve</a> the conflicts and <a href='continue'>continue</a> rebase.<br/>" +
"If you want to start from the beginning, you can <a href='abort'>abort</a> 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<GitRepository> successful) {
String description = "Rebase failed with error" + mention(currentRepository) + ": " + error + "<br/>" +
@NotNull final Collection<GitRepository> successful,
@NotNull MultiMap<GitRepository, GitRebaseUtils.CommitInfo> skippedCommits) {
String repo = myRepositoryManager.moreThanOneRoot() ? getShortRepositoryName(currentRepository) + ": " : "";
String description = repo + error + "<br/>" +
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<String> untrackedPaths,
@NotNull GitRepository currentRepository,
boolean somethingWasRebased,
@NotNull Collection<GitRepository> successful) {
@NotNull Collection<GitRepository> successful,
MultiMap<GitRepository, GitRebaseUtils.CommitInfo> 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<GitRepository, GitRebaseUtils.CommitInfo> skippedCommits) {
if (skippedCommits.isEmpty()) return "";
String message = "<br/>";
if (mySkippedCommits.values().size() == 1) {
if (skippedCommits.values().size() == 1) {
message += "The following commit was skipped during rebase:<br/>";
}
else {
message += "The following commits were skipped during rebase:<br/>";
}
message += StringUtil.join(mySkippedCommits.values(), new Function<GitRebaseUtils.CommitInfo, String>() {
message += StringUtil.join(skippedCommits.values(), new Function<GitRebaseUtils.CommitInfo, String>() {
@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<GitRepository, GitRebaseUtils.CommitInfo> getSkippedCommits(@NotNull Map<GitRepository, ? extends GitRebaseStatus> statuses) {
MultiMap<GitRepository, GitRebaseUtils.CommitInfo> 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<String> 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<GitRepository, GitSuccessfulRebase> getSuccessfulRepositories(@NotNull Map<GitRepository, GitRebaseStatus> statuses) {
Map<GitRepository, GitSuccessfulRebase> 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<GitRepository> mySuccessful;
private final MultiMap<GitRepository, GitRebaseUtils.CommitInfo> mySkippedCommits;
RebaseNotificationListener(@NotNull GitRepository currentRepository,
boolean somethingWasRebased,
@NotNull Collection<GitRepository> successful) {
MultiMap<GitRepository, GitRebaseUtils.CommitInfo> 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<GitRepository, GitRebaseUtils.CommitInfo> 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<GitRepository>() {
private static GitRepository findRootBySkippedCommit(@NotNull final String hash,
final MultiMap<GitRepository, GitRebaseUtils.CommitInfo> skippedCommits) {
return find(skippedCommits.keySet(), new Condition<GitRepository>() {
@Override
public boolean value(GitRepository repository) {
return ContainerUtil.exists(mySkippedCommits.get(repository), new Condition<GitRebaseUtils.CommitInfo>() {
return exists(skippedCommits.get(repository), new Condition<GitRebaseUtils.CommitInfo>() {
@Override
public boolean value(GitRebaseUtils.CommitInfo info) {
return info.revision.asString().equals(hash);
@@ -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;
}
}
@@ -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<GitRepository, GitRebaseStatus> myStatuses;
@NotNull private final Map<GitRepository, String> myInitialHeadPositions;
@NotNull private final GitChangesSaver mySaver;
private final boolean myShouldBeSaved;
public GitRebaseSpec(@Nullable GitRebaseParams params,
@NotNull Map<GitRepository, GitRebaseStatus> statuses,
@NotNull Map<GitRepository, String> 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<GitRepository> repositories,
@NotNull ProgressIndicator indicator) {
Map<GitRepository, String> initialHeadPositions = readInitialHeadPositions(repositories);
Map<GitRepository, GitRebaseStatus> initialStatusMap = new TreeMap<GitRepository, GitRebaseStatus>(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.<GitRebaseUtils.CommitInfo>emptyList());
return new GitRebaseSpec(null, Collections.singletonMap(repository, suspended),
Collections.<GitRepository, String>emptyMap(), newSaver(project, indicator), false);
}
public boolean isValid() {
return singleOngoingRebase() && rebaseStatusesMatch();
}
@NotNull
public GitChangesSaver getSaver() {
return mySaver;
}
@NotNull
public Collection<GitRepository> getAllRepositories() {
return myStatuses.keySet();
}
@Nullable
public GitRepository getOngoingRebase() {
return ContainerUtil.getFirstItem(getOngoingRebases());
}
@Nullable
public GitRebaseParams getParams() {
return myParams;
}
@NotNull
public Map<GitRepository, GitRebaseStatus> getStatuses() {
return Collections.unmodifiableMap(myStatuses);
}
@NotNull
public Map<GitRepository,String> getHeadPositionsToRollback() {
return ContainerUtil.filter(myInitialHeadPositions, new Condition<GitRepository>() {
@Override
public boolean value(GitRepository repository) {
return myStatuses.get(repository).getType() == GitRebaseStatus.Type.SUCCESS;
}
});
}
@NotNull
public GitRebaseSpec cloneWithNewStatuses(@NotNull Map<GitRepository, GitRebaseStatus> 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. <br/>
* 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<GitRepository> getIncompleteRepositories() {
List<GitRepository> incompleteRepositories = ContainerUtil.newArrayList();
final GitRepository ongoingRebase = getOngoingRebase();
if (ongoingRebase != null) incompleteRepositories.add(ongoingRebase);
incompleteRepositories.addAll(DvcsUtil.sortRepositories(ContainerUtil.filter(myStatuses.keySet(), new Condition<GitRepository>() {
@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<GitRepository, String> readInitialHeadPositions(@NotNull Collection<GitRepository> repositories) {
GitUtil.updateRepositories(repositories);
return ContainerUtil.map2Map(repositories, new Function<GitRepository, Pair<GitRepository, String>>() {
@Override
public Pair<GitRepository, String> 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<GitRepository> getOngoingRebases() {
return ContainerUtil.filter(myStatuses.keySet(), new Condition<GitRepository>() {
@Override
public boolean value(@NotNull GitRepository repository) {
return myStatuses.get(repository).getType() == GitRebaseStatus.Type.SUSPENDED;
}
});
}
private boolean singleOngoingRebase() {
Collection<GitRepository> 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<GitRepository, String>() {
@Override
public String fun(@NotNull GitRepository repository) {
return DvcsUtil.getShortRepositoryName(repository) + ": " + myInitialHeadPositions.get(repository);
}
}, ", ");
String statuses = StringUtil.join(myStatuses.keySet(), new Function<GitRepository, String>() {
@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);
}
}
@@ -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.<br/>
* 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. <br/>
* Retrying such rebase requires calling `git rebase <all params>` again,
* there is nothing to abort.
*/
ERROR,
/**
* Rebase hasn't started yet.
*/
NOT_STARTED
}
@NotNull private final Type myType;
@NotNull private final Collection<GitRebaseUtils.CommitInfo> mySkippedCommits;
@NotNull
static GitRebaseStatus notStarted() {
return new GitRebaseStatus(Type.NOT_STARTED, Collections.<GitRebaseUtils.CommitInfo>emptyList());
}
GitRebaseStatus(@NotNull Type type, @NotNull Collection<GitRebaseUtils.CommitInfo> skippedCommits) {
myType = type;
mySkippedCommits = skippedCommits;
}
@NotNull
Collection<GitRebaseUtils.CommitInfo> getSkippedCommits() {
return mySkippedCommits;
}
@NotNull
Type getType() {
return myType;
}
@Override
public String toString() {
return myType.toString();
}
}
@@ -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.
* <p/>
* 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<GitRepository, String> 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.<GitRepository, String>emptyMap(), indicator, null).abortWithConfirmation();
}
private static boolean isRebaseAllowed(@NotNull Project project, @NotNull Collection<GitRepository> repositories) {
@@ -215,6 +271,16 @@ public class GitRebaseUtils {
return word.endsWith("e") ? word + "d" : word + "ed";
}
@NotNull
public static Collection<GitRepository> getRebasingRepositories(@NotNull Project project) {
return ContainerUtil.filter(GitUtil.getRepositories(project), new Condition<GitRepository>() {
@Override
public boolean value(@NotNull GitRepository repository) {
return repository.getState() == Repository.State.REBASING;
}
});
}
/**
* Short commit info
*/
@@ -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<GitRebaseUtils.CommitInfo> skippedCommits) {
super(Type.SUCCESS, skippedCommits);
mySuccessType = successType;
}
@NotNull
public SuccessType getSuccessType() {
return mySuccessType;
}
@NotNull
static GitSuccessfulRebase parseFromOutput(@NotNull List<String> output, @NotNull Collection<GitRebaseUtils.CommitInfo> 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<String> 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;
}
}
}
@@ -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<GitRepositor
@NotNull private final GitPlatformFacade myPlatformFacade;
@NotNull private final Project myProject;
@Nullable private volatile GitRebaseSpec myOngoingRebaseSpec;
public GitRepositoryManager(@NotNull Project project, @NotNull GitPlatformFacade platformFacade,
@NotNull VcsRepositoryManager vcsRepositoryManager) {
super(vcsRepositoryManager, platformFacade.getVcs(project), GitUtil.DOT_GIT);
@@ -49,4 +53,18 @@ public class GitRepositoryManager extends AbstractRepositoryManager<GitRepositor
public List<GitRepository> 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;
}
}
@@ -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;
}
}
@@ -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<VirtualFile> myStashedRoots;
@@ -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: <br/>
$UNKNOWN_ERROR_TEXT
community: $UNKNOWN_ERROR_TEXT <br/>
You can <a>retry</a> or <a>abort</a> 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() {
@@ -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 <a>resolve</a> the conflicts and <a>continue</a> rebase.<br/>
If you want to start from the beginning, you can <a>abort</a> rebase.
""")
}
protected fun `assert conflict not resolved notification with link to stash`() {
assertWarningNotification("Rebase Suspended",
"""
You have to <a>resolve</a> the conflicts and <a>continue</a> rebase.<br/>
If you want to start from the beginning, you can <a>abort</a> rebase.<br/>
$LOCAL_CHANGES_WARNING
""")
}
protected fun `assert unknown error notification`() {
assertErrorNotification("Rebase Failed",
"""
$UNKNOWN_ERROR_TEXT<br/>
<a>Retry.</a>
""")
}
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<br/>
You can <a>retry</a> or <a>abort</a> rebase.
""")
}
protected fun `assert unknown error notification with link to stash`() {
assertErrorNotification("Rebase Failed",
"""
$UNKNOWN_ERROR_TEXT<br/>
<a>Retry.</a><br/>
$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 <a>retry</a> or <a>abort</a> 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<GitRepository>) :
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<GitRepository>): Collection<GitRepository> {
return repositories.filter { it.isDirty() }
}
protected fun GitRepository.isDirty(): Boolean {
return !gitStatus().isEmpty();
}
}
}
private fun GitRepository.gitStatus() = git(this, "status --porcelain").trim()
@@ -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<GitRepository>): Collection<GitRepository> {
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<GitRepository>): Collection<GitRepository> {
return emptyList()
}
@@ -195,7 +212,7 @@ class GitSingleRepoRebaseTest : GitRebaseBaseTest() {
assertErrorNotification("Rebase Failed",
"""
Rebase failed with error: $UNKNOWN_ERROR_TEXT<br/>
$UNKNOWN_ERROR_TEXT<br/>
<a>Retry.</a><br/>
Note that some local changes were <a>stashed</a> 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 <a>retry</a> or <a>abort</a> 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<GitRepository>): Collection<GitRepository> {
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 <a>resolve</a> the conflicts and <a>continue</a> rebase.<br/>
If you want to start from the beginning, you can <a>abort</a> rebase.
""")
}
private fun `assert conflict not resolved notification with link to stash`() {
assertWarningNotification("Rebase Suspended",
"""
You have to <a>resolve</a> the conflicts and <a>continue</a> rebase.<br/>
If you want to start from the beginning, you can <a>abort</a> rebase.<br/>
$LOCAL_CHANGES_WARNING
""")
}
private fun `assert unknown error notification`() {
assertErrorNotification("Rebase Failed",
"""
Rebase failed with error: $UNKNOWN_ERROR_TEXT<br/>
<a>Retry.</a>
""")
}
private fun `assert unknown error notification with link to abort`() {
assertErrorNotification("Rebase Failed",
"""
Rebase failed with error: $UNKNOWN_ERROR_TEXT<br/>
You can <a>retry</a> or <a>abort</a> rebase.
""")
}
private fun `assert unknown error notification with link to stash`() {
assertErrorNotification("Rebase Failed",
"""
Rebase failed with error: $UNKNOWN_ERROR_TEXT<br/>
<a>Retry.</a><br/>
$LOCAL_CHANGES_WARNING
""")
private fun rebaseOnMaster() {
GitTestingRebaseProcess(myProject, GitRebaseParams("master"), myRepo).rebase()
}
}
@@ -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
}
@@ -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
@@ -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<String>(), 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<String>(), null)
}