mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
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:
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user