IDEA-68526 show progress when pushing active branches
[idea/community.git] / plugins / git4idea / src / git4idea / checkin / GitPushActiveBranchesDialog.java
1 /*
2  * Copyright 2000-2009 JetBrains s.r.o.
3  *
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
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16 package git4idea.checkin;
17
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;
56
57 import javax.swing.*;
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;
65 import java.awt.*;
66 import java.awt.event.ActionEvent;
67 import java.awt.event.ActionListener;
68 import java.util.*;
69 import java.util.List;
70 import java.util.concurrent.atomic.AtomicReference;
71
72 import static git4idea.ui.GitUIUtil.notifyError;
73 import static git4idea.ui.GitUIUtil.notifyMessage;
74
75 /**
76  * The dialog that allows pushing active branches.
77  */
78 public class GitPushActiveBranchesDialog extends DialogWrapper {
79   private static final int HASH_PREFIX_SIZE = 8; // Amount of digits to show in commit prefix
80
81   private final Project myProject;
82   private final List<VirtualFile> myVcsRoots;
83
84   private JPanel myRootPanel;
85   private JButton myViewButton; // view commits
86   private JButton myFetchButton;
87   private JButton myRebaseButton;
88   private JButton myPushButton;
89
90   private CheckboxTree myCommitTree; // The commit tree (sorted by vcs roots)
91   private CheckedTreeNode myTreeRoot;
92
93   private JRadioButton myStashRadioButton; // Save files policy option
94   private JRadioButton myShelveRadioButton;
95   private GitVcs myVcs;
96   private static final Logger LOG = Logger.getInstance(GitPushActiveBranchesDialog.class.getName());
97   private final GeneralSettings myGeneralSettings;
98   private final ProjectManagerEx myProjectManager;
99
100   /**
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.
103    */
104   private static abstract class PushActiveBranchRunnable {
105     abstract void run(List<Root> roots);
106   }
107
108   /**
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
113    */
114   private GitPushActiveBranchesDialog(final Project project, List<VirtualFile> vcsRoots, List<Root> roots) {
115     super(project, true);
116     myVcs = GitVcs.getInstance(project);
117     myProject = project;
118     myVcsRoots = vcsRoots;
119     myGeneralSettings = GeneralSettings.getInstance();
120     myProjectManager = ProjectManagerEx.getInstanceEx();
121
122     updateTree(roots, null);
123     updateUI();
124
125     final GitVcsSettings settings = GitVcsSettings.getInstance(project);
126     if (settings != null) {
127       UpdatePolicyUtils.updatePolicyItem(settings.getPushActiveBranchesRebaseSavePolicy(), myStashRadioButton, myShelveRadioButton);
128     }
129     ChangeListener listener = new ChangeListener() {
130       public void stateChanged(ChangeEvent e) {
131         if (settings != null) {
132           settings.setPushActiveBranchesRebaseSavePolicy(UpdatePolicyUtils.getUpdatePolicy(myStashRadioButton, myShelveRadioButton));
133         }
134       }
135     };
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();
141         if (path == null) {
142           myViewButton.setEnabled(false);
143           return;
144         }
145         DefaultMutableTreeNode node = (DefaultMutableTreeNode)path.getLastPathComponent();
146         myViewButton.setEnabled(node != null && myCommitTree.getSelectionCount() == 1 && node.getUserObject() instanceof Commit);
147       }
148     });
149     myViewButton.addActionListener(new ActionListener() {
150       public void actionPerformed(ActionEvent e) {
151         TreePath path = myCommitTree.getSelectionModel().getSelectionPath();
152         if (path == null) {
153           return;
154         }
155         DefaultMutableTreeNode node = (DefaultMutableTreeNode)path.getLastPathComponent();
156         if (node == null || !(node.getUserObject() instanceof Commit)) {
157           return;
158         }
159         Commit c = (Commit)node.getUserObject();
160         GitShowAllSubmittedFilesAction.showSubmittedFiles(project, c.revision.asString(), c.root.root);
161       }
162     });
163     myFetchButton.addActionListener(new ActionListener() {
164       public void actionPerformed(ActionEvent e) {
165         fetch();
166       }
167     });
168     myRebaseButton.addActionListener(new ActionListener() {
169       public void actionPerformed(ActionEvent e) {
170         rebase();
171       }
172     });
173
174     myPushButton.addActionListener(new ActionListener() {
175       @Override public void actionPerformed(ActionEvent e) {
176         push();
177       }
178     });
179
180     setTitle(GitBundle.getString("push.active.title"));
181     setOKButtonText(GitBundle.getString("push.active.rebase.and.push"));
182     setCancelButtonText(GitBundle.getString("git.push.active.close"));
183     init();
184   }
185
186   /**
187    * Show dialog for the project
188    */
189   public static void showDialogForProject(final Project project) {
190     GitVcs vcs = GitVcs.getInstance(project);
191     List<VirtualFile> roots = GitRepositoryAction.getGitRoots(project, vcs);
192     if (roots == null) {
193       return;
194     }
195     List<VcsException> pushExceptions = new ArrayList<VcsException>();
196     showDialog(project, roots, pushExceptions);
197     vcs.showErrors(pushExceptions, GitBundle.getString("push.active.action.name"));
198   }
199
200   /**
201    * Show the dialog
202    * @param project    the context project
203    * @param vcsRoots   the vcs roots in the project
204    * @param exceptions the collected exceptions
205    */
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);
210       return;
211     }
212     final GitPushActiveBranchesDialog d = new GitPushActiveBranchesDialog(project, vcsRoots, emptyRoots);
213     d.refreshTree(true, null, false); // start initial fetch
214     d.show();
215     if (d.isOK()) {
216       d.rebaseAndPush();
217     }
218   }
219
220   /**
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.
225    * 4. Then pushes.
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.
228    */
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();
236
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
241               int commitsNum = 0;
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();
247                 }
248               }
249               GitUIUtil.notifySuccess(myProject, "Pushed successfully",
250                                       "Pushed " + commitsNum + " " + StringUtil.pluralize("commit", commitsNum) + ".");
251               return;
252             }
253             pushExceptions = new ArrayList<VcsException>(exceptions);
254             exceptions.clear();
255           }
256
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);
260             return;
261           }
262           updateTree(roots, rebaseInfo.uncheckedCommits);
263
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);
269               return;
270             }
271             GitUtil.refreshFiles(myProject, rebaseInfo.roots);
272           }
273         }
274         notifyMessage(myProject, "Failed to push", "Update project and push again", NotificationType.ERROR, true, pushExceptions);
275       }
276     };
277     GitVcs.runInBackground(rebaseAndPushTask);
278   }
279
280   /**
281    * Pushes selected commits synchronously in foreground.
282    */
283   private void push() {
284     final Collection<Root> rootsToPush = getRootsToPush();
285     final AtomicReference<Collection<VcsException>> errors = new AtomicReference<Collection<VcsException>>();
286
287     ProgressManager.getInstance().runProcessWithProgressSynchronously(new Runnable() {
288       public void run() {
289         errors.set(executePushCommand(rootsToPush));
290       }
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"));
294     }
295     refreshTree(false, null);
296   }
297
298   /**
299    * Executes 'git push' for the given roots to push.
300    * Returns the list of errors if there were any.
301    */
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));
310     }
311     return errors;
312   }
313
314   /**
315    * From the dialog collects roots and commits to be pushed.
316    * @return roots to be pushed.
317    */
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) {
324         continue;
325       }
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();
332             if (!topCommit) {
333               r.commitToPush = commit.revision.asString();
334             }
335             rootsToPush.add(r);
336             break;
337           }
338           topCommit = false;
339         }
340       }
341     }
342     return rootsToPush;
343   }
344
345   /**
346    * Executes when FETCH button is pressed.
347    * Fetches repository in background. Then updates the commit tree.
348    */
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());
360           }
361         }
362       }
363       if (!uncheckedCommits.isEmpty()) {
364         unchecked.put(r.root, uncheckedCommits);
365       }
366     }
367     refreshTree(true, unchecked);
368   }
369
370   /**
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
373    */
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) {
379         continue;
380       }
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;
387           }
388           else {
389             if (seenCheckedNode) {
390               return true;
391             }
392           }
393         }
394       }
395       if (seenCheckedNode && r.remoteCommits > 0) {
396         return true;
397       }
398     }
399     return false;
400   }
401
402   /**
403    * This is called when rebase is pressed: executes rebase in background.
404    */
405   private void rebase() {
406     final List<VcsException> exceptions = new ArrayList<VcsException>();
407     final RebaseInfo rebaseInfo = collectRebaseInfo();
408
409     ProgressManager.getInstance().runProcessWithProgressSynchronously(new Runnable() {
410       public void run() {
411         executeRebase(exceptions, rebaseInfo);
412       }
413     }, GitBundle.getString("push.active.rebasing"), true, myProject);
414     if (!exceptions.isEmpty()) {
415       GitUIUtil.showOperationErrors(myProject, exceptions, "git rebase");
416     }
417     refreshTree(false, rebaseInfo.uncheckedCommits);
418     GitUtil.refreshFiles(myProject, rebaseInfo.roots);
419   }
420
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);
425
426     // then we reorder commits
427     if (result) {
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);
433     } else {
434       notifyMessage(myProject, "Commits weren't pushed", "Rebase failed.", NotificationType.WARNING, true, null);
435       return false;
436     }
437
438   }
439
440   private boolean reorderCommitsIfNeeded(@NotNull final RebaseInfo rebaseInfo) {
441     if (rebaseInfo.reorderedCommits.isEmpty()) {
442       return true;
443     }
444
445     ProgressIndicator progressIndicator = ProgressManager.getInstance().getProgressIndicator();
446     if (progressIndicator == null) {
447       progressIndicator = new EmptyProgressIndicator();
448     }
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);
451
452     final Boolean[] result = new Boolean[1];
453     result[0] = false;
454     new GitUpdateLikeProcess(myProject) {
455       @Override
456       protected void runImpl(ContinuationContext context) {
457         try {
458           final Set<VirtualFile> rootsToReorder = rebaseInfo.reorderedCommits.keySet();
459           saver.saveLocalChanges(rootsToReorder);
460
461           try {
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);
466               if (b == null) {
467                 LOG.info("executeRebase: current branch is null");
468                 continue;
469               }
470               GitBranch t = b.tracked(myProject, root);
471               if (t == null) {
472                 LOG.info("executeRebase: tracked branch is null");
473                 continue;
474               }
475
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);
479                 continue;
480               }
481
482               String parentCommit = mergeBase.getRev();
483               result[0] = rebaser.reoderCommitsIfNeeded(root, parentCommit, rootToCommits.getValue());
484             }
485
486           } catch (VcsException e) {
487             notifyMessage(myProject, "Commits weren't pushed", "Failed to reorder commits", NotificationType.WARNING, true,
488                           Collections.singleton(e));
489           } finally {
490             saver.restoreLocalChanges(context);
491           }
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);
497         }
498       }
499     }.execute();
500     return result[0];
501   }
502
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;
509
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) {
514
515       this.reorderedCommits = reorderedCommits;
516       this.rootsWithMerges = rootsWithMerges;
517       this.uncheckedCommits = uncheckedCommits;
518       this.roots = roots;
519       this.policy = policy;
520     }
521   }
522
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) {
535           roots.add(r.root);
536         }
537         continue;
538       }
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;
549           }
550           else {
551             unchecked.add(commit.commitId());
552             if (seenCheckedNode) {
553               reorderNeeded = true;
554             }
555           }
556         }
557       }
558       if (seenMerges) {
559         rootsWithMerges.add(r.root);
560       }
561       if (r.remoteCommits > 0 || reorderNeeded) {
562         roots.add(r.root);
563       }
564       if (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());
572             }
573           }
574         }
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());
581             }
582           }
583         }
584         Collections.reverse(reordered);
585         reorderedCommits.put(r.root, reordered);
586       }
587     }
588     final GitVcsSettings.UpdateChangesPolicy p = UpdatePolicyUtils.getUpdatePolicy(myStashRadioButton, myShelveRadioButton);
589     assert p == GitVcsSettings.UpdateChangesPolicy.STASH || p == GitVcsSettings.UpdateChangesPolicy.SHELVE;
590
591     return new RebaseInfo(reorderedCommits, rootsWithMerges, uncheckedCommits, roots, p);
592   }
593
594   /**
595    * Refresh tree
596    *
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.
602    */
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(){
606       @Override
607       void run(List<Root> roots) {
608         Map<VirtualFile, Set<String>> uncheckedCommits;
609         if (!updateCommits) {
610           RebaseInfo info = collectRebaseInfo();
611           uncheckedCommits = info.uncheckedCommits;
612         } else {
613           uncheckedCommits = unchecked;
614         }
615         updateTree(roots, uncheckedCommits);
616         updateUI();
617         myCommitTree.setPaintBusy(false);
618       }
619     });
620   }
621
622   private void refreshTree(boolean fetchData, Map<VirtualFile, Set<String>> unchecked) {
623     refreshTree(fetchData, unchecked, true);
624   }
625
626   /**
627    * Update the tree according to the list of loaded roots
628    *
629    *
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
632    */
633   private void updateTree(List<Root> roots, Map<VirtualFile, Set<String>> uncheckedCommits) {
634     myTreeRoot.removeAllChildren();
635     if (roots == null) {
636       roots = Collections.emptyList();
637     }
638     for (Root r : roots) {
639       CheckedTreeNode rootNode = new CheckedTreeNode(r);
640       Status status = new Status();
641       status.root = r;
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);
647         rootNode.add(child);
648         child.setChecked(r.remoteName != null && !unchecked.contains(c.commitId()));
649       }
650       myTreeRoot.add(rootNode);
651     }
652   }
653
654   // Execute from AWT thread.
655   private void updateUI() {
656     ((DefaultTreeModel)myCommitTree.getModel()).reload(myTreeRoot);
657     TreeUtil.expandAll(myCommitTree);
658     updateButtons();
659   }
660
661   /**
662    * Update buttons on the form
663    */
664   private void updateButtons() {
665     String error = null;
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;
681           }
682           else {
683             seenUnchecked = true;
684             if (seenCheckedNode) {
685               reorderNeeded = true;
686             }
687           }
688         }
689       }
690       if (!seenCheckedNode) {
691         continue;
692       }
693       Root r = (Root)node.getUserObject();
694       if (seenMerges && seenUnchecked) {
695         error = GitBundle.getString("push.active.error.merges.unchecked");
696       }
697       if (seenMerges && reorderNeeded) {
698         reorderMerges = true;
699         error = GitBundle.getString("push.active.error.reorder.merges");
700       }
701       if (reorderNeeded) {
702         if (error == null) {
703           error = GitBundle.getString("push.active.error.reorder.needed");
704         }
705       }
706       if (r.currentBranch == null) {
707         if (error == null) {
708           error = GitBundle.getString("push.active.error.no.branch");
709         }
710         break;
711       }
712       wasCheckedNode |= r.remoteBranch != null;
713       if (r.remoteCommits != 0 && r.commits.size() != 0) {
714         if (error == null) {
715           error = GitBundle.getString("push.active.error.behind");
716         }
717         break;
718       }
719     }
720     boolean rebaseNeeded = isRebaseNeeded();
721     myPushButton.setEnabled(wasCheckedNode && error == null && !rebaseNeeded);
722     setErrorText(error);
723     myRebaseButton.setEnabled(rebaseNeeded && !reorderMerges);
724     setOKActionEnabled(myPushButton.isEnabled() || myRebaseButton.isEnabled());
725   }
726
727   /**
728    * {@inheritDoc}
729    */
730   @Override
731   protected JComponent createCenterPanel() {
732     return myRootPanel;
733   }
734
735   /**
736    * {@inheritDoc}
737    */
738   @Override
739   protected String getDimensionServiceKey() {
740     return getClass().getName();
741   }
742
743   /**
744    * {@inheritDoc}
745    */
746   @Override
747   protected String getHelpId() {
748     return "reference.VersionControl.Git.PushActiveBranches";
749   }
750
751   /**
752    * Load VCS roots
753    *
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
759    */
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) {
766       try {
767         Root r = new Root();
768         rc.add(r);
769         r.root = root;
770         GitBranch b = GitBranch.current(project, root);
771         if (b != null) {
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);
781             }
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");
787             }
788
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) {
798                   r.remoteCommits++;
799                 }
800               }
801             }
802
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()) {
812               if (sp.isEol()) {
813                 sp.line();
814                 continue;
815               }
816               Commit c = new Commit();
817               c.root = r;
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;
824               r.commits.add(c);
825             }
826           }
827         }
828       }
829       catch (VcsException e) {
830         exceptions.add(e);
831       }
832     }
833     return rc;
834   }
835
836   /**
837    * Loads roots (fetches) in background. When finished, executes the given task in the AWT thread.
838    * @param postUiTask
839    */
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"));
847           return;
848         }
849
850         if (postUiTask != null) {
851           UIUtil.invokeAndWaitIfNeeded(new Runnable() {
852             @Override
853             public void run() {
854               postUiTask.run(roots);
855             }
856           });
857         }
858       }
859     };
860     UIUtil.invokeLaterIfNeeded(new Runnable() {
861       @Override public void run() {
862         final ModalityState modalityState = ModalityState.stateForComponent(getContentPane());
863         myVcs.getTaskQueue().run(fetchTask, modalityState, null);
864       }
865     });
866   }
867
868   /**
869    * Create UI components for the dialog
870    */
871   private void createUIComponents() {
872     myTreeRoot = new CheckedTreeNode("ROOT");
873     myCommitTree = new CheckboxTree(new CheckboxTree.CheckboxTreeCellRenderer() {
874       @Override
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);
880         }
881         ColoredTreeCellRenderer r = getTextRenderer();
882         if (!(value instanceof DefaultMutableTreeNode)) {
883           // unknown node type
884           renderUnknown(r, value);
885           return;
886         }
887         DefaultMutableTreeNode node = (DefaultMutableTreeNode)value;
888         if (!(node.getUserObject() instanceof Node)) {
889           // unknown node type
890           renderUnknown(r, node.getUserObject());
891           return;
892         }
893         ((Node)node.getUserObject()).render(r);
894       }
895
896       /**
897        * Render unknown node
898        *
899        * @param r     a renderer to use
900        * @param value the unknown value
901        */
902       private void renderUnknown(ColoredTreeCellRenderer r, Object value) {
903         r.append("UNSUPPORTED NODE TYPE: " + (value == null ? "null" : value.getClass().getName()), SimpleTextAttributes.ERROR_ATTRIBUTES);
904       }
905     }, myTreeRoot) {
906       @Override
907       protected void onNodeStateChanged(CheckedTreeNode node) {
908         updateButtons();
909         super.onNodeStateChanged(node);
910       }
911     };
912   }
913
914
915   /**
916    * The base class for nodes in the tree
917    */
918   static abstract class Node {
919     /**
920      * Render the node text
921      */
922     protected abstract void render(ColoredTreeCellRenderer renderer);
923   }
924
925   static class Status extends Node {
926     Root root;
927     private String myMessage = "";
928
929     @Override
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);
935       }
936       else if (root.remoteName == null) {
937         myMessage = GitBundle.message("push.active.status.no.tracked");
938         renderer.append(myMessage, SimpleTextAttributes.GRAYED_BOLD_ATTRIBUTES);
939       }
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);
943       }
944       else if (root.commits.size() == 0) {
945         myMessage = GitBundle.message("push.active.status.no.commits");
946         renderer.append(myMessage, SimpleTextAttributes.GRAYED_BOLD_ATTRIBUTES);
947       }
948       else if (root.remoteCommits != 0) {
949         myMessage = GitBundle.message("push.active.status.behind", root.remoteCommits);
950         renderer.append(myMessage, SimpleTextAttributes.ERROR_ATTRIBUTES);
951       }
952       else {
953         myMessage = GitBundle.message("push.active.status.push", root.commits.size());
954         renderer.append(myMessage);
955       }
956     }
957
958     @Override
959     public String toString() {
960       return GitBundle.getString("push.active.status.status") + myMessage;
961     }
962   }
963
964   /**
965    * The commit descriptor
966    */
967   static class Commit extends Node {
968     Root root;
969     GitRevisionNumber revision;
970     String message;
971     String authorTime;
972     boolean isMerge; // true if this commit is a merge commit
973
974     @Override
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);
979       if (isMerge) {
980         renderer.append(" " + GitBundle.getString("push.active.commit.node.merge"), SimpleTextAttributes.GRAYED_ATTRIBUTES);
981       }
982     }
983
984     /**
985      * @return the identifier that is supposed to be stable with respect to rebase
986      */
987     String commitId() {
988       return authorTime + ":" + message;
989     }
990
991     @Override
992     public String toString() {
993       String mergeCommitStr = isMerge ? " " + GitBundle.getString("push.active.commit.node.merge") : "";
994       return revision.toShortString() + " " + message + mergeCommitStr;
995     }
996   }
997
998   /**
999    * The root node
1000    */
1001   static class Root extends Node {
1002     int remoteCommits;
1003     VirtualFile root;
1004     String currentBranch;
1005     String remoteName;
1006     String remoteBranch;
1007     String commitToPush; // The commit that will be actually pushed
1008     List<Commit> commits = new ArrayList<Commit>();
1009
1010     @Override
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;
1017       }
1018       else if (remoteName == null || commits.size() == 0) {
1019         rootAttributes = SimpleTextAttributes.GRAYED_BOLD_ATTRIBUTES;
1020         branchAttributes = SimpleTextAttributes.GRAYED_ATTRIBUTES;
1021       }
1022       else {
1023         branchAttributes = SimpleTextAttributes.REGULAR_ATTRIBUTES;
1024         rootAttributes = SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES;
1025       }
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);
1031         }
1032         renderer.append("]", branchAttributes);
1033       }
1034     }
1035
1036     @Override
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);
1044         }
1045         string.append("]");
1046       }
1047       return string.toString();
1048     }
1049   }
1050 }