2 * Copyright 2000-2009 JetBrains s.r.o.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
16 package git4idea.checkin;
18 import com.intellij.ide.GeneralSettings;
19 import com.intellij.notification.NotificationType;
20 import com.intellij.openapi.application.ModalityState;
21 import com.intellij.openapi.diagnostic.Logger;
22 import com.intellij.openapi.progress.EmptyProgressIndicator;
23 import com.intellij.openapi.progress.ProgressIndicator;
24 import com.intellij.openapi.progress.ProgressManager;
25 import com.intellij.openapi.progress.Task;
26 import com.intellij.openapi.project.Project;
27 import com.intellij.openapi.project.ex.ProjectManagerEx;
28 import com.intellij.openapi.ui.DialogWrapper;
29 import com.intellij.openapi.util.Clock;
30 import com.intellij.openapi.util.text.StringUtil;
31 import com.intellij.openapi.vcs.VcsException;
32 import com.intellij.openapi.vcs.update.UpdatedFiles;
33 import com.intellij.openapi.vfs.VirtualFile;
34 import com.intellij.ui.CheckboxTree;
35 import com.intellij.ui.CheckedTreeNode;
36 import com.intellij.ui.ColoredTreeCellRenderer;
37 import com.intellij.ui.SimpleTextAttributes;
38 import com.intellij.util.continuation.ContinuationContext;
39 import com.intellij.util.text.DateFormatUtil;
40 import com.intellij.util.ui.UIUtil;
41 import com.intellij.util.ui.tree.TreeUtil;
42 import git4idea.GitBranch;
43 import git4idea.GitRevisionNumber;
44 import git4idea.GitUtil;
45 import git4idea.GitVcs;
46 import git4idea.actions.GitRepositoryAction;
47 import git4idea.actions.GitShowAllSubmittedFilesAction;
48 import git4idea.commands.*;
49 import git4idea.config.GitVcsSettings;
50 import git4idea.i18n.GitBundle;
51 import git4idea.rebase.GitRebaser;
52 import git4idea.ui.GitUIUtil;
53 import git4idea.update.*;
54 import org.jetbrains.annotations.NotNull;
55 import org.jetbrains.annotations.Nullable;
58 import javax.swing.event.ChangeEvent;
59 import javax.swing.event.ChangeListener;
60 import javax.swing.event.TreeSelectionEvent;
61 import javax.swing.event.TreeSelectionListener;
62 import javax.swing.tree.DefaultMutableTreeNode;
63 import javax.swing.tree.DefaultTreeModel;
64 import javax.swing.tree.TreePath;
66 import java.awt.event.ActionEvent;
67 import java.awt.event.ActionListener;
69 import java.util.List;
70 import java.util.concurrent.atomic.AtomicReference;
72 import static git4idea.ui.GitUIUtil.notifyError;
73 import static git4idea.ui.GitUIUtil.notifyMessage;
76 * The dialog that allows pushing active branches.
78 public class GitPushActiveBranchesDialog extends DialogWrapper {
79 private static final int HASH_PREFIX_SIZE = 8; // Amount of digits to show in commit prefix
81 private final Project myProject;
82 private final List<VirtualFile> myVcsRoots;
84 private JPanel myRootPanel;
85 private JButton myViewButton; // view commits
86 private JButton myFetchButton;
87 private JButton myRebaseButton;
88 private JButton myPushButton;
90 private CheckboxTree myCommitTree; // The commit tree (sorted by vcs roots)
91 private CheckedTreeNode myTreeRoot;
93 private JRadioButton myStashRadioButton; // Save files policy option
94 private JRadioButton myShelveRadioButton;
96 private static final Logger LOG = Logger.getInstance(GitPushActiveBranchesDialog.class.getName());
97 private final GeneralSettings myGeneralSettings;
98 private final ProjectManagerEx myProjectManager;
101 * A modification of Runnable with the roots-parameter.
102 * Also for user code simplification myInvokeInAwt variable stores the need of calling run in AWT thread.
104 private static abstract class PushActiveBranchRunnable {
105 abstract void run(List<Root> roots);
109 * Constructs new dialog. Loads settings, registers listeners.
110 * @param project the project
111 * @param vcsRoots the vcs roots
112 * @param roots the loaded information about roots
114 private GitPushActiveBranchesDialog(final Project project, List<VirtualFile> vcsRoots, List<Root> roots) {
115 super(project, true);
116 myVcs = GitVcs.getInstance(project);
118 myVcsRoots = vcsRoots;
119 myGeneralSettings = GeneralSettings.getInstance();
120 myProjectManager = ProjectManagerEx.getInstanceEx();
122 updateTree(roots, null);
125 final GitVcsSettings settings = GitVcsSettings.getInstance(project);
126 if (settings != null) {
127 UpdatePolicyUtils.updatePolicyItem(settings.getPushActiveBranchesRebaseSavePolicy(), myStashRadioButton, myShelveRadioButton);
129 ChangeListener listener = new ChangeListener() {
130 public void stateChanged(ChangeEvent e) {
131 if (settings != null) {
132 settings.setPushActiveBranchesRebaseSavePolicy(UpdatePolicyUtils.getUpdatePolicy(myStashRadioButton, myShelveRadioButton));
136 myStashRadioButton.addChangeListener(listener);
137 myShelveRadioButton.addChangeListener(listener);
138 myCommitTree.getSelectionModel().addTreeSelectionListener(new TreeSelectionListener() {
139 public void valueChanged(TreeSelectionEvent e) {
140 TreePath path = myCommitTree.getSelectionModel().getSelectionPath();
142 myViewButton.setEnabled(false);
145 DefaultMutableTreeNode node = (DefaultMutableTreeNode)path.getLastPathComponent();
146 myViewButton.setEnabled(node != null && myCommitTree.getSelectionCount() == 1 && node.getUserObject() instanceof Commit);
149 myViewButton.addActionListener(new ActionListener() {
150 public void actionPerformed(ActionEvent e) {
151 TreePath path = myCommitTree.getSelectionModel().getSelectionPath();
155 DefaultMutableTreeNode node = (DefaultMutableTreeNode)path.getLastPathComponent();
156 if (node == null || !(node.getUserObject() instanceof Commit)) {
159 Commit c = (Commit)node.getUserObject();
160 GitShowAllSubmittedFilesAction.showSubmittedFiles(project, c.revision.asString(), c.root.root);
163 myFetchButton.addActionListener(new ActionListener() {
164 public void actionPerformed(ActionEvent e) {
168 myRebaseButton.addActionListener(new ActionListener() {
169 public void actionPerformed(ActionEvent e) {
174 myPushButton.addActionListener(new ActionListener() {
175 @Override public void actionPerformed(ActionEvent e) {
180 setTitle(GitBundle.getString("push.active.title"));
181 setOKButtonText(GitBundle.getString("push.active.rebase.and.push"));
182 setCancelButtonText(GitBundle.getString("git.push.active.close"));
187 * Show dialog for the project
189 public static void showDialogForProject(final Project project) {
190 GitVcs vcs = GitVcs.getInstance(project);
191 List<VirtualFile> roots = GitRepositoryAction.getGitRoots(project, vcs);
195 List<VcsException> pushExceptions = new ArrayList<VcsException>();
196 showDialog(project, roots, pushExceptions);
197 vcs.showErrors(pushExceptions, GitBundle.getString("push.active.action.name"));
202 * @param project the context project
203 * @param vcsRoots the vcs roots in the project
204 * @param exceptions the collected exceptions
206 public static void showDialog(final Project project, final List<VirtualFile> vcsRoots, final Collection<VcsException> exceptions) {
207 final List<Root> emptyRoots = loadRoots(project, vcsRoots, exceptions, false); // collect roots without fetching - just to show dialog
208 if (!exceptions.isEmpty()) {
209 exceptions.addAll(exceptions);
212 final GitPushActiveBranchesDialog d = new GitPushActiveBranchesDialog(project, vcsRoots, emptyRoots);
213 d.refreshTree(true, null, false); // start initial fetch
221 * This is called when "Rebase and Push" button (default button) is pressed.
222 * 1. Closes the dialog.
223 * 2. Fetches project and rebases.
224 * 3. Repeats step 2 if needed - while current repository is behind the parent one.
226 * It may fail on one of these steps (especially on rebasing with conflict) - then a notification error will be shown and the process
227 * will be interrupted.
229 private void rebaseAndPush() {
230 final Task.Backgroundable rebaseAndPushTask = new Task.Backgroundable(myProject, GitBundle.getString("push.active.fetching")) {
231 public void run(@NotNull ProgressIndicator indicator) {
232 List<VcsException> exceptions = new ArrayList<VcsException>();
233 List<VcsException> pushExceptions = new ArrayList<VcsException>();
234 for (int i = 0; i < 3; i++) {
235 RebaseInfo rebaseInfo = collectRebaseInfo();
237 if (rebaseInfo.reorderedCommits.isEmpty()) { // if we have to reorder commits, rebase must pre
238 final Collection<Root> rootsToPush = getRootsToPush(); // collect roots from the dialog
239 exceptions = executePushCommand(rootsToPush);
240 if (exceptions.isEmpty() && !rootsToPush.isEmpty()) { // if nothing to push, execute rebase anyway
242 for (Root root : rootsToPush) {
243 commitsNum += root.commits.size();
244 Set<String> unchecked = rebaseInfo.uncheckedCommits.get(root.root);
245 if (unchecked != null) {
246 commitsNum -= unchecked.size();
249 GitUIUtil.notifySuccess(myProject, "Pushed successfully",
250 "Pushed " + commitsNum + " " + StringUtil.pluralize("commit", commitsNum) + ".");
253 pushExceptions = new ArrayList<VcsException>(exceptions);
257 final List<Root> roots = loadRoots(myProject, myVcsRoots, exceptions, true); // fetch
258 if (!exceptions.isEmpty()) {
259 notifyMessage(myProject, "Failed to fetch", null, NotificationType.ERROR, true, exceptions);
262 updateTree(roots, rebaseInfo.uncheckedCommits);
264 if (isRebaseNeeded()) {
265 rebaseInfo = collectRebaseInfo();
266 executeRebase(exceptions, rebaseInfo);
267 if (!exceptions.isEmpty()) {
268 notifyMessage(myProject, "Failed to rebase", null, NotificationType.ERROR, true, exceptions);
271 GitUtil.refreshFiles(myProject, rebaseInfo.roots);
274 notifyMessage(myProject, "Failed to push", "Update project and push again", NotificationType.ERROR, true, pushExceptions);
277 GitVcs.runInBackground(rebaseAndPushTask);
281 * Pushes selected commits synchronously in foreground.
283 private void push() {
284 final Collection<Root> rootsToPush = getRootsToPush();
285 final AtomicReference<Collection<VcsException>> errors = new AtomicReference<Collection<VcsException>>();
287 ProgressManager.getInstance().runProcessWithProgressSynchronously(new Runnable() {
289 errors.set(executePushCommand(rootsToPush));
291 }, GitBundle.getString("push.active.pushing"), true, myProject);
292 if (errors.get() != null && !errors.get().isEmpty()) {
293 GitUIUtil.showOperationErrors(myProject, errors.get(), GitBundle.getString("push.active.pushing"));
295 refreshTree(false, null);
299 * Executes 'git push' for the given roots to push.
300 * Returns the list of errors if there were any.
302 private List<VcsException> executePushCommand(final Collection<Root> rootsToPush) {
303 final ArrayList<VcsException> errors = new ArrayList<VcsException>();
304 for (Root r : rootsToPush) {
305 GitLineHandler h = new GitLineHandler(myProject, r.root, GitCommand.PUSH);
306 String src = r.commitToPush != null ? r.commitToPush : r.currentBranch;
307 h.addParameters("-v", r.remoteName, src + ":" + r.remoteBranch);
308 GitPushUtils.trackPushRejectedAsError(h, "Rejected push (" + r.root.getPresentableUrl() + "): ");
309 errors.addAll(GitHandlerUtil.doSynchronouslyWithExceptions(h));
315 * From the dialog collects roots and commits to be pushed.
316 * @return roots to be pushed.
318 private Collection<Root> getRootsToPush() {
319 final ArrayList<Root> rootsToPush = new ArrayList<Root>();
320 for (int i = 0; i < myTreeRoot.getChildCount(); i++) {
321 CheckedTreeNode node = (CheckedTreeNode) myTreeRoot.getChildAt(i);
322 Root r = (Root)node.getUserObject();
323 if (r.remoteName == null || r.commits.size() == 0) {
326 boolean topCommit = true;
327 for (int j = 0; j < node.getChildCount(); j++) {
328 if (node.getChildAt(j) instanceof CheckedTreeNode) {
329 CheckedTreeNode commitNode = (CheckedTreeNode)node.getChildAt(j);
330 if (commitNode.isChecked()) {
331 Commit commit = (Commit)commitNode.getUserObject();
333 r.commitToPush = commit.revision.asString();
346 * Executes when FETCH button is pressed.
347 * Fetches repository in background. Then updates the commit tree.
349 private void fetch() {
350 Map<VirtualFile, Set<String>> unchecked = new HashMap<VirtualFile, Set<String>>();
351 for (int i = 0; i < myTreeRoot.getChildCount(); i++) {
352 Set<String> uncheckedCommits = new HashSet<String>();
353 CheckedTreeNode node = (CheckedTreeNode)myTreeRoot.getChildAt(i);
354 Root r = (Root)node.getUserObject();
355 for (int j = 0; j < node.getChildCount(); j++) {
356 if (node.getChildAt(j) instanceof CheckedTreeNode) {
357 CheckedTreeNode commitNode = (CheckedTreeNode)node.getChildAt(j);
358 if (!commitNode.isChecked()) {
359 uncheckedCommits.add(((Commit)commitNode.getUserObject()).commitId());
363 if (!uncheckedCommits.isEmpty()) {
364 unchecked.put(r.root, uncheckedCommits);
367 refreshTree(true, unchecked);
371 * The rebase operation is needed if the current branch is behind remote branch or if some commit is not selected.
372 * @return true if rebase is needed for at least one vcs root
374 private boolean isRebaseNeeded() {
375 for (int i = 0; i < myTreeRoot.getChildCount(); i++) {
376 CheckedTreeNode node = (CheckedTreeNode)myTreeRoot.getChildAt(i);
377 Root r = (Root)node.getUserObject();
378 if (r.commits.size() == 0) {
381 boolean seenCheckedNode = false;
382 for (int j = 0; j < node.getChildCount(); j++) {
383 if (node.getChildAt(j) instanceof CheckedTreeNode) {
384 CheckedTreeNode commitNode = (CheckedTreeNode)node.getChildAt(j);
385 if (commitNode.isChecked()) {
386 seenCheckedNode = true;
389 if (seenCheckedNode) {
395 if (seenCheckedNode && r.remoteCommits > 0) {
403 * This is called when rebase is pressed: executes rebase in background.
405 private void rebase() {
406 final List<VcsException> exceptions = new ArrayList<VcsException>();
407 final RebaseInfo rebaseInfo = collectRebaseInfo();
409 ProgressManager.getInstance().runProcessWithProgressSynchronously(new Runnable() {
411 executeRebase(exceptions, rebaseInfo);
413 }, GitBundle.getString("push.active.rebasing"), true, myProject);
414 if (!exceptions.isEmpty()) {
415 GitUIUtil.showOperationErrors(myProject, exceptions, "git rebase");
417 refreshTree(false, rebaseInfo.uncheckedCommits);
418 GitUtil.refreshFiles(myProject, rebaseInfo.roots);
421 private boolean executeRebase(final List<VcsException> exceptions, RebaseInfo rebaseInfo) {
422 // TODO this is a workaround to attach PushActiveBranched to the new update.
423 // at first we update via rebase
424 boolean result = new GitUpdateProcess(myProject, new EmptyProgressIndicator(), rebaseInfo.roots, UpdatedFiles.create()).update(true, true);
426 // then we reorder commits
428 // getting new rebase info because commit hashes changed because of rebase
429 final List<Root> roots = loadRoots(myProject, new ArrayList<VirtualFile>(rebaseInfo.roots), exceptions, false);
430 updateTree(roots, rebaseInfo.uncheckedCommits);
431 rebaseInfo = collectRebaseInfo();
432 return reorderCommitsIfNeeded(rebaseInfo);
434 notifyMessage(myProject, "Commits weren't pushed", "Rebase failed.", NotificationType.WARNING, true, null);
440 private boolean reorderCommitsIfNeeded(@NotNull final RebaseInfo rebaseInfo) {
441 if (rebaseInfo.reorderedCommits.isEmpty()) {
445 ProgressIndicator progressIndicator = ProgressManager.getInstance().getProgressIndicator();
446 if (progressIndicator == null) {
447 progressIndicator = new EmptyProgressIndicator();
449 String stashMessage = "Uncommitted changes before rebase operation at " + DateFormatUtil.formatDateTime(Clock.getTime());
450 final GitChangesSaver saver = rebaseInfo.policy == GitVcsSettings.UpdateChangesPolicy.SHELVE ? new GitShelveChangesSaver(myProject, progressIndicator, stashMessage) : new GitStashChangesSaver(myProject, progressIndicator, stashMessage);
452 final Boolean[] result = new Boolean[1];
454 new GitUpdateLikeProcess(myProject) {
456 protected void runImpl(ContinuationContext context) {
458 final Set<VirtualFile> rootsToReorder = rebaseInfo.reorderedCommits.keySet();
459 saver.saveLocalChanges(rootsToReorder);
462 GitRebaser rebaser = new GitRebaser(myProject);
463 for (Map.Entry<VirtualFile, List<String>> rootToCommits: rebaseInfo.reorderedCommits.entrySet()) {
464 final VirtualFile root = rootToCommits.getKey();
465 GitBranch b = GitBranch.current(myProject, root);
467 LOG.info("executeRebase: current branch is null");
470 GitBranch t = b.tracked(myProject, root);
472 LOG.info("executeRebase: tracked branch is null");
476 final GitRevisionNumber mergeBase = b.getMergeBase(myProject, root, t);
477 if (mergeBase == null) {
478 LOG.info("executeRebase: merge base is null for " + b + " and " + t);
482 String parentCommit = mergeBase.getRev();
483 result[0] = rebaser.reoderCommitsIfNeeded(root, parentCommit, rootToCommits.getValue());
486 } catch (VcsException e) {
487 notifyMessage(myProject, "Commits weren't pushed", "Failed to reorder commits", NotificationType.WARNING, true,
488 Collections.singleton(e));
490 saver.restoreLocalChanges(context);
492 } catch (VcsException e) {
493 LOG.info("Couldn't save local changes", e);
494 notifyError(myProject, "Couldn't save local changes",
495 "Tried to save uncommitted changes in " + saver.getSaverName() + " before update, but failed with an error.<br/>" +
496 "Update was cancelled.", true, e);
503 private static class RebaseInfo {
504 final Set<VirtualFile> rootsWithMerges;
505 private final Map<VirtualFile, Set<String>> uncheckedCommits;
506 private final Set<VirtualFile> roots;
507 private final GitVcsSettings.UpdateChangesPolicy policy;
508 final Map<VirtualFile,List<String>> reorderedCommits;
510 public RebaseInfo(Map<VirtualFile, List<String>> reorderedCommits,
511 Set<VirtualFile> rootsWithMerges,
512 Map<VirtualFile, Set<String>> uncheckedCommits, Set<VirtualFile> roots,
513 GitVcsSettings.UpdateChangesPolicy policy) {
515 this.reorderedCommits = reorderedCommits;
516 this.rootsWithMerges = rootsWithMerges;
517 this.uncheckedCommits = uncheckedCommits;
519 this.policy = policy;
523 private RebaseInfo collectRebaseInfo() {
524 final Set<VirtualFile> roots = new HashSet<VirtualFile>();
525 final Set<VirtualFile> rootsWithMerges = new HashSet<VirtualFile>();
526 final Map<VirtualFile, List<String>> reorderedCommits = new HashMap<VirtualFile, List<String>>();
527 final Map<VirtualFile, Set<String>> uncheckedCommits = new HashMap<VirtualFile, Set<String>>();
528 for (int i = 0; i < myTreeRoot.getChildCount(); i++) {
529 CheckedTreeNode node = (CheckedTreeNode)myTreeRoot.getChildAt(i);
530 Root r = (Root)node.getUserObject();
531 Set<String> unchecked = new HashSet<String>();
532 uncheckedCommits.put(r.root, unchecked);
533 if (r.commits.size() == 0) {
534 if (r.remoteCommits > 0) {
539 boolean seenCheckedNode = false;
540 boolean reorderNeeded = false;
541 boolean seenMerges = false;
542 for (int j = 0; j < node.getChildCount(); j++) {
543 if (node.getChildAt(j) instanceof CheckedTreeNode) {
544 CheckedTreeNode commitNode = (CheckedTreeNode)node.getChildAt(j);
545 Commit commit = (Commit)commitNode.getUserObject();
546 seenMerges |= commit.isMerge;
547 if (commitNode.isChecked()) {
548 seenCheckedNode = true;
551 unchecked.add(commit.commitId());
552 if (seenCheckedNode) {
553 reorderNeeded = true;
559 rootsWithMerges.add(r.root);
561 if (r.remoteCommits > 0 || reorderNeeded) {
565 List<String> reordered = new ArrayList<String>();
566 for (int j = 0; j < node.getChildCount(); j++) {
567 if (node.getChildAt(j) instanceof CheckedTreeNode) {
568 CheckedTreeNode commitNode = (CheckedTreeNode)node.getChildAt(j);
569 if (!commitNode.isChecked()) {
570 Commit commit = (Commit)commitNode.getUserObject();
571 reordered.add(commit.revision.asString());
575 for (int j = 0; j < node.getChildCount(); j++) {
576 if (node.getChildAt(j) instanceof CheckedTreeNode) {
577 CheckedTreeNode commitNode = (CheckedTreeNode)node.getChildAt(j);
578 if (commitNode.isChecked()) {
579 Commit commit = (Commit)commitNode.getUserObject();
580 reordered.add(commit.revision.asString());
584 Collections.reverse(reordered);
585 reorderedCommits.put(r.root, reordered);
588 final GitVcsSettings.UpdateChangesPolicy p = UpdatePolicyUtils.getUpdatePolicy(myStashRadioButton, myShelveRadioButton);
589 assert p == GitVcsSettings.UpdateChangesPolicy.STASH || p == GitVcsSettings.UpdateChangesPolicy.SHELVE;
591 return new RebaseInfo(reorderedCommits, rootsWithMerges, uncheckedCommits, roots, p);
597 * @param fetchData if true, the current state is fetched from remote
598 * @param unchecked the map from vcs root to commit identifiers that should be unchecked
599 * @param updateCommits if true, then the specified unchecked commits should be used for building tree.
600 * if false, then <code>unchecked</code> are ignored and values are retrieved from the dialog.
601 * The latter is used for initial refresh which may finish after user has deselected some commits.
603 private void refreshTree(final boolean fetchData, final Map<VirtualFile, Set<String>> unchecked, final boolean updateCommits) {
604 myCommitTree.setPaintBusy(true);
605 loadRootsInBackground(fetchData, new PushActiveBranchRunnable(){
607 void run(List<Root> roots) {
608 Map<VirtualFile, Set<String>> uncheckedCommits;
609 if (!updateCommits) {
610 RebaseInfo info = collectRebaseInfo();
611 uncheckedCommits = info.uncheckedCommits;
613 uncheckedCommits = unchecked;
615 updateTree(roots, uncheckedCommits);
617 myCommitTree.setPaintBusy(false);
622 private void refreshTree(boolean fetchData, Map<VirtualFile, Set<String>> unchecked) {
623 refreshTree(fetchData, unchecked, true);
627 * Update the tree according to the list of loaded roots
630 * @param roots the list of roots to add to the tree
631 * @param uncheckedCommits the map from vcs root to commit identifiers that should be uncheckedCommits
633 private void updateTree(List<Root> roots, Map<VirtualFile, Set<String>> uncheckedCommits) {
634 myTreeRoot.removeAllChildren();
636 roots = Collections.emptyList();
638 for (Root r : roots) {
639 CheckedTreeNode rootNode = new CheckedTreeNode(r);
640 Status status = new Status();
642 rootNode.add(new DefaultMutableTreeNode(status, false));
643 Set<String> unchecked =
644 uncheckedCommits != null && uncheckedCommits.containsKey(r.root) ? uncheckedCommits.get(r.root) : Collections.<String>emptySet();
645 for (Commit c : r.commits) {
646 CheckedTreeNode child = new CheckedTreeNode(c);
648 child.setChecked(r.remoteName != null && !unchecked.contains(c.commitId()));
650 myTreeRoot.add(rootNode);
654 // Execute from AWT thread.
655 private void updateUI() {
656 ((DefaultTreeModel)myCommitTree.getModel()).reload(myTreeRoot);
657 TreeUtil.expandAll(myCommitTree);
662 * Update buttons on the form
664 private void updateButtons() {
666 boolean wasCheckedNode = false;
667 boolean reorderMerges = false;
668 for (int i = 0; i < myTreeRoot.getChildCount(); i++) {
669 CheckedTreeNode node = (CheckedTreeNode)myTreeRoot.getChildAt(i);
670 boolean seenCheckedNode = false;
671 boolean reorderNeeded = false;
672 boolean seenMerges = false;
673 boolean seenUnchecked = false;
674 for (int j = 0; j < node.getChildCount(); j++) {
675 if (node.getChildAt(j) instanceof CheckedTreeNode) {
676 CheckedTreeNode commitNode = (CheckedTreeNode)node.getChildAt(j);
677 Commit commit = (Commit)commitNode.getUserObject();
678 seenMerges |= commit.isMerge;
679 if (commitNode.isChecked()) {
680 seenCheckedNode = true;
683 seenUnchecked = true;
684 if (seenCheckedNode) {
685 reorderNeeded = true;
690 if (!seenCheckedNode) {
693 Root r = (Root)node.getUserObject();
694 if (seenMerges && seenUnchecked) {
695 error = GitBundle.getString("push.active.error.merges.unchecked");
697 if (seenMerges && reorderNeeded) {
698 reorderMerges = true;
699 error = GitBundle.getString("push.active.error.reorder.merges");
703 error = GitBundle.getString("push.active.error.reorder.needed");
706 if (r.currentBranch == null) {
708 error = GitBundle.getString("push.active.error.no.branch");
712 wasCheckedNode |= r.remoteBranch != null;
713 if (r.remoteCommits != 0 && r.commits.size() != 0) {
715 error = GitBundle.getString("push.active.error.behind");
720 boolean rebaseNeeded = isRebaseNeeded();
721 myPushButton.setEnabled(wasCheckedNode && error == null && !rebaseNeeded);
723 myRebaseButton.setEnabled(rebaseNeeded && !reorderMerges);
724 setOKActionEnabled(myPushButton.isEnabled() || myRebaseButton.isEnabled());
731 protected JComponent createCenterPanel() {
739 protected String getDimensionServiceKey() {
740 return getClass().getName();
747 protected String getHelpId() {
748 return "reference.VersionControl.Git.PushActiveBranches";
754 * @param project the project
755 * @param roots the VCS root list
756 * @param exceptions the list of of exceptions to use
757 * @param fetchData if true, the data for remote is fetched.
758 * @return the loaded information about vcs roots
760 private static List<Root> loadRoots(final Project project,
761 final List<VirtualFile> roots,
762 final Collection<VcsException> exceptions,
763 final boolean fetchData) {
764 final ArrayList<Root> rc = new ArrayList<Root>();
765 for (VirtualFile root : roots) {
770 GitBranch b = GitBranch.current(project, root);
772 r.currentBranch = b.getFullName();
773 r.remoteName = b.getTrackedRemoteName(project, root);
774 r.remoteBranch = b.getTrackedBranchName(project, root);
775 if (r.remoteName != null) {
776 if (fetchData && !r.remoteName.equals(".")) {
777 GitLineHandler fetch = new GitLineHandler(project, root, GitCommand.FETCH);
778 fetch.addParameters(r.remoteName, "-v");
779 Collection<VcsException> exs = GitHandlerUtil.doSynchronouslyWithExceptions(fetch);
780 exceptions.addAll(exs);
782 GitBranch tracked = b.tracked(project, root);
783 assert tracked != null : "Tracked branch cannot be null here";
784 final boolean trackedBranchExists = tracked.exists(root);
785 if (!trackedBranchExists) {
786 LOG.info("loadRoots tracked branch " + tracked + " doesn't exist yet");
789 // check what remote commits are not yet merged
790 if (trackedBranchExists) {
791 GitSimpleHandler toPull = new GitSimpleHandler(project, root, GitCommand.LOG);
792 toPull.addParameters("--pretty=format:%H", r.currentBranch + ".." + tracked.getFullName());
793 toPull.setNoSSH(true);
794 toPull.setStdoutSuppressed(true);
795 StringScanner su = new StringScanner(toPull.run());
796 while (su.hasMoreData()) {
797 if (su.line().trim().length() != 0) {
803 // check what local commits are to be pushed
804 GitSimpleHandler toPush = new GitSimpleHandler(project, root, GitCommand.LOG);
805 // if the tracked branch doesn't exist yet (nobody pushed the branch yet), show all commits on this branch.
806 final String revisions = trackedBranchExists ? tracked.getFullName() + ".." + r.currentBranch : r.currentBranch;
807 toPush.addParameters("--pretty=format:%H%x20%ct%x20%at%x20%s%n%P", revisions);
808 toPush.setNoSSH(true);
809 toPush.setStdoutSuppressed(true);
810 StringScanner sp = new StringScanner(toPush.run());
811 while (sp.hasMoreData()) {
816 Commit c = new Commit();
818 String hash = sp.spaceToken();
819 String time = sp.spaceToken();
820 c.revision = new GitRevisionNumber(hash, new Date(Long.parseLong(time) * 1000L));
821 c.authorTime = sp.spaceToken();
822 c.message = sp.line();
823 c.isMerge = sp.line().indexOf(' ') != -1;
829 catch (VcsException e) {
837 * Loads roots (fetches) in background. When finished, executes the given task in the AWT thread.
840 private void loadRootsInBackground(final boolean fetchData, @Nullable final PushActiveBranchRunnable postUiTask) {
841 final Task.Backgroundable fetchTask = new Task.Backgroundable(myProject, GitBundle.getString("push.active.fetching")) {
842 public void run(@NotNull ProgressIndicator indicator) {
843 final Collection<VcsException> exceptions = new HashSet<VcsException>(1);
844 final List<Root> roots = loadRoots(myProject, myVcsRoots, exceptions, fetchData);
845 if (!exceptions.isEmpty()) {
846 setErrorText(GitBundle.getString("push.active.fetch.failed"));
850 if (postUiTask != null) {
851 UIUtil.invokeAndWaitIfNeeded(new Runnable() {
854 postUiTask.run(roots);
860 UIUtil.invokeLaterIfNeeded(new Runnable() {
861 @Override public void run() {
862 final ModalityState modalityState = ModalityState.stateForComponent(getContentPane());
863 myVcs.getTaskQueue().run(fetchTask, modalityState, null);
869 * Create UI components for the dialog
871 private void createUIComponents() {
872 myTreeRoot = new CheckedTreeNode("ROOT");
873 myCommitTree = new CheckboxTree(new CheckboxTree.CheckboxTreeCellRenderer() {
875 public void customizeRenderer(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
876 // Fix GTK background
877 if (UIUtil.isUnderGTKLookAndFeel()) {
878 final Color background = selected ? UIUtil.getTreeSelectionBackground() : UIUtil.getTreeTextBackground();
879 UIUtil.changeBackGround(this, background);
881 ColoredTreeCellRenderer r = getTextRenderer();
882 if (!(value instanceof DefaultMutableTreeNode)) {
884 renderUnknown(r, value);
887 DefaultMutableTreeNode node = (DefaultMutableTreeNode)value;
888 if (!(node.getUserObject() instanceof Node)) {
890 renderUnknown(r, node.getUserObject());
893 ((Node)node.getUserObject()).render(r);
897 * Render unknown node
899 * @param r a renderer to use
900 * @param value the unknown value
902 private void renderUnknown(ColoredTreeCellRenderer r, Object value) {
903 r.append("UNSUPPORTED NODE TYPE: " + (value == null ? "null" : value.getClass().getName()), SimpleTextAttributes.ERROR_ATTRIBUTES);
907 protected void onNodeStateChanged(CheckedTreeNode node) {
909 super.onNodeStateChanged(node);
916 * The base class for nodes in the tree
918 static abstract class Node {
920 * Render the node text
922 protected abstract void render(ColoredTreeCellRenderer renderer);
925 static class Status extends Node {
927 private String myMessage = "";
930 protected void render(ColoredTreeCellRenderer renderer) {
931 renderer.append(GitBundle.getString("push.active.status.status"));
932 if (root.currentBranch == null) {
933 myMessage = GitBundle.message("push.active.status.no.branch");
934 renderer.append(myMessage, SimpleTextAttributes.ERROR_ATTRIBUTES);
936 else if (root.remoteName == null) {
937 myMessage = GitBundle.message("push.active.status.no.tracked");
938 renderer.append(myMessage, SimpleTextAttributes.GRAYED_BOLD_ATTRIBUTES);
940 else if (root.remoteCommits != 0 && root.commits.size() == 0) {
941 myMessage = GitBundle.message("push.active.status.no.commits.behind", root.remoteCommits);
942 renderer.append(myMessage, SimpleTextAttributes.GRAYED_BOLD_ATTRIBUTES);
944 else if (root.commits.size() == 0) {
945 myMessage = GitBundle.message("push.active.status.no.commits");
946 renderer.append(myMessage, SimpleTextAttributes.GRAYED_BOLD_ATTRIBUTES);
948 else if (root.remoteCommits != 0) {
949 myMessage = GitBundle.message("push.active.status.behind", root.remoteCommits);
950 renderer.append(myMessage, SimpleTextAttributes.ERROR_ATTRIBUTES);
953 myMessage = GitBundle.message("push.active.status.push", root.commits.size());
954 renderer.append(myMessage);
959 public String toString() {
960 return GitBundle.getString("push.active.status.status") + myMessage;
965 * The commit descriptor
967 static class Commit extends Node {
969 GitRevisionNumber revision;
972 boolean isMerge; // true if this commit is a merge commit
975 protected void render(ColoredTreeCellRenderer renderer) {
976 renderer.append(revision.asString().substring(0, HASH_PREFIX_SIZE), SimpleTextAttributes.GRAYED_ATTRIBUTES);
977 renderer.append(": ");
978 renderer.append(message);
980 renderer.append(" " + GitBundle.getString("push.active.commit.node.merge"), SimpleTextAttributes.GRAYED_ATTRIBUTES);
985 * @return the identifier that is supposed to be stable with respect to rebase
988 return authorTime + ":" + message;
992 public String toString() {
993 String mergeCommitStr = isMerge ? " " + GitBundle.getString("push.active.commit.node.merge") : "";
994 return revision.toShortString() + " " + message + mergeCommitStr;
1001 static class Root extends Node {
1004 String currentBranch;
1006 String remoteBranch;
1007 String commitToPush; // The commit that will be actually pushed
1008 List<Commit> commits = new ArrayList<Commit>();
1011 protected void render(ColoredTreeCellRenderer renderer) {
1012 SimpleTextAttributes rootAttributes;
1013 SimpleTextAttributes branchAttributes;
1014 if (remoteName != null && commits.size() != 0 && remoteCommits != 0 || currentBranch == null) {
1015 rootAttributes = SimpleTextAttributes.ERROR_ATTRIBUTES.derive(SimpleTextAttributes.STYLE_BOLD, null, null, null);
1016 branchAttributes = SimpleTextAttributes.ERROR_ATTRIBUTES;
1018 else if (remoteName == null || commits.size() == 0) {
1019 rootAttributes = SimpleTextAttributes.GRAYED_BOLD_ATTRIBUTES;
1020 branchAttributes = SimpleTextAttributes.GRAYED_ATTRIBUTES;
1023 branchAttributes = SimpleTextAttributes.REGULAR_ATTRIBUTES;
1024 rootAttributes = SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES;
1026 renderer.append(root.getPresentableUrl(), rootAttributes);
1027 if (currentBranch != null) {
1028 renderer.append(" [" + currentBranch, branchAttributes);
1029 if (remoteName != null) {
1030 renderer.append(" -> " + remoteName + "#" + remoteBranch, branchAttributes);
1032 renderer.append("]", branchAttributes);
1037 public String toString() {
1038 final StringBuilder string = new StringBuilder();
1039 string.append(root.getPresentableUrl());
1040 if (currentBranch != null) {
1041 string.append(" [").append(currentBranch);
1042 if (remoteName != null) {
1043 string.append(" -> ").append(remoteName).append("#").append(remoteBranch);
1047 return string.toString();