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.
*/
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<Root> 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<VirtualFile> 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<Root> roots) {
+ private GitPushActiveBranchesDialog(final Project project, List<VirtualFile> vcsRoots, List<Root> 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();
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 void doFetch() {
+ Map<VirtualFile, Set<String>> unchecked = new HashMap<VirtualFile, Set<String>>();
+ for (int i = 0; i < myTreeRoot.getChildCount(); i++) {
+ Set<String> uncheckedCommits = new HashSet<String>();
+ 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<VirtualFile> roots = new HashSet<VirtualFile>();
+ final Set<VirtualFile> rootsWithMerges = new HashSet<VirtualFile>();
+ final Map<VirtualFile, List<String>> reorderedCommits = new HashMap<VirtualFile, List<String>>();
+ final Map<VirtualFile, Set<String>> uncheckedCommits = new HashMap<VirtualFile, Set<String>>();
+ for (int i = 0; i < myTreeRoot.getChildCount(); i++) {
+ CheckedTreeNode node = (CheckedTreeNode)myTreeRoot.getChildAt(i);
+ Root r = (Root)node.getUserObject();
+ Set<String> unchecked = new HashSet<String>();
+ 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<String> reordered = new ArrayList<String>();
+ 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<VcsException> exceptions = new ArrayList<VcsException>();
+ 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<VirtualFile, Set<String>> unchecked) {
+ ArrayList<VcsException> exceptions = new ArrayList<VcsException>();
+ List<Root> 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 TreeNode createTree() {
- DefaultMutableTreeNode treeRoot = new DefaultMutableTreeNode("ROOT", true);
- for (Root r : myRoots) {
- DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode(r, true);
+ private void updateTree(List<Root> roots, Map<VirtualFile, Set<String>> 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<String> unchecked =
+ uncheckedCommits != null && uncheckedCommits.containsKey(r.root) ? uncheckedCommits.get(r.root) : Collections.<String>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}
/**
* 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<Root> loadRoots(final Project project, final List<VirtualFile> roots, final Collection<VcsException> exceptions) {
+ static List<Root> loadRoots(final Project project,
+ final List<VirtualFile> roots,
+ final Collection<VcsException> exceptions,
+ final boolean fetchData) {
final ProgressManager manager = ProgressManager.getInstance();
final ArrayList<Root> rc = new ArrayList<Root>();
manager.runProcessWithProgressSynchronously(new Runnable() {
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<VcsException> exs = GitHandlerUtil.doSynchronouslyWithExceptions(fetch);
}
}
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());
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);
}
}
* @param exceptions the collected exceptions
*/
public static void showDialog(final Project project, List<VirtualFile> vcsRoots, final Collection<VcsException> exceptions) {
- final List<Root> roots = loadRoots(project, vcsRoots, exceptions);
+ final List<Root> 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<Root> rootsToPush = new ArrayList<Root>();
+ 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
*/
* {@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);
+ }
+ else if (root.remote == null) {
+ renderer.append(GitBundle.message("push.active.status.no.tracked"), SimpleTextAttributes.GRAYED_BOLD_ATTRIBUTES);
}
- if (root.remote == null) {
- return GitBundle.message("push.active.status.no.tracked");
+ 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.remoteCommits != 0 && root.commits.size() == 0) {
- return GitBundle.message("push.active.status.no.commits.behind", root.remoteCommits);
+ else if (root.commits.size() == 0) {
+ renderer.append(GitBundle.message("push.active.status.no.commits"), SimpleTextAttributes.GRAYED_BOLD_ATTRIBUTES);
}
- if (root.commits.size() == 0) {
- return GitBundle.message("push.active.status.no.commits");
+ else if (root.remoteCommits != 0) {
+ renderer.append(GitBundle.message("push.active.status.behind", root.remoteCommits), SimpleTextAttributes.ERROR_ATTRIBUTES);
}
- if (root.remoteCommits != 0) {
- return GitBundle.message("push.active.status.behind", root.remoteCommits);
+ 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
*/
* 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
*/
* the remote branch name
*/
String remoteBranch;
+ /**
+ * The commit that will be actually pushed
+ */
+ String commitToPush;
/**
* the commit
*/
* {@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);
}
}
}
--- /dev/null
+/*
+ * 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<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;
+ /**
+ * 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<VcsException> exceptions,
+ boolean autoStash,
+ Map<VirtualFile, List<String>> reorderedCommits,
+ Set<VirtualFile> rootsWithMerges) {
+ super(vcs, project, exceptions);
+ myAutoStash = autoStash;
+ myReorderedCommits = reorderedCommits;
+ myRootsWithMerges = rootsWithMerges;
+ myRebaseEditorService = GitRebaseEditorService.getInstance();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected GitLineHandler makeStartHandler(VirtualFile root) throws VcsException {
+ List<String> 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<String> 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<String> 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<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);
+ }
+ }
+ }
+}
\ No newline at end of file