git: rebase: make rebase dialog reuse the existing common rebase process

This fixes: IDEA-81093, IDEA-55479, IDEA-55672, IDEA-76778, IDEA-55672
This commit is contained in:
Kirill Likhodedov
2016-01-21 18:53:16 +03:00
parent 3ee8e8adb5
commit 905fdebb97
13 changed files with 247 additions and 241 deletions
@@ -444,7 +444,7 @@ public class DvcsUtil {
} }
@Nullable @Nullable
private static VirtualFile guessVcsRoot(@NotNull Project project, @Nullable VirtualFile file) { public static VirtualFile guessVcsRoot(@NotNull Project project, @Nullable VirtualFile file) {
VirtualFile root = null; VirtualFile root = null;
if (file != null) { if (file != null) {
root = ProjectLevelVcsManager.getInstance(project).getVcsRootFor(file); root = ProjectLevelVcsManager.getInstance(project).getVcsRootFor(file);
@@ -44,7 +44,7 @@ abstract class GitAbstractRebaseAction extends DumbAwareAction {
super.update(e); super.update(e);
Project project = e.getProject(); Project project = e.getProject();
if (project == null || !hasGitRepositories(project)) { if (project == null || !hasGitRepositories(project)) {
e.getPresentation().setVisible(false); e.getPresentation().setEnabledAndVisible(false);
} }
else { else {
e.getPresentation().setEnabledAndVisible(hasRebaseInProgress(project)); e.getPresentation().setEnabledAndVisible(hasRebaseInProgress(project));
@@ -15,38 +15,58 @@
*/ */
package git4idea.actions; package git4idea.actions;
import com.intellij.dvcs.DvcsUtil;
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.project.Project;
import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.VirtualFile;
import git4idea.commands.GitLineHandler; import com.intellij.util.containers.ContainerUtil;
import git4idea.i18n.GitBundle;
import git4idea.rebase.GitRebaseDialog; import git4idea.rebase.GitRebaseDialog;
import git4idea.rebase.GitRebaseUtils;
import git4idea.repo.GitRepository;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List; import java.util.List;
/** import static com.intellij.dvcs.DvcsUtil.sortRepositories;
* Git rebase action import static git4idea.GitUtil.*;
*/ import static git4idea.rebase.GitRebaseUtils.getRebasingRepositories;
public class GitRebase extends GitRebaseActionBase { import static java.util.Collections.singletonList;
/** public class GitRebase extends DumbAwareAction {
* {@inheritDoc}
*/ @Override
@NotNull public void update(@NotNull AnActionEvent e) {
protected String getActionName() { super.update(e);
return GitBundle.getString("rebase.action.name"); Project project = e.getProject();
if (project == null || !hasGitRepositories(project)) {
e.getPresentation().setEnabledAndVisible(false);
}
else {
e.getPresentation().setVisible(true);
e.getPresentation().setEnabled(getRebasingRepositories(project).size() < getRepositories(project).size());
}
} }
/** @Override
* {@inheritDoc} public void actionPerformed(@NotNull AnActionEvent e) {
*/ final Project project = e.getRequiredData(CommonDataKeys.PROJECT);
@Nullable ArrayList<GitRepository> repositories = ContainerUtil.newArrayList(getRepositories(project));
protected GitLineHandler createHandler(Project project, List<VirtualFile> gitRoots, VirtualFile defaultRoot) { repositories.removeAll(getRebasingRepositories(project));
GitRebaseDialog dialog = new GitRebaseDialog(project, gitRoots, defaultRoot); List<VirtualFile> roots = ContainerUtil.newArrayList(getRootsFromRepositories(sortRepositories(repositories)));
if (!dialog.showAndGet()) { VirtualFile defaultRoot = DvcsUtil.guessVcsRoot(project, e.getData(CommonDataKeys.VIRTUAL_FILE));
return null; final GitRebaseDialog dialog = new GitRebaseDialog(project, roots, defaultRoot);
if (dialog.showAndGet()) {
ProgressManager.getInstance().run(new Task.Backgroundable(project, "Rebasing...") {
public void run(@NotNull ProgressIndicator indicator) {
GitRebaseUtils.rebase(project, singletonList(dialog.getSelectedRepository()), dialog.getSelectedParams(), indicator);
}
});
} }
return dialog.handler();
} }
} }
@@ -1,154 +0,0 @@
/*
* Copyright 2000-2014 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.dvcs.DvcsUtil;
import com.intellij.openapi.application.AccessToken;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.VcsNotifier;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VirtualFile;
import git4idea.GitUtil;
import git4idea.commands.Git;
import git4idea.commands.GitCommandResult;
import git4idea.commands.GitLineHandler;
import git4idea.i18n.GitBundle;
import git4idea.rebase.GitInteractiveRebaseEditorHandler;
import git4idea.rebase.GitRebaseEditorService;
import git4idea.rebase.GitRebaseLineListener;
import git4idea.repo.GitRepositoryManager;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Set;
/**
* The base class for rebase actions that use editor
*/
public abstract class GitRebaseActionBase extends GitRepositoryAction {
/**
* {@inheritDoc}
*/
protected void perform(@NotNull final Project project,
@NotNull final List<VirtualFile> gitRoots,
@NotNull final VirtualFile defaultRoot,
final Set<VirtualFile> affectedRoots,
final List<VcsException> exceptions) throws VcsException {
final GitLineHandler h = createHandler(project, gitRoots, defaultRoot);
if (h == null) {
return;
}
final VirtualFile root = h.workingDirectoryFile();
GitRebaseEditorService service = GitRebaseEditorService.getInstance();
final GitInteractiveRebaseEditorHandler editor = new GitInteractiveRebaseEditorHandler(service, project, root, h);
final GitRebaseLineListener resultListener = new GitRebaseLineListener();
h.addLineListener(resultListener);
configureEditor(editor);
affectedRoots.add(root);
service.configureHandler(h, editor.getHandlerNo());
new Task.Backgroundable(project, GitBundle.getString("rebasing.title"), true) {
@Override
public void run(@NotNull ProgressIndicator indicator) {
AccessToken token = DvcsUtil.workingTreeChangeStarted(project);
try {
GitCommandResult result = ServiceManager.getService(Git.class).runCommand(h);
editor.close();
GitRepositoryManager manager = GitUtil.getRepositoryManager(project);
manager.updateRepository(root);
VfsUtil.markDirtyAndRefresh(false, true, false, root);
notifyAboutResult(result, resultListener, editor.wasNoopSituationDetected(), exceptions, project);
}
finally {
DvcsUtil.workingTreeChangeFinished(project, token);
}
}
}.queue();
}
private static void notifyAboutResult(@NotNull GitCommandResult commandResult,
@NotNull GitRebaseLineListener resultListener,
boolean noopSituation,
@NotNull List<VcsException> exceptions,
@NotNull Project project) {
final GitRebaseLineListener.Result result = resultListener.getResult();
String messageId;
String message = null;
boolean isError = true;
switch (result.status) {
case CONFLICT:
messageId = "rebase.result.conflict";
break;
case ERROR:
messageId = "rebase.result.error";
message = commandResult.getErrorOutputAsHtmlString();
break;
case CANCELLED:
// we do not need to show a message if editing was cancelled.
exceptions.clear();
return;
case EDIT:
isError = false;
messageId = "rebase.result.amend";
break;
case FINISHED:
isError = false;
messageId = "rebase.result.success";
if (noopSituation) {
message = "Current branch was reset to the base branch";
}
break;
default:
throw new IllegalStateException("Unsupported rebase result: " + result.status);
}
String title = GitBundle.message(messageId + ".title");
if (message == null) {
message = GitBundle.message(messageId, result.current, result.total);
}
if (isError) {
VcsNotifier.getInstance(project).notifyError(title, message);
}
else {
VcsNotifier.getInstance(project).notifySuccess(title, message);
}
}
/**
* This method could be overridden to supply additional information to the editor.
*
* @param editor the editor to configure
*/
protected void configureEditor(GitInteractiveRebaseEditorHandler editor) {
}
/**
* Create line handler that represents a git operation
*
* @param project the context project
* @param gitRoots the git roots
* @param defaultRoot the default root
* @return the line handler or null
*/
@Nullable
protected abstract GitLineHandler createHandler(Project project, List<VirtualFile> gitRoots, VirtualFile defaultRoot);
}
@@ -18,31 +18,71 @@ package git4idea.branch;
import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.containers.ContainerUtil; import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List; import java.util.List;
import static com.intellij.openapi.util.text.StringUtil.nullize;
import static java.util.Arrays.asList;
public class GitRebaseParams { public class GitRebaseParams {
@NotNull private final String myNewBase; @Nullable private final String myBranch;
@Nullable private final String myNewBase;
@NotNull private final String myUpstream;
private final boolean myInteractive;
private final boolean myPreserveMerges;
public GitRebaseParams(@NotNull String newBase) { public GitRebaseParams(@NotNull String upstream) {
myNewBase = newBase; this(null, null, upstream, false, false);
}
public GitRebaseParams(@Nullable String branch,
@Nullable String newBase,
@NotNull String upstream,
boolean interactive,
boolean preserveMerges) {
myBranch = nullize(branch, true);
myNewBase = nullize(newBase, true);
myUpstream = upstream;
myInteractive = interactive;
myPreserveMerges = preserveMerges;
} }
@NotNull @NotNull
public List<String> asCommandLineArguments() { public List<String> asCommandLineArguments() {
List<String> args = ContainerUtil.newArrayList(); List<String> args = ContainerUtil.newArrayList();
args.add(myNewBase); if (myInteractive) {
args.add("--interactive");
}
if (myPreserveMerges) {
args.add("--preserve-merges");
}
if (myNewBase != null) {
args.addAll(asList("--onto", myNewBase));
}
args.add(myUpstream);
if (myBranch != null) {
args.add(myBranch);
}
return args; return args;
} }
@NotNull @Nullable
public String getNewBase() { public String getNewBase() {
return myNewBase; return myNewBase;
} }
@NotNull
public String getUpstream() {
return myUpstream;
}
@Override @Override
public String toString() { public String toString() {
return StringUtil.join(asCommandLineArguments(), " "); return StringUtil.join(asCommandLineArguments(), " ");
} }
public boolean isInteractive() {
return myInteractive;
}
} }
@@ -15,6 +15,7 @@
*/ */
package git4idea.commands; package git4idea.commands;
import com.google.common.annotations.VisibleForTesting;
import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project; import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Computable; import com.intellij.openapi.util.Computable;
@@ -567,10 +568,12 @@ public class GitImpl implements Git {
public GitCommandResult rebase(@NotNull GitRepository repository, public GitCommandResult rebase(@NotNull GitRepository repository,
@NotNull GitRebaseParams parameters, @NotNull GitRebaseParams parameters,
@NotNull GitLineHandlerListener... listeners) { @NotNull GitLineHandlerListener... listeners) {
GitLineHandler handler = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.REBASE); Project project = repository.getProject();
VirtualFile root = repository.getRoot();
GitLineHandler handler = new GitLineHandler(project, root, GitCommand.REBASE);
handler.addParameters(parameters.asCommandLineArguments()); handler.addParameters(parameters.asCommandLineArguments());
addListeners(handler, listeners); addListeners(handler, listeners);
return run(handler); return parameters.isInteractive() ? runWithEditor(project, root, handler, true) : run(handler);
} }
@NotNull @NotNull
@@ -595,9 +598,9 @@ public class GitImpl implements Git {
} }
@NotNull @NotNull
private static GitCommandResult rebaseResume(@NotNull GitRepository repository, private GitCommandResult rebaseResume(@NotNull GitRepository repository,
@NotNull GitRebaseResumeMode rebaseMode, @NotNull GitRebaseResumeMode rebaseMode,
@NotNull GitLineHandlerListener[] listeners) { @NotNull GitLineHandlerListener[] listeners) {
Project project = repository.getProject(); Project project = repository.getProject();
VirtualFile root = repository.getRoot(); VirtualFile root = repository.getRoot();
GitLineHandler handler = new GitLineHandler(project, root, GitCommand.REBASE); GitLineHandler handler = new GitLineHandler(project, root, GitCommand.REBASE);
@@ -607,10 +610,10 @@ public class GitImpl implements Git {
} }
@NotNull @NotNull
private static GitCommandResult runWithEditor(@NotNull Project project, private GitCommandResult runWithEditor(@NotNull Project project,
@NotNull VirtualFile root, @NotNull VirtualFile root,
@NotNull GitLineHandler handler, @NotNull GitLineHandler handler,
boolean commitListAware) { boolean commitListAware) {
GitInteractiveRebaseEditorHandler editor = configureEditor(project, root, handler, commitListAware); GitInteractiveRebaseEditorHandler editor = configureEditor(project, root, handler, commitListAware);
try { try {
return run(handler); return run(handler);
@@ -620,11 +623,12 @@ public class GitImpl implements Git {
} }
} }
@VisibleForTesting
@NotNull @NotNull
private static GitInteractiveRebaseEditorHandler configureEditor(@NotNull Project project, protected GitInteractiveRebaseEditorHandler configureEditor(@NotNull Project project,
@NotNull VirtualFile root, @NotNull VirtualFile root,
@NotNull GitLineHandler handler, @NotNull GitLineHandler handler,
boolean commitListAware) { boolean commitListAware) {
GitRebaseEditorService service = GitRebaseEditorService.getInstance(); GitRebaseEditorService service = GitRebaseEditorService.getInstance();
GitInteractiveRebaseEditorHandler editor = new GitInteractiveRebaseEditorHandler(service, project, root, handler); GitInteractiveRebaseEditorHandler editor = new GitInteractiveRebaseEditorHandler(service, project, root, handler);
if (!commitListAware) { if (!commitListAware) {
@@ -26,14 +26,14 @@ import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.ui.DocumentAdapter; import com.intellij.ui.DocumentAdapter;
import git4idea.*; import git4idea.*;
import git4idea.branch.GitBranchUtil; import git4idea.branch.GitBranchUtil;
import git4idea.commands.GitCommand; import git4idea.branch.GitRebaseParams;
import git4idea.commands.GitLineHandler;
import git4idea.config.GitConfigUtil; import git4idea.config.GitConfigUtil;
import git4idea.config.GitRebaseSettings; import git4idea.config.GitRebaseSettings;
import git4idea.i18n.GitBundle; import git4idea.i18n.GitBundle;
import git4idea.merge.GitMergeUtil; import git4idea.merge.GitMergeUtil;
import git4idea.repo.GitRemote; import git4idea.repo.GitRemote;
import git4idea.repo.GitRepository; import git4idea.repo.GitRepository;
import git4idea.repo.GitRepositoryManager;
import git4idea.ui.GitReferenceValidator; import git4idea.ui.GitReferenceValidator;
import git4idea.util.GitUIUtil; import git4idea.util.GitUIUtil;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@@ -46,6 +46,9 @@ import java.awt.event.ActionListener;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import static com.intellij.openapi.util.text.StringUtil.isEmptyOrSpaces;
import static com.intellij.util.ObjectUtils.assertNotNull;
/** /**
* The dialog that allows initiating git rebase activity * The dialog that allows initiating git rebase activity
*/ */
@@ -53,6 +56,8 @@ public class GitRebaseDialog extends DialogWrapper {
private static final Logger LOG = Logger.getInstance(GitRebaseDialog.class); private static final Logger LOG = Logger.getInstance(GitRebaseDialog.class);
@NotNull private final GitRepositoryManager myRepositoryManager;
/** /**
* Git root selector * Git root selector
*/ */
@@ -151,6 +156,7 @@ public class GitRebaseDialog extends DialogWrapper {
init(); init();
myProject = project; myProject = project;
mySettings = ServiceManager.getService(myProject, GitRebaseSettings.class); mySettings = ServiceManager.getService(myProject, GitRebaseSettings.class);
myRepositoryManager = GitUtil.getRepositoryManager(myProject);
final Runnable validateRunnable = new Runnable() { final Runnable validateRunnable = new Runnable() {
public void run() { public void run() {
validateFields(); validateFields();
@@ -210,39 +216,6 @@ public class GitRebaseDialog extends DialogWrapper {
} }
} }
public GitLineHandler handler() {
GitLineHandler h = new GitLineHandler(myProject, gitRoot(), GitCommand.REBASE);
h.setStdoutSuppressed(false);
if (myInteractiveCheckBox.isSelected() && myInteractiveCheckBox.isEnabled()) {
h.addParameters("-i");
}
h.addParameters("-v");
if (!myDoNotUseMergeCheckBox.isSelected()) {
if (myMergeStrategyComboBox.getSelectedItem().equals(GitMergeUtil.DEFAULT_STRATEGY)) {
h.addParameters("-m");
}
else {
h.addParameters("-s", myMergeStrategyComboBox.getSelectedItem().toString());
}
}
if (myPreserveMergesCheckBox.isSelected()) {
h.addParameters("-p");
}
String from = GitUIUtil.getTextField(myFromComboBox).getText();
String onto = GitUIUtil.getTextField(myOntoComboBox).getText();
if (from.length() == 0) {
h.addParameters(onto);
}
else {
h.addParameters("--onto", onto, from);
}
final String selectedBranch = (String)myBranchComboBox.getSelectedItem();
if (myCurrentBranch != null && !myCurrentBranch.getName().equals(selectedBranch)) {
h.addParameters(selectedBranch);
}
return h;
}
@Override @Override
protected void doOKAction() { protected void doOKAction() {
try { try {
@@ -457,6 +430,31 @@ public class GitRebaseDialog extends DialogWrapper {
return (VirtualFile)myGitRootComboBox.getSelectedItem(); return (VirtualFile)myGitRootComboBox.getSelectedItem();
} }
@NotNull
public GitRepository getSelectedRepository() {
return assertNotNull(myRepositoryManager.getRepositoryForRoot(gitRoot()));
}
@NotNull
public GitRebaseParams getSelectedParams() {
String selectedBranch = (String)myBranchComboBox.getSelectedItem();
String branch = myCurrentBranch != null && !myCurrentBranch.getName().equals(selectedBranch) ? selectedBranch : null;
String from = GitUIUtil.getTextField(myFromComboBox).getText();
String onto = GitUIUtil.getTextField(myOntoComboBox).getText();
String upstream;
String newBase;
if (isEmptyOrSpaces(from)) {
upstream = onto;
newBase = null;
}
else {
upstream = from;
newBase = onto;
}
return new GitRebaseParams(branch, newBase, upstream, myInteractiveCheckBox.isSelected(), myPreserveMergesCheckBox.isSelected());
}
/** /**
* {@inheritDoc} * {@inheritDoc}
@@ -42,10 +42,12 @@ public class GitRebaseProblemDetector extends GitLineHandlerAdapter {
"you have unstaged changes", "you have unstaged changes",
"your index contains uncommitted changes" "your index contains uncommitted changes"
}; };
private static final String STOPPED_FOR_EDITING = "You can amend the commit now";
private volatile boolean myMergeConflict; private volatile boolean myMergeConflict;
private volatile boolean myNoChangeError; private volatile boolean myNoChangeError;
private volatile boolean myDirtyTree; private volatile boolean myDirtyTree;
private volatile boolean myStoppedForEditing;
public boolean isNoChangeError() { public boolean isNoChangeError() {
return myNoChangeError; return myNoChangeError;
@@ -59,6 +61,10 @@ public class GitRebaseProblemDetector extends GitLineHandlerAdapter {
return myDirtyTree; return myDirtyTree;
} }
public boolean hasStoppedForEditing() {
return myStoppedForEditing;
}
@Override @Override
public void onLineAvailable(String line, Key outputType) { public void onLineAvailable(String line, Key outputType) {
for (String conflictIndicator : REBASE_CONFLICT_INDICATORS) { for (String conflictIndicator : REBASE_CONFLICT_INDICATORS) {
@@ -79,5 +85,9 @@ public class GitRebaseProblemDetector extends GitLineHandlerAdapter {
return; return;
} }
} }
if (StringUtil.containsIgnoreCase(line, STOPPED_FOR_EDITING)) {
myStoppedForEditing = true;
}
} }
} }
@@ -200,6 +200,10 @@ public class GitRebaseProcess {
boolean somethingRebased = customMode != null || progressListener.getResult().current > 1; boolean somethingRebased = customMode != null || progressListener.getResult().current > 1;
if (result.success()) { if (result.success()) {
if (rebaseDetector.hasStoppedForEditing()) {
showStoppedForEditingMessage(repository);
return new GitRebaseStatus(GitRebaseStatus.Type.SUSPENDED, skippedCommits);
}
LOG.debug("Successfully rebased " + repoName); LOG.debug("Successfully rebased " + repoName);
return GitSuccessfulRebase.parseFromOutput(result.getOutput(), skippedCommits); return GitSuccessfulRebase.parseFromOutput(result.getOutput(), skippedCommits);
} }
@@ -346,7 +350,7 @@ public class GitRebaseProcess {
}); });
SuccessType commonType = getItemIfAllTheSame(successTypes, SuccessType.REBASED); SuccessType commonType = getItemIfAllTheSame(successTypes, SuccessType.REBASED);
GitRebaseParams params = myRebaseSpec.getParams(); GitRebaseParams params = myRebaseSpec.getParams();
String message = commonType.formatMessage(rebasedBranch, params == null ? null : params.getNewBase()); String message = commonType.formatMessage(rebasedBranch, params == null ? null : notNull(params.getNewBase(), params.getUpstream()));
message += mentionSkippedCommits(skippedCommits); message += mentionSkippedCommits(skippedCommits);
myNotifier.notifyMinorInfo("Rebase Successful", message, new NotificationListener.Adapter() { myNotifier.notifyMinorInfo("Rebase Successful", message, new NotificationListener.Adapter() {
@Override @Override
@@ -391,6 +395,12 @@ public class GitRebaseProcess {
return ResolveConflictResult.UNRESOLVED_REMAIN; return ResolveConflictResult.UNRESOLVED_REMAIN;
} }
private void showStoppedForEditingMessage(@NotNull GitRepository repository) {
String description = "Once you are satisfied with your changes you may <a href='continue'>continue</a>";
myNotifier.notifyImportantInfo("Rebase Stopped for Editing", description,
new RebaseNotificationListener(repository, MultiMap.<GitRepository, GitRebaseUtils.CommitInfo>empty()));
}
private void showFatalError(@NotNull final String error, private void showFatalError(@NotNull final String error,
@NotNull final GitRepository currentRepository, @NotNull final GitRepository currentRepository,
boolean somethingWasRebased, boolean somethingWasRebased,
@@ -507,10 +517,10 @@ public class GitRebaseProcess {
private class RebaseNotificationListener extends NotificationListener.Adapter { private class RebaseNotificationListener extends NotificationListener.Adapter {
@NotNull private final GitRepository myCurrentRepository; @NotNull private final GitRepository myCurrentRepository;
private final MultiMap<GitRepository, GitRebaseUtils.CommitInfo> mySkippedCommits; @NotNull private final MultiMap<GitRepository, GitRebaseUtils.CommitInfo> mySkippedCommits;
RebaseNotificationListener(@NotNull GitRepository currentRepository, RebaseNotificationListener(@NotNull GitRepository currentRepository,
MultiMap<GitRepository, GitRebaseUtils.CommitInfo> skippedCommits) { @NotNull MultiMap<GitRepository, GitRebaseUtils.CommitInfo> skippedCommits) {
myCurrentRepository = currentRepository; myCurrentRepository = currentRepository;
mySkippedCommits = skippedCommits; mySkippedCommits = skippedCommits;
} }
@@ -29,7 +29,7 @@ class GitRebaseStatus {
SUCCESS, SUCCESS,
/** /**
* Rebase started, and some commits were already applied, * Rebase started, and some commits were already applied,
* but then rebase stopped because of conflicts, or because of an error.<br/> * but then rebase stopped because of conflicts, or to edit during interactive rebase, or because of an error.<br/>
* Such rebase can be retried/continued by calling `git rebase --continue/--skip`, or * Such rebase can be retried/continued by calling `git rebase --continue/--skip`, or
* it can be aborted by calling `git rebase --abort`. * it can be aborted by calling `git rebase --abort`.
*/ */
@@ -18,6 +18,7 @@ package git4idea.rebase
import com.intellij.dvcs.DvcsUtil import com.intellij.dvcs.DvcsUtil
import com.intellij.openapi.progress.EmptyProgressIndicator import com.intellij.openapi.progress.EmptyProgressIndicator
import com.intellij.openapi.ui.Messages import com.intellij.openapi.ui.Messages
import com.intellij.util.LineSeparator
import git4idea.branch.GitRebaseParams import git4idea.branch.GitRebaseParams
import git4idea.repo.GitRepository import git4idea.repo.GitRepository
import git4idea.test.GitExecutor.file import git4idea.test.GitExecutor.file
@@ -174,7 +175,7 @@ class GitSingleRepoRebaseTest : GitRebaseBaseTest() {
myRepo.`diverge feature and master`() myRepo.`diverge feature and master`()
val localChange = LocalChange(myRepo, "new.txt").generate() val localChange = LocalChange(myRepo, "new.txt").generate()
object : GitTestingRebaseProcess(myProject, GitRebaseParams("master"), myRepo) { object : GitTestingRebaseProcess(myProject, simpleParams("master"), myRepo) {
override fun getDirtyRoots(repositories: Collection<GitRepository>): Collection<GitRepository> { override fun getDirtyRoots(repositories: Collection<GitRepository>): Collection<GitRepository> {
return listOf(myRepo) return listOf(myRepo)
} }
@@ -190,7 +191,7 @@ class GitSingleRepoRebaseTest : GitRebaseBaseTest() {
myRepo.`diverge feature and master`() myRepo.`diverge feature and master`()
val localChange = LocalChange(myRepo, "new.txt").generate() val localChange = LocalChange(myRepo, "new.txt").generate()
object : GitTestingRebaseProcess(myProject, GitRebaseParams("master"), myRepo) { object : GitTestingRebaseProcess(myProject, simpleParams("master"), myRepo) {
override fun getDirtyRoots(repositories: Collection<GitRepository>): Collection<GitRepository> { override fun getDirtyRoots(repositories: Collection<GitRepository>): Collection<GitRepository> {
return emptyList() return emptyList()
} }
@@ -392,12 +393,48 @@ class GitSingleRepoRebaseTest : GitRebaseBaseTest() {
""") """)
} }
fun `test interactive rebase stopped for editing`() {
build {
master {
0()
1()
}
feature(1) {
2()
3()
}
}
myGit.setInteractiveRebaseEditor {
it.lines().mapIndexed { i, s ->
if (i != 0) s
else s.replace("pick", "edit")
}.joinToString(LineSeparator.getSystemLineSeparator().separatorString)
}
GitTestingRebaseProcess(myProject, GitRebaseParams(null, null, "master", true, false), myRepo).rebase()
assertSuccessfulNotification("Rebase Stopped for Editing", "Once you are satisfied with your changes you may <a href='continue'>continue</a>")
assertEquals("The repository must be in the 'SUSPENDED' state", myRepo, myGitRepositoryManager.ongoingRebaseSpec!!.ongoingRebase)
GitRebaseUtils.continueRebase(myProject)
assertSuccessfulNotification("Rebased feature on master")
myRepo.`assert feature rebased on master`()
assertNoRebaseInProgress(myRepo)
}
private fun build(f: RepoBuilder.() -> Unit) { private fun build(f: RepoBuilder.() -> Unit) {
build(myRepo, f) build(myRepo, f)
} }
private fun rebaseOnMaster() { private fun rebaseOnMaster() {
GitTestingRebaseProcess(myProject, GitRebaseParams("master"), myRepo).rebase() GitTestingRebaseProcess(myProject, simpleParams("master"), myRepo).rebase()
}
private fun simpleParams(newBase: String): GitRebaseParams {
return GitRebaseParams(newBase)
} }
} }
@@ -216,8 +216,12 @@ abstract class GitPlatformTest : PlatformTestCase() {
hookFile.setExecutable(true, false) hookFile.setExecutable(true, false)
} }
protected fun assertSuccessfulNotification(title: String, message: String) {
GitTestUtil.assertNotification(NotificationType.INFORMATION, title, message, myVcsNotifier.lastNotification)
}
protected fun assertSuccessfulNotification(message: String) { protected fun assertSuccessfulNotification(message: String) {
GitTestUtil.assertNotification(NotificationType.INFORMATION, "Rebase Successful", message, myVcsNotifier.lastNotification) assertSuccessfulNotification("Rebase Successful", message)
} }
protected fun assertWarningNotification(title: String, message: String) { protected fun assertWarningNotification(title: String, message: String) {
@@ -15,12 +15,20 @@
*/ */
package git4idea.test package git4idea.test
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.vfs.VirtualFile
import git4idea.branch.GitRebaseParams import git4idea.branch.GitRebaseParams
import git4idea.commands.GitCommandResult import git4idea.commands.GitCommandResult
import git4idea.commands.GitImpl import git4idea.commands.GitImpl
import git4idea.commands.GitLineHandler
import git4idea.commands.GitLineHandlerListener import git4idea.commands.GitLineHandlerListener
import git4idea.rebase.GitInteractiveRebaseEditorHandler
import git4idea.rebase.GitRebaseEditorService
import git4idea.repo.GitRemote import git4idea.repo.GitRemote
import git4idea.repo.GitRepository import git4idea.repo.GitRepository
import java.io.File
/** /**
* Any unknown error that could be returned by Git. * Any unknown error that could be returned by Git.
@@ -28,9 +36,11 @@ import git4idea.repo.GitRepository
val UNKNOWN_ERROR_TEXT: String = "unknown error" val UNKNOWN_ERROR_TEXT: String = "unknown error"
class TestGitImpl : GitImpl() { class TestGitImpl : GitImpl() {
private val LOG = Logger.getInstance(TestGitImpl::class.java)
private var myRebaseShouldFail: (GitRepository) -> Boolean = { false } @Volatile private var myRebaseShouldFail: (GitRepository) -> Boolean = { false }
private var myPushHandler: (GitRepository) -> GitCommandResult? = { null } @Volatile private var myPushHandler: (GitRepository) -> GitCommandResult? = { null }
@Volatile private var myInteractiveRebaseEditor: ((String) -> String)? = null
override fun push(repository: GitRepository, override fun push(repository: GitRepository,
remote: GitRemote, remote: GitRemote,
@@ -67,6 +77,28 @@ class TestGitImpl : GitImpl() {
} }
} }
override fun configureEditor(project: Project, root: VirtualFile, handler: GitLineHandler,
commitListAware: Boolean): GitInteractiveRebaseEditorHandler {
if (myInteractiveRebaseEditor == null) return super.configureEditor(project, root, handler, commitListAware)
val service = GitRebaseEditorService.getInstance()
val editor = object: GitInteractiveRebaseEditorHandler(service, project, root, handler) {
override fun editCommits(path: String?): Int {
try {
val file = File(path)
FileUtil.writeToFile(file, myInteractiveRebaseEditor!!(FileUtil.loadFile(file)))
}
catch(e: Exception) {
LOG.error(e)
return 1
}
return 0
}
}
service.configureHandler(handler, editor.handlerNo)
return editor
}
fun setShouldRebaseFail(shouldFail: (GitRepository) -> Boolean) { fun setShouldRebaseFail(shouldFail: (GitRepository) -> Boolean) {
myRebaseShouldFail = shouldFail myRebaseShouldFail = shouldFail
} }
@@ -75,9 +107,14 @@ class TestGitImpl : GitImpl() {
myPushHandler = pushHandler; myPushHandler = pushHandler;
} }
fun setInteractiveRebaseEditor(editor: (String) -> String) {
myInteractiveRebaseEditor = editor
}
fun reset() { fun reset() {
myRebaseShouldFail = { false } myRebaseShouldFail = { false }
myPushHandler = { null } myPushHandler = { null }
myInteractiveRebaseEditor = null
} }
private fun failOrCall(repository: GitRepository, delegate: () -> GitCommandResult): GitCommandResult { private fun failOrCall(repository: GitRepository, delegate: () -> GitCommandResult): GitCommandResult {