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