git4idea: Added possiblitity to selectively push commits in push active branches...
authorConstantine Plotnikov <Constantine.Plotnikov@jetbrains.com>
Mon, 2 Nov 2009 20:28:29 +0000 (23:28 +0300)
committerConstantine Plotnikov <Constantine.Plotnikov@jetbrains.com>
Tue, 3 Nov 2009 12:58:35 +0000 (15:58 +0300)
plugins/git4idea/src/git4idea/checkin/GitPushActiveBranchesDialog.form
plugins/git4idea/src/git4idea/checkin/GitPushActiveBranchesDialog.java
plugins/git4idea/src/git4idea/checkin/GitPushRebaseProcess.java [new file with mode: 0644]
plugins/git4idea/src/git4idea/i18n/GitBundle.properties
plugins/git4idea/src/git4idea/ui/GitUIUtil.java

index 44dc5e59f6ac7e0b44c9bbf611dba62e78d54a16..7bac34fa91863e40caeb9f453ddeae3ee569d4e2 100644 (file)
         <properties/>
         <border type="none"/>
         <children>
-          <component id="c6c81" class="javax.swing.JTree" binding="myCommitTree">
+          <component id="56124" class="com.intellij.ui.CheckboxTree" binding="myCommitTree" custom-create="true">
             <constraints/>
-            <properties>
-              <rootVisible value="false"/>
-            </properties>
+            <properties/>
           </component>
         </children>
       </scrollpane>
           <text resource-bundle="git4idea/i18n/GitBundle" key="push.active.commits"/>
         </properties>
       </component>
-      <component id="3f20a" class="javax.swing.JButton" binding="myViewButton" default-binding="true">
+      <grid id="a16e5" layout-manager="GridLayoutManager" row-count="5" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
+        <margin top="0" left="0" bottom="0" right="0"/>
         <constraints>
-          <grid row="1" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="1" fill="1" indent="0" use-parent-layout="false"/>
+          <grid row="1" column="1" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
         </constraints>
-        <properties>
-          <enabled value="false"/>
-          <text resource-bundle="git4idea/i18n/GitBundle" key="push.active.view"/>
-        </properties>
-      </component>
+        <properties/>
+        <border type="none"/>
+        <children>
+          <component id="3f20a" class="javax.swing.JButton" binding="myViewButton" default-binding="true">
+            <constraints>
+              <grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="1" fill="1" indent="0" use-parent-layout="false"/>
+            </constraints>
+            <properties>
+              <enabled value="false"/>
+              <text resource-bundle="git4idea/i18n/GitBundle" key="push.active.view"/>
+            </properties>
+          </component>
+          <component id="2dbc5" class="javax.swing.JButton" binding="myFetchButton" default-binding="true">
+            <constraints>
+              <grid row="2" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
+            </constraints>
+            <properties>
+              <text resource-bundle="git4idea/i18n/GitBundle" key="push.active.fetch"/>
+              <toolTipText resource-bundle="git4idea/i18n/GitBundle" key="push.active.fetch.tooltip"/>
+            </properties>
+          </component>
+          <component id="5c023" class="javax.swing.JButton" binding="myRebaseButton" default-binding="true">
+            <constraints>
+              <grid row="3" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
+            </constraints>
+            <properties>
+              <text resource-bundle="git4idea/i18n/GitBundle" key="push.active.rebase"/>
+              <toolTipText resource-bundle="git4idea/i18n/GitBundle" key="push.active.rebase.tooltip"/>
+            </properties>
+          </component>
+          <component id="8ca38" class="javax.swing.JCheckBox" binding="myAutoStashCheckBox" default-binding="true">
+            <constraints>
+              <grid row="4" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+            </constraints>
+            <properties>
+              <selected value="true"/>
+              <text resource-bundle="git4idea/i18n/GitBundle" key="push.active.autostash"/>
+              <toolTipText resource-bundle="git4idea/i18n/GitBundle" key="push.active.autostash.tooltip"/>
+            </properties>
+          </component>
+          <vspacer id="90dc0">
+            <constraints>
+              <grid row="1" column="0" row-span="1" col-span="1" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
+            </constraints>
+          </vspacer>
+        </children>
+      </grid>
     </children>
   </grid>
 </form>
index f8ebb6730eeccd49123aeed83da6f7417b525fee..20a88fa74379b42c013f2737619222787cfbd72c 100644 (file)
@@ -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<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();
@@ -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 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}
@@ -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<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() {
@@ -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<VcsException> 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<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
      */
@@ -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);
+      }
+      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
      */
@@ -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 (file)
index 0000000..08422c0
--- /dev/null
@@ -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<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
index d1240b8ff96d257a0bef811c1197e0366469dcda..1e7d7abdafcfdb3d1e4ccb60d903ba1067440794 100644 (file)
@@ -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=<li>{0}</li>
+errors.message=<html>The git operation ended with multiple errors:<ul></ul></html>
 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=<html><span style="color: #7f7f7f">{0}</span>: {1}</html>
+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=<html><span style="color: red"><b>{0}</b>[{1} -> {2}#{3}]</span></html>
-push.active.root.node.no.branch=<html><span style="color: red"><b>{0}</b></span></html>
-push.active.root.node.no.commits=<html><span style="color: #7f7f7f"><b>{0}</b>[{1} -> {2}#{3}]</span></html>
-push.active.root.node.no.tracked=<html><span style="color: #7f7f7f"><b>{0}</b>[{1}]</span></html>
-push.active.root.node.push=<html><b>{0}</b>[{1} -> {2}#{3}]</html>
-push.active.status.behind=<html>Status: <span style="color: red">Unable to push. The current branch is behind tracked branch by {0} commit(s).</span></html>
-push.active.status.no.branch=<html>Status: <span style="color: red">The head is not on the branch.</span></html>
-push.active.status.no.commits.behind=<html>Status: <span style="color: #7f7f7f">Nothing to push. The current branch is behind tracked branch by {0} commit(s).</span></html>
-push.active.status.no.commits=<html>Status: <span style="color: #7f7f7f">Nothing to push.</span></html>
-push.active.status.no.tracked=<html>Status: <span style="color: #7f7f7f">No tracked branch is configured.</span></html>
-push.active.status.push=<html>Status: {0} commit(s) will be pushed.</html>
+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
index bcd9a9014b3f27ef8475a5a3e4a9827324e7de59..11aa2594ad03a6874e009defd453bbcc29f52dc7 100644 (file)
@@ -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<VcsException> 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
    *