blob: 65883e848bd0f9d432635a5ea329cafed8a7d34f [file] [log] [blame]
/*
* Copyright 2000-2011 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.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.io.FileUtil;
import com.intellij.openapi.vcs.ProjectLevelVcsManager;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vfs.VirtualFile;
import git4idea.GitPlatformFacade;
import git4idea.GitUtil;
import git4idea.GitVcs;
import git4idea.commands.*;
import git4idea.merge.GitConflictResolver;
import git4idea.update.GitUpdateResult;
import git4idea.util.GitUIUtil;
import git4idea.util.StringScanner;
import git4idea.util.UntrackedFilesNotifier;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
/**
* @author Kirill Likhodedov
*/
public class GitRebaser {
private final Project myProject;
private GitVcs myVcs;
private List<GitRebaseUtils.CommitInfo> mySkippedCommits;
private static final Logger LOG = Logger.getInstance(GitRebaser.class);
@NotNull private final Git myGit;
private @Nullable ProgressIndicator myProgressIndicator;
public GitRebaser(Project project, @NotNull Git git, @Nullable ProgressIndicator progressIndicator) {
myProject = project;
myGit = git;
myProgressIndicator = progressIndicator;
myVcs = GitVcs.getInstance(project);
mySkippedCommits = new ArrayList<GitRebaseUtils.CommitInfo>();
}
public void setProgressIndicator(@Nullable ProgressIndicator progressIndicator) {
myProgressIndicator = progressIndicator;
}
public GitUpdateResult rebase(@NotNull VirtualFile root,
@NotNull List<String> parameters,
@Nullable final Runnable onCancel,
@Nullable GitLineHandlerListener lineListener) {
final GitLineHandler rebaseHandler = createHandler(root);
rebaseHandler.addParameters(parameters);
if (lineListener != null) {
rebaseHandler.addLineListener(lineListener);
}
final GitRebaseProblemDetector rebaseConflictDetector = new GitRebaseProblemDetector();
rebaseHandler.addLineListener(rebaseConflictDetector);
GitUntrackedFilesOverwrittenByOperationDetector untrackedFilesDetector = new GitUntrackedFilesOverwrittenByOperationDetector(root);
rebaseHandler.addLineListener(untrackedFilesDetector);
String progressTitle = "Rebasing";
GitTask rebaseTask = new GitTask(myProject, rebaseHandler, progressTitle);
rebaseTask.setProgressIndicator(myProgressIndicator);
rebaseTask.setProgressAnalyzer(new GitStandardProgressAnalyzer());
final AtomicReference<GitUpdateResult> updateResult = new AtomicReference<GitUpdateResult>();
final AtomicBoolean failure = new AtomicBoolean();
rebaseTask.executeInBackground(true, new GitTaskResultHandlerAdapter() {
@Override
protected void onSuccess() {
updateResult.set(GitUpdateResult.SUCCESS);
}
@Override
protected void onCancel() {
if (onCancel != null) {
onCancel.run();
}
updateResult.set(GitUpdateResult.CANCEL);
}
@Override
protected void onFailure() {
failure.set(true);
}
});
if (failure.get()) {
updateResult.set(handleRebaseFailure(root, rebaseHandler, rebaseConflictDetector, untrackedFilesDetector));
}
return updateResult.get();
}
protected GitLineHandler createHandler(VirtualFile root) {
return new GitLineHandler(myProject, root, GitCommand.REBASE);
}
public GitUpdateResult handleRebaseFailure(VirtualFile root, GitLineHandler pullHandler,
GitRebaseProblemDetector rebaseConflictDetector,
GitMessageWithFilesDetector untrackedWouldBeOverwrittenDetector) {
if (rebaseConflictDetector.isMergeConflict()) {
LOG.info("handleRebaseFailure merge conflict");
final boolean allMerged = new MyConflictResolver(myProject, myGit, root, this).merge();
return allMerged ? GitUpdateResult.SUCCESS_WITH_RESOLVED_CONFLICTS : GitUpdateResult.INCOMPLETE;
} else if (untrackedWouldBeOverwrittenDetector.wasMessageDetected()) {
LOG.info("handleRebaseFailure: untracked files would be overwritten by checkout");
UntrackedFilesNotifier.notifyUntrackedFilesOverwrittenBy(myProject,
untrackedWouldBeOverwrittenDetector.getFiles(), "rebase", null);
return GitUpdateResult.ERROR;
} else {
LOG.info("handleRebaseFailure error " + pullHandler.errors());
GitUIUtil.notifyImportantError(myProject, "Rebase error", GitUIUtil.stringifyErrors(pullHandler.errors()));
return GitUpdateResult.ERROR;
}
}
public void abortRebase(@NotNull VirtualFile root) {
LOG.info("abortRebase " + root);
final GitLineHandler rh = new GitLineHandler(myProject, root, GitCommand.REBASE);
rh.addParameters("--abort");
GitTask task = new GitTask(myProject, rh, "Aborting rebase");
task.setProgressIndicator(myProgressIndicator);
task.executeAsync(new GitTaskResultNotificationHandler(myProject, "Rebase aborted", "Abort rebase cancelled", "Error aborting rebase"));
}
public boolean continueRebase(@NotNull VirtualFile root) {
return continueRebase(root, "--continue");
}
/**
* Runs 'git rebase --continue' on several roots consequently.
* @return true if rebase successfully finished.
*/
public boolean continueRebase(@NotNull Collection<VirtualFile> rebasingRoots) {
boolean success = true;
for (VirtualFile root : rebasingRoots) {
success &= continueRebase(root);
}
return success;
}
// start operation may be "--continue" or "--skip" depending on the situation.
private boolean continueRebase(final @NotNull VirtualFile root, @NotNull String startOperation) {
LOG.info("continueRebase " + root + " " + startOperation);
final GitLineHandler rh = new GitLineHandler(myProject, root, GitCommand.REBASE);
rh.addParameters(startOperation);
final GitRebaseProblemDetector rebaseConflictDetector = new GitRebaseProblemDetector();
rh.addLineListener(rebaseConflictDetector);
makeContinueRebaseInteractiveEditor(root, rh);
final GitTask rebaseTask = new GitTask(myProject, rh, "git rebase " + startOperation);
rebaseTask.setProgressAnalyzer(new GitStandardProgressAnalyzer());
rebaseTask.setProgressIndicator(myProgressIndicator);
return executeRebaseTaskInBackground(root, rh, rebaseConflictDetector, rebaseTask);
}
protected void makeContinueRebaseInteractiveEditor(VirtualFile root, GitLineHandler rh) {
GitRebaseEditorService rebaseEditorService = GitRebaseEditorService.getInstance();
// TODO If interactive rebase with commit rewording was invoked, this should take the reworded message
GitRebaser.TrivialEditor editor = new GitRebaser.TrivialEditor(rebaseEditorService, myProject, root, rh);
Integer rebaseEditorNo = editor.getHandlerNo();
rebaseEditorService.configureHandler(rh, rebaseEditorNo);
}
/**
* @return Roots which have unfinished rebase process. May be empty.
*/
public @NotNull Collection<VirtualFile> getRebasingRoots() {
final Collection<VirtualFile> rebasingRoots = new HashSet<VirtualFile>();
for (VirtualFile root : ProjectLevelVcsManager.getInstance(myProject).getRootsUnderVcs(myVcs)) {
if (GitRebaseUtils.isRebaseInTheProgress(root)) {
rebasingRoots.add(root);
}
}
return rebasingRoots;
}
/**
* Reorders commits so that the given commits go before others, just after the given parentCommit.
* For example, if A->B->C->D are unpushed commits and B and D are supplied to this method, then after rebase the commits will
* look like that: B->D->A->C.
* NB: If there are merges in the unpushed commits being reordered, a conflict would happen. The calling code should probably
* prohibit reordering merge commits.
*/
public boolean reoderCommitsIfNeeded(@NotNull final VirtualFile root, @NotNull String parentCommit, @NotNull List<String> olderCommits) throws VcsException {
List<String> allCommits = new ArrayList<String>(); //TODO
if (olderCommits.isEmpty() || olderCommits.size() == allCommits.size()) {
LOG.info("Nothing to reorder. olderCommits: " + olderCommits + " allCommits: " + allCommits);
return true;
}
final GitLineHandler h = new GitLineHandler(myProject, root, GitCommand.REBASE);
Integer rebaseEditorNo = null;
GitRebaseEditorService rebaseEditorService = GitRebaseEditorService.getInstance();
try {
h.addParameters("-i", "-m", "-v");
h.addParameters(parentCommit);
final GitRebaseProblemDetector rebaseConflictDetector = new GitRebaseProblemDetector();
h.addLineListener(rebaseConflictDetector);
final PushRebaseEditor pushRebaseEditor = new PushRebaseEditor(rebaseEditorService, root, olderCommits, false, h);
rebaseEditorNo = pushRebaseEditor.getHandlerNo();
rebaseEditorService.configureHandler(h, rebaseEditorNo);
final GitTask rebaseTask = new GitTask(myProject, h, "Reordering commits");
rebaseTask.setProgressIndicator(myProgressIndicator);
return executeRebaseTaskInBackground(root, h, rebaseConflictDetector, rebaseTask);
} finally { // TODO should be unregistered in the task.success
// unregistering rebase service
if (rebaseEditorNo != null) {
rebaseEditorService.unregisterHandler(rebaseEditorNo);
}
}
}
private boolean executeRebaseTaskInBackground(VirtualFile root, GitLineHandler h, GitRebaseProblemDetector rebaseConflictDetector, GitTask rebaseTask) {
final AtomicBoolean result = new AtomicBoolean();
final AtomicBoolean failure = new AtomicBoolean();
rebaseTask.executeInBackground(true, new GitTaskResultHandlerAdapter() {
@Override protected void onSuccess() {
result.set(true);
}
@Override protected void onCancel() {
result.set(false);
}
@Override protected void onFailure() {
failure.set(true);
}
});
if (failure.get()) {
result.set(handleRebaseFailure(root, h, rebaseConflictDetector));
}
return result.get();
}
/**
* @return true if the failure situation was resolved successfully, false if we failed to resolve the problem.
*/
private boolean handleRebaseFailure(final VirtualFile root, final GitLineHandler h, GitRebaseProblemDetector rebaseConflictDetector) {
if (rebaseConflictDetector.isMergeConflict()) {
LOG.info("handleRebaseFailure merge conflict");
return new GitConflictResolver(myProject, myGit, ServiceManager.getService(GitPlatformFacade.class), Collections.singleton(root), makeParamsForRebaseConflict()) {
@Override protected boolean proceedIfNothingToMerge() {
return continueRebase(root, "--continue");
}
@Override protected boolean proceedAfterAllMerged() {
return continueRebase(root, "--continue");
}
}.merge();
}
else if (rebaseConflictDetector.isNoChangeError()) {
LOG.info("handleRebaseFailure no changes error detected");
try {
if (GitUtil.hasLocalChanges(true, myProject, root)) {
LOG.error("The rebase detector incorrectly detected 'no changes' situation. Attempting to continue rebase.");
return continueRebase(root);
}
else if (GitUtil.hasLocalChanges(false, myProject, root)) {
LOG.warn("No changes from patch were not added to the index. Adding all changes from tracked files.");
stageEverything(root);
return continueRebase(root);
}
else {
GitRebaseUtils.CommitInfo commit = GitRebaseUtils.getCurrentRebaseCommit(root);
LOG.info("no changes confirmed. Skipping commit " + commit);
mySkippedCommits.add(commit);
return continueRebase(root, "--skip");
}
}
catch (VcsException e) {
LOG.info("Failed to work around 'no changes' error.", e);
String message = "Couldn't proceed with rebase. " + e.getMessage();
GitUIUtil.notifyImportantError(myProject, "Error rebasing", message);
return false;
}
}
else {
LOG.info("handleRebaseFailure error " + h.errors());
GitUIUtil.notifyImportantError(myProject, "Error rebasing", GitUIUtil.stringifyErrors(h.errors()));
return false;
}
}
private void stageEverything(@NotNull VirtualFile root) throws VcsException {
GitSimpleHandler handler = new GitSimpleHandler(myProject, root, GitCommand.ADD);
handler.setSilent(false);
handler.addParameters("--update");
handler.run();
}
private static GitConflictResolver.Params makeParamsForRebaseConflict() {
return new GitConflictResolver.Params().
setReverse(true).
setErrorNotificationTitle("Can't continue rebase").
setMergeDescription("Merge conflicts detected. Resolve them before continuing rebase.").
setErrorNotificationAdditionalDescription("Then you may <b>continue rebase</b>. <br/> " +
"You also may <b>abort rebase</b> to restore the original branch and stop rebasing.");
}
private static class MyConflictResolver extends GitConflictResolver {
private final GitRebaser myRebaser;
private final VirtualFile myRoot;
public MyConflictResolver(Project project, @NotNull Git git, VirtualFile root, GitRebaser rebaser) {
super(project, git, ServiceManager.getService(GitPlatformFacade.class), Collections.singleton(root), makeParams());
myRebaser = rebaser;
myRoot = root;
}
private static Params makeParams() {
Params params = new Params();
params.setReverse(true);
params.setMergeDescription("Merge conflicts detected. Resolve them before continuing rebase.");
params.setErrorNotificationTitle("Can't continue rebase");
params.setErrorNotificationAdditionalDescription("Then you may <b>continue rebase</b>. <br/> You also may <b>abort rebase</b> to restore the original branch and stop rebasing.");
return params;
}
@Override protected boolean proceedIfNothingToMerge() throws VcsException {
return myRebaser.continueRebase(myRoot);
}
@Override protected boolean proceedAfterAllMerged() throws VcsException {
return myRebaser.continueRebase(myRoot);
}
}
public static class TrivialEditor extends GitInteractiveRebaseEditorHandler{
public TrivialEditor(@NotNull GitRebaseEditorService service,
@NotNull Project project,
@NotNull VirtualFile root,
@NotNull GitHandler handler) {
super(service, project, root, handler);
}
@Override
public int editCommits(String path) {
return 0;
}
}
@NotNull
public GitUpdateResult handleRebaseFailure(@NotNull GitLineHandler handler, @NotNull VirtualFile root,
@NotNull GitRebaseProblemDetector rebaseConflictDetector,
@NotNull GitMessageWithFilesDetector untrackedWouldBeOverwrittenDetector) {
if (rebaseConflictDetector.isMergeConflict()) {
LOG.info("handleRebaseFailure merge conflict");
final boolean allMerged = new GitRebaser.ConflictResolver(myProject, myGit, root, this).merge();
return allMerged ? GitUpdateResult.SUCCESS_WITH_RESOLVED_CONFLICTS : GitUpdateResult.INCOMPLETE;
} else if (untrackedWouldBeOverwrittenDetector.wasMessageDetected()) {
LOG.info("handleRebaseFailure: untracked files would be overwritten by checkout");
UntrackedFilesNotifier.notifyUntrackedFilesOverwrittenBy(myProject,
untrackedWouldBeOverwrittenDetector.getFiles(), "rebase", null);
return GitUpdateResult.ERROR;
} else {
LOG.info("handleRebaseFailure error " + handler.errors());
GitUIUtil.notifyImportantError(myProject, "Rebase error", GitUIUtil.stringifyErrors(handler.errors()));
return GitUpdateResult.ERROR;
}
}
public static class ConflictResolver extends GitConflictResolver {
@NotNull private final GitRebaser myRebaser;
@NotNull private final VirtualFile myRoot;
public ConflictResolver(@NotNull Project project, @NotNull Git git, @NotNull VirtualFile root, @NotNull GitRebaser rebaser) {
super(project, git, ServiceManager.getService(GitPlatformFacade.class), Collections.singleton(root), makeParams());
myRebaser = rebaser;
myRoot = root;
}
private static Params makeParams() {
Params params = new Params();
params.setReverse(true);
params.setMergeDescription("Merge conflicts detected. Resolve them before continuing rebase.");
params.setErrorNotificationTitle("Can't continue rebase");
params.setErrorNotificationAdditionalDescription("Then you may <b>continue rebase</b>. <br/> You also may <b>abort rebase</b> to restore the original branch and stop rebasing.");
return params;
}
@Override protected boolean proceedIfNothingToMerge() throws VcsException {
return myRebaser.continueRebase(myRoot);
}
@Override protected boolean proceedAfterAllMerged() throws VcsException {
return myRebaser.continueRebase(myRoot);
}
}
/**
* The rebase editor that just overrides the list of commits
*/
class PushRebaseEditor extends GitInteractiveRebaseEditorHandler {
private final Logger LOG = Logger.getInstance(PushRebaseEditor.class);
private final List<String> myCommits; // The reordered commits
private final boolean myHasMerges; // true means that the root has merges
/**
* The constructor from fields that is expected to be
* accessed only from {@link git4idea.rebase.GitRebaseEditorService}.
*
* @param rebaseEditorService
* @param root the git repository root
* @param commits the reordered commits
* @param hasMerges if true, the vcs root has merges
*/
public PushRebaseEditor(GitRebaseEditorService rebaseEditorService,
final VirtualFile root,
List<String> commits,
boolean hasMerges,
GitHandler h) {
super(rebaseEditorService, myProject, root, h);
myCommits = commits;
myHasMerges = hasMerges;
}
public int editCommits(String path) {
if (!myRebaseEditorShown) {
myRebaseEditorShown = true;
if (myHasMerges) {
return 0;
}
try {
TreeMap<String, String> pickLines = new TreeMap<String, String>();
StringScanner s = new StringScanner(new String(FileUtil.loadFileText(new File(path), GitUtil.UTF8_ENCODING)));
while (s.hasMoreData()) {
if (!s.tryConsume("pick ")) {
s.line();
continue;
}
String commit = s.spaceToken();
pickLines.put(commit, "pick " + commit + " " + s.line());
}
PrintWriter w = new PrintWriter(new OutputStreamWriter(new FileOutputStream(path), GitUtil.UTF8_ENCODING));
try {
for (String commit : myCommits) {
String key = pickLines.headMap(commit + "\u0000").lastKey();
if (key == null || !commit.startsWith(key)) {
continue; // commit from merged branch
}
w.print(pickLines.get(key) + "\n");
}
}
finally {
w.close();
}
return 0;
}
catch (Exception ex) {
LOG.error("Editor failed: ", ex);
return 1;
}
}
else {
return super.editCommits(path);
}
}
}
}