From 509ef6fc50eb971d91b482452b69fd070ea0d01b Mon Sep 17 00:00:00 2001 From: Constantine Plotnikov Date: Mon, 2 Nov 2009 23:28:29 +0300 Subject: [PATCH] git4idea: Added possiblitity to selectively push commits in push active branches dialog --- .../checkin/GitPushActiveBranchesDialog.form | 63 ++- .../checkin/GitPushActiveBranchesDialog.java | 515 +++++++++++++++--- .../checkin/GitPushRebaseProcess.java | 228 ++++++++ .../src/git4idea/i18n/GitBundle.properties | 32 +- .../git4idea/src/git4idea/ui/GitUIUtil.java | 25 + 5 files changed, 767 insertions(+), 96 deletions(-) create mode 100644 plugins/git4idea/src/git4idea/checkin/GitPushRebaseProcess.java diff --git a/plugins/git4idea/src/git4idea/checkin/GitPushActiveBranchesDialog.form b/plugins/git4idea/src/git4idea/checkin/GitPushActiveBranchesDialog.form index 44dc5e59f6ac..7bac34fa9186 100644 --- a/plugins/git4idea/src/git4idea/checkin/GitPushActiveBranchesDialog.form +++ b/plugins/git4idea/src/git4idea/checkin/GitPushActiveBranchesDialog.form @@ -15,11 +15,9 @@ - + - - - + @@ -32,15 +30,58 @@ - + + - + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/git4idea/src/git4idea/checkin/GitPushActiveBranchesDialog.java b/plugins/git4idea/src/git4idea/checkin/GitPushActiveBranchesDialog.java index f8ebb6730eec..20a88fa74379 100644 --- a/plugins/git4idea/src/git4idea/checkin/GitPushActiveBranchesDialog.java +++ b/plugins/git4idea/src/git4idea/checkin/GitPushActiveBranchesDialog.java @@ -21,26 +21,28 @@ import com.intellij.openapi.ui.DialogWrapper; import com.intellij.openapi.ui.Messages; import com.intellij.openapi.vcs.VcsException; 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.ui.tree.TreeUtil; import git4idea.GitBranch; import git4idea.GitRevisionNumber; +import git4idea.GitVcs; import git4idea.actions.GitShowAllSubmittedFilesAction; import git4idea.commands.*; import git4idea.i18n.GitBundle; +import git4idea.ui.GitUIUtil; import javax.swing.*; import javax.swing.event.TreeSelectionEvent; import javax.swing.event.TreeSelectionListener; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeModel; -import javax.swing.tree.TreeNode; import javax.swing.tree.TreePath; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Date; -import java.util.List; +import java.util.*; /** * The dialog that allows pushing active branches. @@ -59,37 +61,47 @@ public class GitPushActiveBranchesDialog extends DialogWrapper { */ private JPanel myPanel; /** - * The commit tree control + * Fetch changes from remote repository */ - private JTree myCommitTree; + private JButton myFetchButton; /** - * The root information structure + * Rebase commits to new roots */ - private List myRoots; + private JButton myRebaseButton; + /** + * If selected, the changes are auto-stashed before rebase + */ + private JCheckBox myAutoStashCheckBox; + /** + * The commit tree (sorted by vcs roots) + */ + private CheckboxTree myCommitTree; + /** + * The root node + */ + private CheckedTreeNode myTreeRoot; + /** + * The context project + */ + private Project myProject; + /** + * The vcs roots for the project + */ + private List myVcsRoots; /** * The constructor * - * @param project the project - * @param roots the loaded roots + * @param project the project + * @param vcsRoots the vcs roots + * @param roots the loaded information about roots */ - private GitPushActiveBranchesDialog(final Project project, List roots) { + private GitPushActiveBranchesDialog(final Project project, List vcsRoots, List roots) { super(project, true); - myRoots = roots; - myCommitTree.setModel(new DefaultTreeModel(createTree())); + myProject = project; + myVcsRoots = vcsRoots; + updateTree(roots, null); TreeUtil.expandAll(myCommitTree); - for (Root r : roots) { - if (r.branch == null) { - setErrorText(GitBundle.getString("push.active.error.no.branch")); - setOKActionEnabled(false); - break; - } - if (r.remoteCommits != 0 && r.commits.size() != 0) { - setErrorText(GitBundle.getString("push.active.error.behind")); - setOKActionEnabled(false); - break; - } - } myCommitTree.getSelectionModel().addTreeSelectionListener(new TreeSelectionListener() { public void valueChanged(TreeSelectionEvent e) { TreePath path = myCommitTree.getSelectionModel().getSelectionPath(); @@ -115,29 +127,267 @@ public class GitPushActiveBranchesDialog extends DialogWrapper { GitShowAllSubmittedFilesAction.showSubmittedFiles(project, c.revision.asString(), c.root.root); } }); + myFetchButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + doFetch(); + } + }); + myRebaseButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + doRebase(); + } + }); setTitle(GitBundle.getString("push.active.title")); setOKButtonText(GitBundle.getString("push.active.button")); init(); } /** - * @return the created tree + * Perform fetch operation */ - private TreeNode createTree() { - DefaultMutableTreeNode treeRoot = new DefaultMutableTreeNode("ROOT", true); - for (Root r : myRoots) { - DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode(r, true); + private void doFetch() { + Map> unchecked = new HashMap>(); + for (int i = 0; i < myTreeRoot.getChildCount(); i++) { + Set uncheckedCommits = new HashSet(); + CheckedTreeNode node = (CheckedTreeNode)myTreeRoot.getChildAt(i); + Root r = (Root)node.getUserObject(); + for (int j = 0; j < node.getChildCount(); j++) { + if (node.getChildAt(j) instanceof CheckedTreeNode) { + CheckedTreeNode commitNode = (CheckedTreeNode)node.getChildAt(j); + if (!commitNode.isChecked()) { + uncheckedCommits.add(((Commit)commitNode.getUserObject()).commitId()); + } + } + } + if (!uncheckedCommits.isEmpty()) { + unchecked.put(r.root, uncheckedCommits); + } + } + refreshTree(true, unchecked); + } + + /** + * The rebase operation is needed if the current branch is behind remote branch or if some commit is not selected. + * + * @return true if rebase is needed for at least one vcs root + */ + private boolean isRebaseNeeded() { + for (int i = 0; i < myTreeRoot.getChildCount(); i++) { + CheckedTreeNode node = (CheckedTreeNode)myTreeRoot.getChildAt(i); + Root r = (Root)node.getUserObject(); + if (r.commits.size() == 0) { + continue; + } + if (r.remoteCommits > 0) { + return true; + } + boolean seenCheckedNode = false; + for (int j = 0; j < node.getChildCount(); j++) { + if (node.getChildAt(j) instanceof CheckedTreeNode) { + CheckedTreeNode commitNode = (CheckedTreeNode)node.getChildAt(j); + if (commitNode.isChecked()) { + seenCheckedNode = true; + } + else { + if (seenCheckedNode) { + return true; + } + } + } + } + } + return false; + } + + /** + * Preform rebase operation + */ + private void doRebase() { + final Set roots = new HashSet(); + final Set rootsWithMerges = new HashSet(); + final Map> reorderedCommits = new HashMap>(); + final Map> uncheckedCommits = new HashMap>(); + for (int i = 0; i < myTreeRoot.getChildCount(); i++) { + CheckedTreeNode node = (CheckedTreeNode)myTreeRoot.getChildAt(i); + Root r = (Root)node.getUserObject(); + Set unchecked = new HashSet(); + uncheckedCommits.put(r.root, unchecked); + if (r.commits.size() == 0) { + continue; + } + boolean seenCheckedNode = false; + boolean reorderNeeded = false; + boolean seenMerges = false; + for (int j = 0; j < node.getChildCount(); j++) { + if (node.getChildAt(j) instanceof CheckedTreeNode) { + CheckedTreeNode commitNode = (CheckedTreeNode)node.getChildAt(j); + Commit commit = (Commit)commitNode.getUserObject(); + seenMerges |= commit.isMerge; + if (commitNode.isChecked()) { + seenCheckedNode = true; + } + else { + unchecked.add(commit.commitId()); + if (seenCheckedNode) { + reorderNeeded = true; + } + } + } + } + if (seenMerges) { + rootsWithMerges.add(r.root); + } + if (r.remoteCommits > 0 && seenCheckedNode || reorderNeeded) { + roots.add(r.root); + } + if (reorderNeeded) { + List reordered = new ArrayList(); + for (int j = 0; j < node.getChildCount(); j++) { + if (node.getChildAt(j) instanceof CheckedTreeNode) { + CheckedTreeNode commitNode = (CheckedTreeNode)node.getChildAt(j); + if (!commitNode.isChecked()) { + Commit commit = (Commit)commitNode.getUserObject(); + reordered.add(commit.revision.asString()); + } + } + } + for (int j = 0; j < node.getChildCount(); j++) { + if (node.getChildAt(j) instanceof CheckedTreeNode) { + CheckedTreeNode commitNode = (CheckedTreeNode)node.getChildAt(j); + if (commitNode.isChecked()) { + Commit commit = (Commit)commitNode.getUserObject(); + reordered.add(commit.revision.asString()); + } + } + } + Collections.reverse(reordered); + reorderedCommits.put(r.root, reordered); + } + } + final List exceptions = new ArrayList(); + final boolean autoStash = myAutoStashCheckBox.isSelected(); + final ProgressManager progressManager = ProgressManager.getInstance(); + final GitVcs vcs = GitVcs.getInstance(myProject); + progressManager.runProcessWithProgressSynchronously(new Runnable() { + public void run() { + GitPushRebaseProcess process = new GitPushRebaseProcess(vcs, myProject, exceptions, autoStash, reorderedCommits, rootsWithMerges); + process.doUpdate(progressManager.getProgressIndicator(), roots); + } + }, GitBundle.getString("push.active.rebasing"), false, myProject); + refreshTree(false, uncheckedCommits); + if (!exceptions.isEmpty()) { + GitUIUtil.showOperationErrors(myProject, exceptions, "git rebase"); + } + } + + /** + * Refresh tree + * + * @param fetchData if true, the current state is fetched from remote + * @param unchecked the map from vcs root to commit identifiers that should be unchecked + */ + private void refreshTree(final boolean fetchData, Map> unchecked) { + ArrayList exceptions = new ArrayList(); + List roots = loadRoots(myProject, myVcsRoots, exceptions, fetchData); + if (!exceptions.isEmpty()) { + //noinspection ThrowableResultOfMethodCallIgnored + GitUIUtil.showOperationErrors(myProject, exceptions, "Refreshing root information"); + return; + } + updateTree(roots, unchecked); + } + + /** + * Update the tree according to the list of loaded roots + * + * @param roots the list of roots to add to the tree + * @param uncheckedCommits the map from vcs root to commit identifiers that should be uncheckedCommits + */ + private void updateTree(List roots, Map> uncheckedCommits) { + myTreeRoot.removeAllChildren(); + for (Root r : roots) { + CheckedTreeNode rootNode = new CheckedTreeNode(r); Status status = new Status(); status.root = r; rootNode.add(new DefaultMutableTreeNode(status, false)); + Set unchecked = + uncheckedCommits != null && uncheckedCommits.containsKey(r.root) ? uncheckedCommits.get(r.root) : Collections.emptySet(); for (Commit c : r.commits) { - rootNode.add(new DefaultMutableTreeNode(c, false)); + CheckedTreeNode child = new CheckedTreeNode(c); + rootNode.add(child); + child.setChecked(r.remote != null && !unchecked.contains(c.commitId())); } - treeRoot.add(rootNode); + myTreeRoot.add(rootNode); } - return treeRoot; + ((DefaultTreeModel)myCommitTree.getModel()).reload(myTreeRoot); + TreeUtil.expandAll(myCommitTree); + updateButtons(); } + /** + * Update buttons on the form + */ + private void updateButtons() { + String error = null; + boolean wasCheckedNode = false; + boolean reorderMerges = false; + for (int i = 0; i < myTreeRoot.getChildCount(); i++) { + CheckedTreeNode node = (CheckedTreeNode)myTreeRoot.getChildAt(i); + boolean seenCheckedNode = false; + boolean reorderNeeded = false; + boolean seenMerges = false; + boolean seenUnchecked = false; + for (int j = 0; j < node.getChildCount(); j++) { + if (node.getChildAt(j) instanceof CheckedTreeNode) { + CheckedTreeNode commitNode = (CheckedTreeNode)node.getChildAt(j); + Commit commit = (Commit)commitNode.getUserObject(); + seenMerges |= commit.isMerge; + if (commitNode.isChecked()) { + seenCheckedNode = true; + } + else { + seenUnchecked = true; + if (seenCheckedNode) { + reorderNeeded = true; + } + } + } + } + if (!seenCheckedNode) { + continue; + } + Root r = (Root)node.getUserObject(); + if( seenMerges && seenUnchecked) { + error = GitBundle.getString("push.active.error.merges.unchecked"); + } + if (seenMerges && reorderNeeded) { + reorderMerges = true; + error = GitBundle.getString("push.active.error.reorder.merges"); + } + if (reorderNeeded) { + if (error == null) { + error = GitBundle.getString("push.active.error.reorder.needed"); + } + } + if (r.branch == null) { + if (error == null) { + error = GitBundle.getString("push.active.error.no.branch"); + } + break; + } + wasCheckedNode |= r.remoteBranch != null; + if (r.remoteCommits != 0 && r.commits.size() != 0) { + if (error == null) { + error = GitBundle.getString("push.active.error.behind"); + } + break; + } + } + boolean rebaseNeeded = isRebaseNeeded(); + setOKActionEnabled(wasCheckedNode && error == null && !rebaseNeeded); + setErrorText(error); + myRebaseButton.setEnabled(rebaseNeeded && !reorderMerges); + } /** * {@inheritDoc} @@ -158,11 +408,16 @@ public class GitPushActiveBranchesDialog extends DialogWrapper { /** * Load VCS roots * - * @param project the project - * @param roots the VCS root list + * @param project the project + * @param roots the VCS root list + * @param exceptions the list of of exceptions to use + * @param fetchData if true, the data for remote is fetched. * @return the loaded information about vcs roots */ - static List loadRoots(final Project project, final List roots, final Collection exceptions) { + static List loadRoots(final Project project, + final List roots, + final Collection exceptions, + final boolean fetchData) { final ProgressManager manager = ProgressManager.getInstance(); final ArrayList rc = new ArrayList(); manager.runProcessWithProgressSynchronously(new Runnable() { @@ -178,7 +433,7 @@ public class GitPushActiveBranchesDialog extends DialogWrapper { r.remote = b.getTrackedRemoteName(project, root); r.remoteBranch = b.getTrackedBranchName(project, root); if (r.remote != null) { - if(!r.remote.equals(".")) { + if (fetchData && !r.remote.equals(".")) { GitLineHandler fetch = new GitLineHandler(project, root, GitHandler.FETCH); fetch.addParameters(r.remote, "-v"); Collection exs = GitHandlerUtil.doSynchronouslyWithExceptions(fetch); @@ -197,7 +452,7 @@ public class GitPushActiveBranchesDialog extends DialogWrapper { } } GitSimpleHandler toPush = new GitSimpleHandler(project, root, GitHandler.LOG); - toPush.addParameters("--pretty=format:%H%x20%ct%x20%s", tracked.getFullName() + ".." + r.branch); + toPush.addParameters("--pretty=format:%H%x20%ct%x20%at%x20%s%n%P", tracked.getFullName() + ".." + r.branch); toPush.setNoSSH(true); toPush.setStdoutSuppressed(true); StringScanner sp = new StringScanner(toPush.run()); @@ -211,7 +466,9 @@ public class GitPushActiveBranchesDialog extends DialogWrapper { String hash = sp.spaceToken(); String time = sp.spaceToken(); c.revision = new GitRevisionNumber(hash, new Date(Long.parseLong(time) * 1000L)); + c.authorTime = sp.spaceToken(); c.message = sp.line(); + c.isMerge = sp.line().indexOf(' ') != -1; r.commits.add(c); } } @@ -234,35 +491,110 @@ public class GitPushActiveBranchesDialog extends DialogWrapper { * @param exceptions the collected exceptions */ public static void showDialog(final Project project, List vcsRoots, final Collection exceptions) { - final List roots = loadRoots(project, vcsRoots, exceptions); + final List roots = loadRoots(project, vcsRoots, exceptions, true); if (!exceptions.isEmpty()) { Messages .showErrorDialog(project, GitBundle.getString("push.active.fetch.failed"), GitBundle.getString("push.active.fetch.failed.title")); return; } - GitPushActiveBranchesDialog d = new GitPushActiveBranchesDialog(project, roots); + GitPushActiveBranchesDialog d = new GitPushActiveBranchesDialog(project, vcsRoots, roots); d.show(); if (d.isOK()) { + final ArrayList rootsToPush = new ArrayList(); + for (int i = 0; i < d.myTreeRoot.getChildCount(); i++) { + CheckedTreeNode node = (CheckedTreeNode)d.myTreeRoot.getChildAt(i); + Root r = (Root)node.getUserObject(); + if (r.remote == null || r.commits.size() == 0) { + continue; + } + boolean topCommit = true; + for (int j = 0; j < node.getChildCount(); j++) { + if (node.getChildAt(j) instanceof CheckedTreeNode) { + CheckedTreeNode commitNode = (CheckedTreeNode)node.getChildAt(j); + if (commitNode.isChecked()) { + Commit commit = (Commit)commitNode.getUserObject(); + if (!topCommit) { + r.commitToPush = commit.revision.asString(); + } + rootsToPush.add(r); + break; + } + topCommit = false; + } + } + } final ProgressManager manager = ProgressManager.getInstance(); manager.runProcessWithProgressSynchronously(new Runnable() { public void run() { - for (Root r : roots) { - if (r.remote != null && r.commits.size() != 0) { - GitLineHandler h = new GitLineHandler(project, r.root, GitHandler.PUSH); - h.addParameters("-v", r.remote, r.branch+":"+r.remoteBranch); - GitHandlerUtil.doSynchronouslyWithExceptions(h); - } + for (Root r : rootsToPush) { + GitLineHandler h = new GitLineHandler(project, r.root, GitHandler.PUSH); + String src = r.commitToPush != null ? r.commitToPush : r.branch; + h.addParameters("-v", r.remote, src + ":" + r.remoteBranch); + GitHandlerUtil.doSynchronouslyWithExceptions(h); } } }, GitBundle.getString("push.active.pushing"), false, project); } } + /** + * Create UI components for the dialog + */ + private void createUIComponents() { + myTreeRoot = new CheckedTreeNode("ROOT"); + myCommitTree = new CheckboxTree(new CheckboxTree.CheckboxTreeCellRenderer() { + @Override + public void customizeRenderer(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) { + ColoredTreeCellRenderer r = getTextRenderer(); + if (!(value instanceof DefaultMutableTreeNode)) { + // unknown node type + renderUnknown(r, value); + return; + } + DefaultMutableTreeNode node = (DefaultMutableTreeNode)value; + if (!(node.getUserObject() instanceof Node)) { + // unknown node type + renderUnknown(r, node.getUserObject()); + return; + } + ((Node)node.getUserObject()).render(r); + } + + /** + * Render unknown node + * + * @param r a renderer to use + * @param value the unknown value + */ + private void renderUnknown(ColoredTreeCellRenderer r, Object value) { + r.append("UNSUPPORTED NODE TYPE: " + (value == null ? "null" : value.getClass().getName()), SimpleTextAttributes.ERROR_ATTRIBUTES); + } + }, myTreeRoot) { + @Override + protected void onNodeStateChanged(CheckedTreeNode node) { + updateButtons(); + super.onNodeStateChanged(node); + } + }; + } + + + /** + * The base class for nodes in the tree + */ + static abstract class Node { + /** + * Render the node text + * + * @param renderer the renderer to use + */ + protected abstract void render(ColoredTreeCellRenderer renderer); + } /** * The commit descriptor */ - static class Status { + static class Status extends Node { /** * The root */ @@ -272,30 +604,34 @@ public class GitPushActiveBranchesDialog extends DialogWrapper { * {@inheritDoc} */ @Override - public String toString() { + protected void render(ColoredTreeCellRenderer renderer) { + renderer.append(GitBundle.getString("push.active.status.status")); if (root.branch == null) { - return GitBundle.message("push.active.status.no.branch"); + renderer.append(GitBundle.message("push.active.status.no.branch"), SimpleTextAttributes.ERROR_ATTRIBUTES); } - if (root.remote == null) { - return GitBundle.message("push.active.status.no.tracked"); + else if (root.remote == null) { + renderer.append(GitBundle.message("push.active.status.no.tracked"), SimpleTextAttributes.GRAYED_BOLD_ATTRIBUTES); } - if (root.remoteCommits != 0 && root.commits.size() == 0) { - return GitBundle.message("push.active.status.no.commits.behind", root.remoteCommits); + else if (root.remoteCommits != 0 && root.commits.size() == 0) { + renderer.append(GitBundle.message("push.active.status.no.commits.behind", root.remoteCommits), + SimpleTextAttributes.GRAYED_BOLD_ATTRIBUTES); } - if (root.commits.size() == 0) { - return GitBundle.message("push.active.status.no.commits"); + else if (root.commits.size() == 0) { + renderer.append(GitBundle.message("push.active.status.no.commits"), SimpleTextAttributes.GRAYED_BOLD_ATTRIBUTES); } - if (root.remoteCommits != 0) { - return GitBundle.message("push.active.status.behind", root.remoteCommits); + else if (root.remoteCommits != 0) { + renderer.append(GitBundle.message("push.active.status.behind", root.remoteCommits), SimpleTextAttributes.ERROR_ATTRIBUTES); + } + else { + renderer.append(GitBundle.message("push.active.status.push", root.commits.size())); } - return GitBundle.message("push.active.status.push", root.commits.size()); } } /** * The commit descriptor */ - static class Commit { + static class Commit extends Node { /** * The root */ @@ -308,20 +644,40 @@ public class GitPushActiveBranchesDialog extends DialogWrapper { * The message */ String message; + /** + * The author time + */ + String authorTime; + /** + * If true, the commit is a merge + */ + boolean isMerge; /** * {@inheritDoc} */ @Override - public String toString() { - return GitBundle.message("push.active.commit.node", revision.asString().substring(0, HASH_PREFIX_SIZE), message); + protected void render(ColoredTreeCellRenderer renderer) { + renderer.append(revision.asString().substring(0, HASH_PREFIX_SIZE), SimpleTextAttributes.GRAYED_ATTRIBUTES); + renderer.append(": "); + renderer.append(message); + if (isMerge) { + renderer.append(GitBundle.getString("push.active.commit.node.merge"), SimpleTextAttributes.GRAYED_ATTRIBUTES); + } + } + + /** + * @return the identifier that is supposed to be stable with respect to rebase + */ + String commitId() { + return authorTime + ":" + message; } } /** * The root node */ - static class Root { + static class Root extends Node { /** * if true, the update is required */ @@ -342,6 +698,10 @@ public class GitPushActiveBranchesDialog extends DialogWrapper { * the remote branch name */ String remoteBranch; + /** + * The commit that will be actually pushed + */ + String commitToPush; /** * the commit */ @@ -351,20 +711,29 @@ public class GitPushActiveBranchesDialog extends DialogWrapper { * {@inheritDoc} */ @Override - public String toString() { - if (branch == null) { - return GitBundle.message("push.active.root.node.no.branch", root.getPresentableUrl()); + protected void render(ColoredTreeCellRenderer renderer) { + SimpleTextAttributes rootAttributes; + SimpleTextAttributes branchAttributes; + if (remote != null && commits.size() != 0 && remoteCommits != 0 || branch == null) { + rootAttributes = SimpleTextAttributes.ERROR_ATTRIBUTES.derive(SimpleTextAttributes.STYLE_BOLD, null, null, null); + branchAttributes = SimpleTextAttributes.ERROR_ATTRIBUTES; } - if (remote == null) { - return GitBundle.message("push.active.root.node.no.tracked", root.getPresentableUrl(), branch); + else if (remote == null || commits.size() == 0) { + rootAttributes = SimpleTextAttributes.GRAYED_BOLD_ATTRIBUTES; + branchAttributes = SimpleTextAttributes.GRAYED_ATTRIBUTES; } - if (commits.size() == 0) { - return GitBundle.message("push.active.root.node.no.commits", root.getPresentableUrl(), branch, remote, remoteBranch); + else { + branchAttributes = SimpleTextAttributes.REGULAR_ATTRIBUTES; + rootAttributes = SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES; } - if (remoteCommits != 0) { - return GitBundle.message("push.active.root.node.behind", root.getPresentableUrl(), branch, remote, remoteBranch); + renderer.append(root.getPresentableUrl(), rootAttributes); + if (branch != null) { + renderer.append(" [" + branch, branchAttributes); + if (remote != null) { + renderer.append(" -> " + remote + "#" + remoteBranch, branchAttributes); + } + renderer.append("]", branchAttributes); } - return GitBundle.message("push.active.root.node.push", root.getPresentableUrl(), branch, remote, remoteBranch); } } } diff --git a/plugins/git4idea/src/git4idea/checkin/GitPushRebaseProcess.java b/plugins/git4idea/src/git4idea/checkin/GitPushRebaseProcess.java new file mode 100644 index 000000000000..08422c0a9273 --- /dev/null +++ b/plugins/git4idea/src/git4idea/checkin/GitPushRebaseProcess.java @@ -0,0 +1,228 @@ +/* + * Copyright 2000-2009 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package git4idea.checkin; + +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.GitVcs; +import git4idea.commands.GitHandler; +import git4idea.commands.GitLineHandler; +import git4idea.commands.StringScanner; +import git4idea.rebase.GitInteractiveRebaseEditorHandler; +import git4idea.rebase.GitRebaseEditorService; +import git4idea.update.GitBaseRebaseProcess; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.text.DateFormat; +import java.util.*; + +/** + * This is subclass of {@link git4idea.update.GitBaseRebaseProcess} that implement rebase operation for {@link GitPushActiveBranchesDialog}. + * This operation reorders commits if needed. + */ +public class GitPushRebaseProcess extends GitBaseRebaseProcess { + /** + * The logger + */ + private static final Logger LOG = Logger.getInstance(GitPushRebaseProcess.class.getName()); + /** + * If true, auto-stash is required before running rebase + */ + private final boolean myAutoStash; + /** + * 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 final Map> myReorderedCommits; + /** + * A set of roots that have non-pushed merges + */ + private Set myRootsWithMerges; + /** + * The registration number for the rebase editor + */ + private Integer myRebaseEditorNo; + /** + * The rebase editor service + */ + private final GitRebaseEditorService myRebaseEditorService; + + /** + * The constructor + * + * @param vcs the vcs instance + * @param project the project instance + * @param exceptions the list of exceptions for the process + * @param autoStash if true, the auto-stash is required + * @param rootsWithMerges a set of roots with merges + */ + public GitPushRebaseProcess(final GitVcs vcs, + final Project project, + List exceptions, + boolean autoStash, + Map> reorderedCommits, + Set rootsWithMerges) { + super(vcs, project, exceptions); + myAutoStash = autoStash; + myReorderedCommits = reorderedCommits; + myRootsWithMerges = rootsWithMerges; + myRebaseEditorService = GitRebaseEditorService.getInstance(); + } + + /** + * {@inheritDoc} + */ + @Override + protected GitLineHandler makeStartHandler(VirtualFile root) throws VcsException { + List commits = myReorderedCommits.get(root); + boolean hasMerges = myRootsWithMerges.contains(root); + GitLineHandler h = new GitLineHandler(myProject, root, GitHandler.REBASE); + if (commits != null || hasMerges) { + h.addParameters("-i"); + PushRebaseEditor pushRebaseEditor = new PushRebaseEditor(root, commits, hasMerges); + 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; + } + + /** + * {@inheritDoc} + */ + @Override + protected void cleanupHandler(VirtualFile root, GitLineHandler h) { + if (myRebaseEditorNo != null) { + myRebaseEditorService.unregisterHandler(myRebaseEditorNo); + } + } + + /** + * {@inheritDoc} + */ + @Override + protected void configureRebaseEditor(VirtualFile root, GitLineHandler h) { + GitInteractiveRebaseEditorHandler editorHandler = new GitInteractiveRebaseEditorHandler(myRebaseEditorService, myProject, root); + editorHandler.setRebaseEditorShown(); + myRebaseEditorNo = editorHandler.getHandlerNo(); + myRebaseEditorService.configureHandler(h, myRebaseEditorNo); + } + + /** + * {@inheritDoc} + */ + @Override + protected String makeStashMessage() { + return "Uncommitted changes before rebase operation in push dialog at " + + DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.US).format(new Date()); + } + + /** + * {@inheritDoc} + */ + @Override + protected boolean isAutoStash() { + return myAutoStash; + } + + /** + * The rebase editor that just overrides the list of commits + */ + class PushRebaseEditor extends GitInteractiveRebaseEditorHandler { + /** + * The reordered commits + */ + private List myCommits; + /** + * The true means that the root has merges + */ + private boolean myHasMerges; + + /** + * 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) { + super(myRebaseEditorService, myProject, root); + myCommits = commits; + myHasMerges = hasMerges; + } + + /** + * {@inheritDoc} + */ + 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); + } + } + } +} \ No newline at end of file diff --git a/plugins/git4idea/src/git4idea/i18n/GitBundle.properties b/plugins/git4idea/src/git4idea/i18n/GitBundle.properties index d1240b8ff96d..1e7d7abdafcf 100644 --- a/plugins/git4idea/src/git4idea/i18n/GitBundle.properties +++ b/plugins/git4idea/src/git4idea/i18n/GitBundle.properties @@ -94,6 +94,8 @@ diff.find.error=Finding revision for diff: {0} error.dialog.title=Error error.list.title={0} Error: error.occurred.during=Error occurred during ''{0}'' +errors.message.item=
  • {0}
  • +errors.message=The git operation ended with multiple errors:
      fetch.action.name=Fetch fetch.button=Fetch fetch.force.references.update.tooltip=Forces update of branch references for which update is not forced in refrence mapping. @@ -188,26 +190,32 @@ pull.url.title=Pull URL pulling.title=Pulling changes from {0} push.action.name=Push push.active.action.name=Push Active Branches +push.active.autostash.tooltip=Auto-stash changes before rebase +push.active.autostash=&Auto-stash push.active.button=Push -push.active.commit.node={0}: {1} +push.active.commit.node.merge= (merge commit) push.active.commits=&Commits: push.active.error.behind=Some local branches are behind remote branches +push.active.error.merges.unchecked=Roots with merge commits cannot be selectively pushed push.active.error.no.branch=Some roots are not on the branch +push.active.error.reorder.merges=It is not possible to reorder commits if merges present +push.active.error.reorder.needed=The nodes should be reordered using rebase operation. push.active.fetch.failed.title=Fetch Failed push.active.fetch.failed=The fetch operation failed for some branches +push.active.fetch.tooltip=Fetch state of tracked branches from remote repository +push.active.fetch=&Fetch push.active.fetching=Fetching changes for active branches push.active.pushing=Pushing branches... -push.active.root.node.behind={0}[{1} -> {2}#{3}] -push.active.root.node.no.branch={0} -push.active.root.node.no.commits={0}[{1} -> {2}#{3}] -push.active.root.node.no.tracked={0}[{1}] -push.active.root.node.push={0}[{1} -> {2}#{3}] -push.active.status.behind=Status: Unable to push. The current branch is behind tracked branch by {0} commit(s). -push.active.status.no.branch=Status: The head is not on the branch. -push.active.status.no.commits.behind=Status: Nothing to push. The current branch is behind tracked branch by {0} commit(s). -push.active.status.no.commits=Status: Nothing to push. -push.active.status.no.tracked=Status: No tracked branch is configured. -push.active.status.push=Status: {0} commit(s) will be pushed. +push.active.rebase.tooltip=Rebase branches in order to make push possible (might reorder commits) +push.active.rebase=&Rebase +push.active.rebasing=Rebasing ... +push.active.status.behind=Unable to push. The current branch is behind tracked branch by {0} commit(s). +push.active.status.no.branch=The head is not on the branch. +push.active.status.no.commits.behind=Nothing to push. The current branch is behind tracked branch by {0} commit(s). +push.active.status.no.commits=Nothing to push. +push.active.status.no.tracked=No tracked branch is configured. +push.active.status.push={0} commit(s) will be pushed. +push.active.status.status=Status: push.active.title=Push Active Branches push.active.view=&View push.branches.tooltip=Select branches to push diff --git a/plugins/git4idea/src/git4idea/ui/GitUIUtil.java b/plugins/git4idea/src/git4idea/ui/GitUIUtil.java index bcd9a9014b3f..11aa2594ad03 100644 --- a/plugins/git4idea/src/git4idea/ui/GitUIUtil.java +++ b/plugins/git4idea/src/git4idea/ui/GitUIUtil.java @@ -32,6 +32,7 @@ import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.util.Collection; import java.util.Collections; import java.util.List; @@ -182,6 +183,30 @@ public class GitUIUtil { showOperationError(project, operation, ex.getMessage()); } + /** + * Show errors associated with the specified operation + * + * @param project the project + * @param exs the exceptions to show + * @param operation the operation name + */ + public static void showOperationErrors(final Project project, + final Collection exs, + @NonNls @NotNull final String operation) { + if (exs.size() == 1) { + //noinspection ThrowableResultOfMethodCallIgnored + showOperationError(project, operation, exs.iterator().next().getMessage()); + } + else if (exs.size() > 1) { + // TODO use dialog in order to show big messages + StringBuilder b = new StringBuilder(); + for (VcsException ex : exs) { + b.append(GitBundle.message("errors.message.item", ex.getMessage())); + } + showOperationError(project, operation, GitBundle.message("errors.message", b.toString())); + } + } + /** * Show error associated with the specified operation *