git4idea: Added possiblitity to selectively push commits in push active branches dialog

This commit is contained in:
Constantine Plotnikov
2009-11-03 15:58:35 +03:00
parent 86aae04157
commit 509ef6fc50
5 changed files with 767 additions and 96 deletions
@@ -15,11 +15,9 @@
<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>
@@ -32,15 +30,58 @@
<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>
@@ -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 TreeNode createTree() {
DefaultMutableTreeNode treeRoot = new DefaultMutableTreeNode("ROOT", true);
for (Root r : myRoots) {
DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode(r, true);
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 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);
}
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);
}
}
}
@@ -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);
}
}
}
}
@@ -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
@@ -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
*