git update: adjusted push active branches

Rebase via PushActiveBranches is performed in 2 stages: at first, current branch is rebased to the tracked branch via GitRebaseUpdater; then commits are reordered in a separate rebase. It may change in future to use a single rebase.

GitRebaser is able to reorder commits.
This commit is contained in:
Kirill Likhodedov
2011-03-10 19:09:06 +03:00
parent 9a56f7a3dd
commit 82568c3dd7
11 changed files with 435 additions and 258 deletions
@@ -19,21 +19,27 @@ import com.intellij.notification.Notification;
import com.intellij.notification.NotificationDisplayType;
import com.intellij.notification.NotificationType;
import com.intellij.notification.Notifications;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.EmptyProgressIndicator;
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.project.ex.ProjectManagerEx;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.util.Clock;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.update.UpdatedFiles;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.ui.CheckboxTree;
import com.intellij.ui.CheckedTreeNode;
import com.intellij.ui.ColoredTreeCellRenderer;
import com.intellij.ui.SimpleTextAttributes;
import com.intellij.util.Function;
import com.intellij.util.text.DateFormatUtil;
import com.intellij.util.ui.UIUtil;
import com.intellij.util.ui.tree.TreeUtil;
import git4idea.GitBranch;
@@ -45,8 +51,9 @@ import git4idea.actions.GitShowAllSubmittedFilesAction;
import git4idea.commands.*;
import git4idea.config.GitVcsSettings;
import git4idea.i18n.GitBundle;
import git4idea.rebase.GitRebaser;
import git4idea.ui.GitUIUtil;
import git4idea.update.UpdatePolicyUtils;
import git4idea.update.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -65,6 +72,9 @@ import java.util.*;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import static git4idea.ui.GitUIUtil.notifyError;
import static git4idea.ui.GitUIUtil.notifyImportantError;
/**
* The dialog that allows pushing active branches.
*/
@@ -216,8 +226,8 @@ public class GitPushActiveBranchesDialog extends DialogWrapper {
* will be interrupted.
*/
private void rebaseAndPush() {
final Task.Backgroundable rebaseAndPushTask = new Task.Backgroundable(myProject, GitBundle.getString("push.active.fetching")) {
public void run(@NotNull ProgressIndicator indicator) {
ApplicationManager.getApplication().executeOnPooledThread(new Runnable() {
@Override public void run() {
List<VcsException> exceptions = new ArrayList<VcsException>();
List<VcsException> pushExceptions = new ArrayList<VcsException>();
for (int i = 0; i < 3; i++) {
@@ -261,8 +271,7 @@ public class GitPushActiveBranchesDialog extends DialogWrapper {
}
notifyException("Failed to push", pushExceptions);
}
};
myVcs.runInBackground(rebaseAndPushTask);
});
}
/**
@@ -423,11 +432,88 @@ public class GitPushActiveBranchesDialog extends DialogWrapper {
GitUtil.refreshFiles(myProject, rebaseInfo.roots);
}
private void executeRebase(final List<VcsException> exceptions, final RebaseInfo rebaseInfo) {
// TODO
//GitRebaseUpdater
// process = new GitRebaseUpdater(GitVcs.getInstance(myProject), myProject, exceptions, rebaseInfo.policy, rebaseInfo.reorderedCommits, rebaseInfo.rootsWithMerges);
//process.doUpdate(ProgressManager.getInstance().getProgressIndicator(), rebaseInfo.roots);
private boolean executeRebase(final List<VcsException> exceptions, RebaseInfo rebaseInfo) {
// TODO this is a workaround to attach PushActiveBranched to the new update.
// at first we update via rebase
boolean result = new GitUpdateProcess(myProject, new EmptyProgressIndicator(), rebaseInfo.roots, UpdatedFiles.create()).update(true);
// then we reorder commits
if (result) {
// getting new rebase info because commit hashes changed because of rebase
final List<Root> roots = loadRoots(myProject, new ArrayList<VirtualFile>(rebaseInfo.roots), exceptions, false);
updateTree(roots, rebaseInfo.uncheckedCommits);
rebaseInfo = collectRebaseInfo();
return reorderCommitsIfNeeded(rebaseInfo);
} else {
GitUIUtil.notifyMessage(myProject, "Commits weren't pushed", "Rebase failed.", NotificationType.WARNING, true, null);
return false;
}
}
private boolean reorderCommitsIfNeeded(@NotNull RebaseInfo rebaseInfo) {
if (rebaseInfo.reorderedCommits.isEmpty()) {
return true;
}
ProjectManagerEx projectManager = ProjectManagerEx.getInstanceEx();
ProgressIndicator progressIndicator = ProgressManager.getInstance().getProgressIndicator();
if (progressIndicator == null) {
progressIndicator = new EmptyProgressIndicator();
}
String stashMessage = "Uncommitted changes before rebase operation at " + DateFormatUtil.formatDateTime(Clock.getTime());
GitChangesSaver saver = rebaseInfo.policy == GitVcsSettings.UpdateChangesPolicy.SHELVE ? new GitShelveChangesSaver(myProject, progressIndicator, stashMessage) : new GitStashChangesSaver(myProject, progressIndicator, stashMessage);
projectManager.blockReloadingProjectOnExternalChanges();
try {
final Set<VirtualFile> rootsToReorder = rebaseInfo.reorderedCommits.keySet();
saver.saveLocalChanges(rootsToReorder);
try {
GitRebaser rebaser = new GitRebaser(myProject);
for (Map.Entry<VirtualFile, List<String>> rootToCommits: rebaseInfo.reorderedCommits.entrySet()) {
final VirtualFile root = rootToCommits.getKey();
GitBranch b = GitBranch.current(myProject, root);
if (b == null) {
LOG.info("executeRebase: current branch is null");
continue;
}
GitBranch t = b.tracked(myProject, root);
if (t == null) {
LOG.info("executeRebase: tracked branch is null");
continue;
}
final GitRevisionNumber mergeBase = b.getMergeBase(myProject, root, t);
if (mergeBase == null) {
LOG.info("executeRebase: merge base is null for " + b + " and " + t);
continue;
}
String parentCommit = mergeBase.getRev();
return rebaser.reoderCommitsIfNeeded(root, parentCommit, rootToCommits.getValue());
}
} catch (VcsException e) {
GitUIUtil.notifyMessage(myProject, "Commits weren't pushed", "Failed to reorder commits", NotificationType.WARNING, true,
Collections.singleton(e));
} finally {
try {
saver.restoreLocalChanges();
} catch (VcsException e) {
LOG.info("Couldn't restore local changes after reordering commits", e);
notifyImportantError(myProject, "Couldn't restore local changes after update",
"Restoring changes saved before update failed with an error.<br/>" + e.getLocalizedMessage());
}
}
} catch (VcsException e) {
LOG.info("Couldn't save local changes", e);
notifyError(myProject, "Couldn't save local changes",
"Tried to save uncommitted changes in " + saver.getSaverName() + " before update, but failed with an error.<br/>" +
"Update was cancelled.", true, e);
} finally {
projectManager.unblockReloadingProjectOnExternalChanges();
}
return false;
}
private static class RebaseInfo {
@@ -651,6 +737,7 @@ public class GitPushActiveBranchesDialog extends DialogWrapper {
myPushButton.setEnabled(wasCheckedNode && error == null && !rebaseNeeded);
setErrorText(error);
myRebaseButton.setEnabled(rebaseNeeded && !reorderMerges);
setOKActionEnabled(myPushButton.isEnabled() || myRebaseButton.isEnabled());
}
/**
@@ -1,179 +0,0 @@
/*
* 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.push;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vfs.VirtualFile;
import git4idea.GitBranch;
import git4idea.GitUtil;
import git4idea.commands.GitCommand;
import git4idea.commands.GitHandler;
import git4idea.commands.GitLineHandler;
import git4idea.commands.StringScanner;
import git4idea.rebase.GitInteractiveRebaseEditorHandler;
import git4idea.rebase.GitRebaseEditorService;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
/**
* @author Kirill Likhodedov
*/
public class GitPusher {
private final Project myProject;
// The map from vcs root to list of the commit identifier for reordered commits, if vcs root is not provided, the reordering is not needed.
private Map<VirtualFile, List<String>> myReorderedCommits;
// A set of roots that have non-pushed merges
private Set<VirtualFile> myRootsWithMerges;
// The registration number for the rebase editor
private Integer myRebaseEditorNo;
private GitRebaseEditorService myRebaseEditorService;
public GitPusher(final Project project, Map<VirtualFile, List<String>> reorderedCommits, Set<VirtualFile> rootsWithMerges) {
myProject = project;
myReorderedCommits = reorderedCommits;
myRootsWithMerges = rootsWithMerges;
myRebaseEditorService = GitRebaseEditorService.getInstance();
}
protected GitLineHandler makeStartHandler(VirtualFile root) throws VcsException {
List<String> commits = myReorderedCommits.get(root);
boolean hasMerges = myRootsWithMerges.contains(root);
GitLineHandler h = new GitLineHandler(myProject, root, GitCommand.REBASE);
if (commits != null || hasMerges) {
h.addParameters("-i");
PushRebaseEditor pushRebaseEditor = new PushRebaseEditor(root, commits, hasMerges, h);
myRebaseEditorNo = pushRebaseEditor.getHandlerNo();
myRebaseEditorService.configureHandler(h, myRebaseEditorNo);
if (hasMerges) {
h.addParameters("-p");
}
}
h.addParameters("-m", "-v");
GitBranch currentBranch = GitBranch.current(myProject, root);
assert currentBranch != null;
GitBranch trackedBranch = currentBranch.tracked(myProject, root);
assert trackedBranch != null;
h.addParameters(trackedBranch.getFullName());
return h;
}
protected void cleanupHandler(VirtualFile root, GitLineHandler h) {
if (myRebaseEditorNo != null) {
myRebaseEditorService.unregisterHandler(myRebaseEditorNo);
myRebaseEditorNo = null;
}
}
protected void configureRebaseEditor(VirtualFile root, GitLineHandler h) {
GitInteractiveRebaseEditorHandler editorHandler = new GitInteractiveRebaseEditorHandler(myRebaseEditorService, myProject, root, h);
editorHandler.setRebaseEditorShown();
myRebaseEditorNo = editorHandler.getHandlerNo();
myRebaseEditorService.configureHandler(h, myRebaseEditorNo);
}
//private Collection<VcsException> doRebase(ProgressIndicator progressIndicator,
// VirtualFile root,
// RebaseConflictDetector rebaseConflictDetector,
// final String action) {
// GitLineHandler rh = new GitLineHandler(myProject, root, GitCommand.REBASE);
// // ignore failure for abort
// rh.ignoreErrorCode(1);
// rh.addParameters(action);
// rebaseConflictDetector.reset();
// rh.addLineListener(rebaseConflictDetector);
// if (!"--abort".equals(action)) {
// configureRebaseEditor(root, rh);
// }
// return GitHandlerUtil.doSynchronouslyWithExceptions(rh, progressIndicator, GitHandlerUtil.formatOperationName("Rebasing ", root));
//}
/**
* 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 root the git repository root
* @param commits the reordered commits
* @param hasMerges if true, the vcs root has merges
*/
public PushRebaseEditor(final VirtualFile root, List<String> commits, boolean hasMerges, GitHandler h) {
super(myRebaseEditorService, 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);
}
}
}
}
@@ -32,7 +32,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
* </p>
*/
public class GitRebaseProblemDetector extends GitLineHandlerAdapter {
private final static String REBASE_CONFLICT_INDICATOR = "Merge conflict in";
private final static String[] REBASE_CONFLICT_INDICATORS = {"Merge conflict in", "hint: after resolving the conflicts, mark the corrected paths" };
private static final String REBASE_NO_CHANGE_INDICATOR = "No changes - did you forget to use 'git add'?";
private AtomicBoolean mergeConflict = new AtomicBoolean(false);
@@ -48,9 +48,13 @@ public class GitRebaseProblemDetector extends GitLineHandlerAdapter {
@Override
public void onLineAvailable(String line, Key outputType) {
if (line.contains(REBASE_CONFLICT_INDICATOR)) {
mergeConflict.set(true);
} else if (line.contains(REBASE_NO_CHANGE_INDICATOR)) {
for (String conflictIndicator : REBASE_CONFLICT_INDICATORS) {
if (line.contains(conflictIndicator)) {
mergeConflict.set(true);
return;
}
}
if (line.contains(REBASE_NO_CHANGE_INDICATOR)) {
noChangeError.set(true);
}
}
@@ -15,16 +15,23 @@
*/
package git4idea.rebase;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vcs.AbstractVcsHelper;
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.GitUtil;
import git4idea.GitVcs;
import git4idea.commands.*;
import git4idea.merge.GitMergeConflictResolver;
import git4idea.ui.GitUIUtil;
import org.jetbrains.annotations.NotNull;
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;
@@ -35,13 +42,12 @@ public class GitRebaser {
private final Project myProject;
private GitVcs myVcs;
private final AbstractVcsHelper myVcsHelper;
private List<GitRebaseUtils.CommitInfo> mySkippedCommits;
private static final Logger LOG = Logger.getInstance(GitRebaser.class);
public GitRebaser(Project project) {
myProject = project;
myVcs = GitVcs.getInstance(project);
myVcsHelper = AbstractVcsHelper.getInstance(project);
mySkippedCommits = new ArrayList<GitRebaseUtils.CommitInfo>();
}
@@ -65,38 +71,7 @@ public class GitRebaser {
final GitTask rebaseTask = new GitTask(myProject, rh, "git rebase " + startOperation);
rebaseTask.setExecuteResultInAwt(false);
rebaseTask.setProgressAnalyzer(new GitStandardProgressAnalyzer());
final AtomicBoolean result = 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() {
if (rebaseConflictDetector.isMergeConflict()) {
result.set(new GitMergeConflictResolver(myProject, true, "Merge conflicts detected. Resolve them before continuing rebase.",
"Can't continue rebase", "Then you may <b>continue rebase</b>. <br/> You also may <b>abort rebase</b> to restore the original branch and stop rebasing.") {
@Override protected boolean proceedIfNothingToMerge() {
return continueRebase(root, "--continue");
}
@Override protected boolean proceedAfterAllMerged() {
return continueRebase(root, "--continue");
}
}.mergeFiles(Collections.singleton(root)));
} else if (rebaseConflictDetector.isNoChangeError()) {
mySkippedCommits.add(GitRebaseUtils.getCurrentRebaseCommit(root));
result.set(continueRebase(root, "--skip"));
} else {
result.set(false);
GitUIUtil.notifyImportantError(myProject, "Error rebasing", GitUIUtil.stringifyErrors(rh.errors()));
}
}
});
return result.get();
return executeRebaseTaskInBackground(root, rh, rebaseConflictDetector, rebaseTask);
}
public boolean continueRebase(Collection<VirtualFile> rebasingRoots) {
@@ -120,4 +95,161 @@ public class GitRebaser {
return rebasingRoots;
}
// The registration number for the rebase editor
/**
* 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.setExecuteResultInAwt(false);
return executeRebaseTaskInBackground(root, h, rebaseConflictDetector, rebaseTask);
} finally {
// 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();
}
private boolean handleRebaseFailure(final VirtualFile root, final GitLineHandler h, GitRebaseProblemDetector rebaseConflictDetector) {
if (rebaseConflictDetector.isMergeConflict()) {
return new GitMergeConflictResolver(myProject, true, "Merge conflicts detected. Resolve them before continuing rebase.",
"Can't continue rebase", "Then you may <b>continue rebase</b>. <br/> You also may <b>abort rebase</b> to restore the original branch and stop rebasing.") {
@Override protected boolean proceedIfNothingToMerge() {
return continueRebase(root, "--continue");
}
@Override protected boolean proceedAfterAllMerged() {
return continueRebase(root, "--continue");
}
}.mergeFiles(Collections.singleton(root));
} else if (rebaseConflictDetector.isNoChangeError()) {
mySkippedCommits.add(GitRebaseUtils.getCurrentRebaseCommit(root));
return continueRebase(root, "--skip");
} else {
GitUIUtil.notifyImportantError(myProject, "Error rebasing", GitUIUtil.stringifyErrors(h.errors()));
return false;
}
}
/**
* 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);
}
}
}
}
@@ -158,7 +158,7 @@ public abstract class GitChangesSaver {
/**
* @return name of the save capability provider - stash or shelf.
*/
protected abstract String getSaverName();
public abstract String getSaverName();
/**
* Show the saved local changes in the proper viewer.
@@ -30,6 +30,7 @@ import git4idea.rebase.GitRebaser;
import git4idea.ui.GitUIUtil;
import java.util.Collections;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
/**
@@ -61,6 +62,7 @@ public class GitRebaseUpdater extends GitUpdater {
pullTask.setExecuteResultInAwt(false);
pullTask.setProgressAnalyzer(new GitStandardProgressAnalyzer());
final AtomicReference<GitUpdateResult> updateResult = new AtomicReference<GitUpdateResult>();
final AtomicBoolean failure = new AtomicBoolean();
pullTask.executeInBackground(true, new GitTaskResultHandlerAdapter() {
@Override protected void onSuccess() {
updateResult.set(GitUpdateResult.SUCCESS);
@@ -72,28 +74,35 @@ public class GitRebaseUpdater extends GitUpdater {
}
@Override protected void onFailure() {
if (rebaseConflictDetector.isMergeConflict()) {
final boolean allMerged = new GitMergeConflictResolver(myProject, true, "Merge conflicts detected. Resolve them before continuing rebase.", "Can't continue rebase", "Then you may <b>continue rebase</b>. <br/> You also may <b>abort rebase</b> to restore the original branch and stop rebasing.") {
@Override protected boolean proceedIfNothingToMerge() throws VcsException {
return myRebaser.continueRebase(myRoot);
}
@Override protected boolean proceedAfterAllMerged() throws VcsException {
return myRebaser.continueRebase(myRoot);
}
}.mergeFiles(Collections.singleton(myRoot));
updateResult.set(allMerged ? GitUpdateResult.SUCCESS : GitUpdateResult.INCOMPLETE);
} else {
GitUIUtil.notifyImportantError(myProject, "Error rebasing", GitUIUtil.stringifyErrors(pullHandler.errors()));
updateResult.set(GitUpdateResult.ERROR);
}
failure.set(true);
}
});
if (failure.get()) {
updateResult.set(handleRebaseFailure(rebaseConflictDetector, pullHandler));
}
return updateResult.get();
}
// TODO
private GitUpdateResult handleRebaseFailure(GitRebaseProblemDetector rebaseConflictDetector, GitLineHandler pullHandler) {
if (rebaseConflictDetector.isMergeConflict()) {
final boolean allMerged = new GitMergeConflictResolver(myProject, true, "Merge conflicts detected. Resolve them before continuing rebase.", "Can't continue rebase", "Then you may <b>continue rebase</b>. <br/> You also may <b>abort rebase</b> to restore the original branch and stop rebasing.") {
@Override protected boolean proceedIfNothingToMerge() throws VcsException {
return myRebaser.continueRebase(myRoot);
}
@Override protected boolean proceedAfterAllMerged() throws VcsException {
return myRebaser.continueRebase(myRoot);
}
}.mergeFiles(Collections.singleton(myRoot));
return allMerged ? GitUpdateResult.SUCCESS : GitUpdateResult.INCOMPLETE;
} else {
GitUIUtil.notifyImportantError(myProject, "Error rebasing", GitUIUtil.stringifyErrors(pullHandler.errors()));
return GitUpdateResult.ERROR;
}
}
// TODO
//if (!checkLocallyModified(myRoot)) {
// cancel();
// updateSucceeded.set(false);
@@ -42,7 +42,7 @@ public class GitShelveChangesSaver extends GitChangesSaver {
private final ShelvedChangesViewManager myShelveViewManager;
private ShelvedChangeList myShelvedChangeList;
protected GitShelveChangesSaver(Project project, ProgressIndicator indicator, String stashMessage) {
public GitShelveChangesSaver(Project project, ProgressIndicator indicator, String stashMessage) {
super(project, indicator, stashMessage);
myShelveManager = ShelveChangesManager.getInstance(myProject);
myShelveViewManager = ShelvedChangesViewManager.getInstance(myProject);
@@ -82,7 +82,7 @@ public class GitShelveChangesSaver extends GitChangesSaver {
return myShelvedChangeList != null;
}
@Override protected String getSaverName() {
@Override public String getSaverName() {
return "shelf";
}
@@ -50,7 +50,7 @@ public class GitStashChangesSaver extends GitChangesSaver {
private static final Logger LOG = Logger.getInstance(GitStashChangesSaver.class);
private final Set<VirtualFile> myStashedRoots = new HashSet<VirtualFile>(); // save stashed roots to unstash only them
GitStashChangesSaver(Project project, ProgressIndicator progressIndicator, String stashMessage) {
public GitStashChangesSaver(Project project, ProgressIndicator progressIndicator, String stashMessage) {
super(project, progressIndicator, stashMessage);
}
@@ -75,7 +75,7 @@ public class GitStashChangesSaver extends GitChangesSaver {
return !myStashedRoots.isEmpty();
}
@Override protected String getSaverName() {
@Override public String getSaverName() {
return "stash";
}
@@ -30,6 +30,7 @@ import git4idea.merge.GitMergeConflictResolver;
import git4idea.merge.GitMergeUtil;
import git4idea.merge.GitMerger;
import git4idea.rebase.GitRebaser;
import org.jetbrains.annotations.NotNull;
import java.util.*;
@@ -54,9 +55,9 @@ public class GitUpdateProcess {
private final Map<VirtualFile, GitBranchPair> myTrackedBranches = new HashMap<VirtualFile, GitBranchPair>();
public GitUpdateProcess(Project project,
ProgressIndicator progressIndicator,
Set<VirtualFile> roots, UpdatedFiles updatedFiles) {
public GitUpdateProcess(@NotNull Project project,
@NotNull ProgressIndicator progressIndicator,
@NotNull Set<VirtualFile> roots, @NotNull UpdatedFiles updatedFiles) {
myProject = project;
myRoots = roots;
myUpdatedFiles = updatedFiles;
@@ -72,6 +73,10 @@ public class GitUpdateProcess {
* In case of error shows notification and returns false. If update completes without errors, returns true.
*/
public boolean update() {
return update(false);
}
public boolean update(boolean forceRebase) {
LOG.info("update started");
myProjectManager.blockReloadingProjectOnExternalChanges();
@@ -83,7 +88,8 @@ public class GitUpdateProcess {
// define updaters for each root
Collection<VirtualFile> rootsToSave = new HashSet<VirtualFile>(1);
for (VirtualFile root : myRoots) {
final GitUpdater updater = GitUpdater.getUpdater(myProject, this, root, myProgressIndicator, myUpdatedFiles);
final GitUpdater updater = forceRebase ? GitUpdater.getUpdater(myProject, this, root, myProgressIndicator, myUpdatedFiles) :
new GitRebaseUpdater(myProject, root, this, myProgressIndicator, myUpdatedFiles);
if (updater.isSaveNeeded()) {
rootsToSave.add(root);
}
@@ -96,7 +102,8 @@ public class GitUpdateProcess {
boolean success = true;
for (final VirtualFile root : myRoots) {
try {
final GitUpdater updater = GitUpdater.getUpdater(myProject, this, root, myProgressIndicator, myUpdatedFiles);
final GitUpdater updater = forceRebase ? GitUpdater.getUpdater(myProject, this, root, myProgressIndicator, myUpdatedFiles) :
new GitRebaseUpdater(myProject, root, this, myProgressIndicator, myUpdatedFiles);
GitUpdateResult res = updater.update();
if (res == GitUpdateResult.INCOMPLETE) {
incomplete = true;
@@ -154,11 +154,11 @@ public class GitTestRepository {
}
}
public void commit(@Nullable String commitMessage) throws IOException {
public ProcessOutput commit(@Nullable String commitMessage) throws IOException {
if (commitMessage == null) {
commitMessage = "Sample commit message";
}
execute(true, "commit", "-m", commitMessage);
return execute(true, "commit", "-m", commitMessage);
}
/**
@@ -239,9 +239,9 @@ public class GitTestRepository {
/**
* Calls add() and then commit(). A shorthand for usual test situations when a file is added and then immediately committed.
*/
public void addCommit() throws IOException {
public ProcessOutput addCommit() throws IOException {
add();
commit(null);
return commit(null);
}
public void addCommit(String commitMessage) throws IOException {
@@ -310,4 +310,9 @@ public class GitTestRepository {
public GitTest getTest() {
return myTest;
}
public String lastCommit() throws IOException {
return execute(false, "rev-parse", "HEAD").getStdout().trim();
}
}
@@ -0,0 +1,112 @@
/*
* 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.tests.rebase;
import com.intellij.openapi.vfs.VirtualFile;
import git4idea.rebase.GitRebaser;
import git4idea.tests.GitSingleUserTest;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import static org.testng.Assert.assertEquals;
/**
* NB: we don't test merge commits here, since {@link GitRebaser#reoderCommitsIfNeeded(com.intellij.openapi.vfs.VirtualFile, String, java.util.List)}
* is not suitable for this.
* @author Kirill Likhodedov
*/
public class GitRebaserReorderCommitsTest extends GitSingleUserTest {
private GitRebaser myRebaser;
private VirtualFile myRoot;
private String myFirstCommit;
@BeforeMethod @Override protected void setUp() throws Exception {
super.setUp();
myRebaser = new GitRebaser(myProject);
myRoot = myRepo.getDir();
myFirstCommit = makeCommit();
}
@Test
public void reorderingNothingShouldDoNothing() throws Exception {
myRebaser.reoderCommitsIfNeeded(myRoot, myFirstCommit, Collections.<String>emptyList());
assertCommits(myFirstCommit);
}
@Test
public void reorderingOneShouldDoNothing() throws Exception {
String hash = makeCommit();
myRebaser.reoderCommitsIfNeeded(myRoot, myFirstCommit, Collections.singletonList(hash));
assertCommits(myFirstCommit, hash);
}
@Test
public void reorderingAllShouldDoNothing() throws Exception {
String hash1 = makeCommit();
String hash2 = makeCommit();
myRebaser.reoderCommitsIfNeeded(myRoot, myFirstCommit, Arrays.asList(hash1, hash2));
assertCommits(myFirstCommit, hash1, hash2);
}
@Test
public void reorderingOldestShouldDoNothing() throws Exception {
String[] hashes = makeCommits(3);
myRebaser.reoderCommitsIfNeeded(myRoot, myFirstCommit, Arrays.asList(hashes[0], hashes[1]));
assertCommits(myFirstCommit, hashes[0], hashes[1], hashes[2]);
}
@Test
public void reorderingOneCommit() throws Exception {
String[] hashes = makeCommits(3);
myRebaser.reoderCommitsIfNeeded(myRoot, myFirstCommit, Collections.singletonList(hashes[2]));
assertCommits(myFirstCommit, hashes[2], hashes[0], hashes[1]);
}
@Test
public void reorderingTwoCommits() throws Exception {
String[] hashes = makeCommits(3);
myRebaser.reoderCommitsIfNeeded(myRoot, myFirstCommit, Arrays.asList(hashes[2], hashes[1]));
assertCommits(myFirstCommit, hashes[2], hashes[1], hashes[0]);
}
private String[] makeCommits(int number) throws IOException {
String[] hashes = new String[number];
for (int i = 0; i < hashes.length; i++) {
hashes[i] = makeCommit();
}
return hashes;
}
private String makeCommit() throws IOException {
VirtualFile file = createFileInCommand(Math.random() + ".txt", "initial" + Math.random());
myRepo.addCommit();
return myRepo.lastCommit();
}
private void assertCommits(String... commits) throws IOException {
final String[] hashes = myRepo.execute(false, "rev-list", "--reverse", "HEAD").getStdout().split("\n");
assertEquals(commits.length, hashes.length);
for (int i = 0; i < commits.length; i++) {
assertEquals(hashes[i], commits[i], "Commit #" + i + " doesn't match");
}
}
}