diff --git a/plugins/git4idea/src/git4idea/checkin/GitPushActiveBranchesDialog.java b/plugins/git4idea/src/git4idea/checkin/GitPushActiveBranchesDialog.java index 34c0169ad4fb..5230fa4335d5 100644 --- a/plugins/git4idea/src/git4idea/checkin/GitPushActiveBranchesDialog.java +++ b/plugins/git4idea/src/git4idea/checkin/GitPushActiveBranchesDialog.java @@ -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 exceptions = new ArrayList(); List pushExceptions = new ArrayList(); 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 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 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 roots = loadRoots(myProject, new ArrayList(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 rootsToReorder = rebaseInfo.reorderedCommits.keySet(); + saver.saveLocalChanges(rootsToReorder); + + try { + GitRebaser rebaser = new GitRebaser(myProject); + for (Map.Entry> 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.
" + 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.
" + + "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()); } /** diff --git a/plugins/git4idea/src/git4idea/push/GitPusher.java b/plugins/git4idea/src/git4idea/push/GitPusher.java deleted file mode 100644 index f99f2695c36d..000000000000 --- a/plugins/git4idea/src/git4idea/push/GitPusher.java +++ /dev/null @@ -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> myReorderedCommits; - // A set of roots that have non-pushed merges - private Set myRootsWithMerges; - // The registration number for the rebase editor - private Integer myRebaseEditorNo; - private GitRebaseEditorService myRebaseEditorService; - - public GitPusher(final Project project, Map> reorderedCommits, Set rootsWithMerges) { - myProject = project; - myReorderedCommits = reorderedCommits; - myRootsWithMerges = rootsWithMerges; - myRebaseEditorService = GitRebaseEditorService.getInstance(); - } - - protected GitLineHandler makeStartHandler(VirtualFile root) throws VcsException { - List 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 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 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 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 pickLines = new TreeMap(); - 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); - } - } - } -} diff --git a/plugins/git4idea/src/git4idea/rebase/GitRebaseProblemDetector.java b/plugins/git4idea/src/git4idea/rebase/GitRebaseProblemDetector.java index 5867a8b4ad42..c7d13a4d9166 100644 --- a/plugins/git4idea/src/git4idea/rebase/GitRebaseProblemDetector.java +++ b/plugins/git4idea/src/git4idea/rebase/GitRebaseProblemDetector.java @@ -32,7 +32,7 @@ import java.util.concurrent.atomic.AtomicBoolean; *

*/ 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); } } diff --git a/plugins/git4idea/src/git4idea/rebase/GitRebaser.java b/plugins/git4idea/src/git4idea/rebase/GitRebaser.java index 2014895e7f04..68fea2e7ae49 100644 --- a/plugins/git4idea/src/git4idea/rebase/GitRebaser.java +++ b/plugins/git4idea/src/git4idea/rebase/GitRebaser.java @@ -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 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(); } @@ -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 continue rebase.
You also may abort rebase 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 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 olderCommits) throws VcsException { + List allCommits = new ArrayList(); //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 continue rebase.
You also may abort rebase 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 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 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 pickLines = new TreeMap(); + 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); + } + } + } + + } diff --git a/plugins/git4idea/src/git4idea/update/GitChangesSaver.java b/plugins/git4idea/src/git4idea/update/GitChangesSaver.java index 68a1b9a2d6f0..88d3852ef3ad 100644 --- a/plugins/git4idea/src/git4idea/update/GitChangesSaver.java +++ b/plugins/git4idea/src/git4idea/update/GitChangesSaver.java @@ -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. diff --git a/plugins/git4idea/src/git4idea/update/GitRebaseUpdater.java b/plugins/git4idea/src/git4idea/update/GitRebaseUpdater.java index 6a081e47a3d3..e23e0dc5d128 100644 --- a/plugins/git4idea/src/git4idea/update/GitRebaseUpdater.java +++ b/plugins/git4idea/src/git4idea/update/GitRebaseUpdater.java @@ -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 updateResult = new AtomicReference(); + 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 continue rebase.
You also may abort rebase 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 continue rebase.
You also may abort rebase 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); diff --git a/plugins/git4idea/src/git4idea/update/GitShelveChangesSaver.java b/plugins/git4idea/src/git4idea/update/GitShelveChangesSaver.java index e537fb1d0283..e5ce2145bcbe 100644 --- a/plugins/git4idea/src/git4idea/update/GitShelveChangesSaver.java +++ b/plugins/git4idea/src/git4idea/update/GitShelveChangesSaver.java @@ -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"; } diff --git a/plugins/git4idea/src/git4idea/update/GitStashChangesSaver.java b/plugins/git4idea/src/git4idea/update/GitStashChangesSaver.java index aed097589da1..09526764ae3a 100644 --- a/plugins/git4idea/src/git4idea/update/GitStashChangesSaver.java +++ b/plugins/git4idea/src/git4idea/update/GitStashChangesSaver.java @@ -50,7 +50,7 @@ public class GitStashChangesSaver extends GitChangesSaver { private static final Logger LOG = Logger.getInstance(GitStashChangesSaver.class); private final Set myStashedRoots = new HashSet(); // 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"; } diff --git a/plugins/git4idea/src/git4idea/update/GitUpdateProcess.java b/plugins/git4idea/src/git4idea/update/GitUpdateProcess.java index 4441eecf836a..dcb7b597ae95 100644 --- a/plugins/git4idea/src/git4idea/update/GitUpdateProcess.java +++ b/plugins/git4idea/src/git4idea/update/GitUpdateProcess.java @@ -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 myTrackedBranches = new HashMap(); - public GitUpdateProcess(Project project, - ProgressIndicator progressIndicator, - Set roots, UpdatedFiles updatedFiles) { + public GitUpdateProcess(@NotNull Project project, + @NotNull ProgressIndicator progressIndicator, + @NotNull Set 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 rootsToSave = new HashSet(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; diff --git a/plugins/git4idea/tests/git4idea/tests/GitTestRepository.java b/plugins/git4idea/tests/git4idea/tests/GitTestRepository.java index 54e069b312da..1d2b69a0aeea 100644 --- a/plugins/git4idea/tests/git4idea/tests/GitTestRepository.java +++ b/plugins/git4idea/tests/git4idea/tests/GitTestRepository.java @@ -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(); + } + } diff --git a/plugins/git4idea/tests/git4idea/tests/rebase/GitRebaserReorderCommitsTest.java b/plugins/git4idea/tests/git4idea/tests/rebase/GitRebaserReorderCommitsTest.java new file mode 100644 index 000000000000..9f4ffc9820ad --- /dev/null +++ b/plugins/git4idea/tests/git4idea/tests/rebase/GitRebaserReorderCommitsTest.java @@ -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.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"); + } + } + +}