org.community.intellij.plugins.communitycase.checkin.PushActiveBranchesDialog.java Source code

Java tutorial

Introduction

Here is the source code for org.community.intellij.plugins.communitycase.checkin.PushActiveBranchesDialog.java

Source

/*
 * 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 org.community.intellij.plugins.communitycase.checkin;

import com.intellij.notification.Notification;
import com.intellij.notification.NotificationType;
import com.intellij.notification.Notifications;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.history.VcsRevisionNumber;
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.Function;
import com.intellij.util.ui.UIUtil;
import com.intellij.util.ui.tree.TreeUtil;
import org.community.intellij.plugins.communitycase.Branch;
import org.community.intellij.plugins.communitycase.Util;
import org.community.intellij.plugins.communitycase.Vcs;
import org.community.intellij.plugins.communitycase.actions.RepositoryAction;
import org.community.intellij.plugins.communitycase.actions.ShowAllSubmittedFilesAction;
import org.community.intellij.plugins.communitycase.commands.*;
import org.community.intellij.plugins.communitycase.config.VcsSettings;
import org.community.intellij.plugins.communitycase.history.HistoryUtils;
import org.community.intellij.plugins.communitycase.i18n.Bundle;
import org.community.intellij.plugins.communitycase.ui.UiUtil;
import org.community.intellij.plugins.communitycase.update.UpdatePolicyUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreePath;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.*;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;

/**
 * The dialog that allows pushing active branches.
 */
public class PushActiveBranchesDialog extends DialogWrapper {
    private static final int HASH_PREFIX_SIZE = 8; // Amount of digits to show in commit prefix

    private final Project myProject;
    private final List<VirtualFile> myVcsRoots;

    private JPanel myRootPanel;
    private JButton myViewButton; // view commits
    private JButton myFetchButton;
    private JButton myRebaseButton;
    private JButton myPushButton;

    private CheckboxTree myCommitTree; // The commit tree (sorted by vcs roots)
    private CheckedTreeNode myTreeRoot;

    private JRadioButton myStashRadioButton; // Save files policy option
    private JRadioButton myShelveRadioButton;
    private Vcs myVcs;

    /**
     * A modification of Runnable with the roots-parameter.
     * Also for user code simplification myInvokeInAwt variable stores the need of calling run in AWT thread.
     */
    private static abstract class PushActiveBranchRunnable {
        abstract void run(List<Root> roots);
    }

    /**
     * Constructs new dialog. Loads settings, registers listeners.
     * @param project  the project
     * @param vcsRoots the vcs roots
     * @param roots    the loaded information about roots
     */
    private PushActiveBranchesDialog(final Project project, List<VirtualFile> vcsRoots, List<Root> roots) {
        super(project, true);
        myVcs = Vcs.getInstance(project);
        myProject = project;
        myVcsRoots = vcsRoots;

        updateTree(roots, null);
        updateUI();

        myCommitTree.getSelectionModel().addTreeSelectionListener(new TreeSelectionListener() {
            public void valueChanged(TreeSelectionEvent e) {
                TreePath path = myCommitTree.getSelectionModel().getSelectionPath();
                if (path == null) {
                    myViewButton.setEnabled(false);
                    return;
                }
                DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
                myViewButton.setEnabled(node != null && myCommitTree.getSelectionCount() == 1
                        && node.getUserObject() instanceof Commit);
            }
        });
        myViewButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                TreePath path = myCommitTree.getSelectionModel().getSelectionPath();
                if (path == null) {
                    return;
                }
                DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
                if (node == null || !(node.getUserObject() instanceof Commit)) {
                    return;
                }
                Commit c = (Commit) node.getUserObject();
                ShowAllSubmittedFilesAction.showSubmittedFiles(project, c.revision.asString(), c.root.root);
            }
        });
        myFetchButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                fetch();
            }
        });
        myRebaseButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                rebase();
            }
        });

        myPushButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                push();
            }
        });

        setTitle(Bundle.getString("push.active.title"));
        setOKButtonText(Bundle.getString("push.active.rebase.and.push"));
        init();
    }

    /**
     * Show dialog for the project
     */
    public static void showDialogForProject(final Project project) {
        Vcs vcs = Vcs.getInstance(project);
        List<VirtualFile> roots = RepositoryAction.getRoots(project, vcs);
        if (roots == null) {
            return;
        }
        List<VcsException> pushExceptions = new ArrayList<VcsException>();
        showDialog(project, roots, pushExceptions);
        vcs.showErrors(pushExceptions, Bundle.getString("push.active.action.name"));
    }

    /**
     * Show the dialog
     * @param project    the context project
     * @param vcsRoots   the vcs roots in the project
     * @param exceptions the collected exceptions
     */
    public static void showDialog(final Project project, final List<VirtualFile> vcsRoots,
            final Collection<VcsException> exceptions) {
        final List<Root> emptyRoots = loadRoots(project, vcsRoots, exceptions, false); // collect roots without fetching - just to show dialog
        if (!exceptions.isEmpty()) {
            exceptions.addAll(exceptions);
            return;
        }
        final PushActiveBranchesDialog d = new PushActiveBranchesDialog(project, vcsRoots, emptyRoots);
        d.refreshTree(true, null); // start initial fetch
        d.show();
        if (d.isOK()) {
            d.rebaseAndPush();
        }
    }

    /**
     * This is called when "Rebase and Push" button (default button) is pressed.
     * 1. Closes the dialog.
     * 2. Fetches project and rebases.
     * 3. Repeats step 2 if needed - while current repository is behind the parent one.
     * 4. Then pushes.
     * It may fail on one of these steps (especially on rebasing with conflict) - then a notification error will be shown and the process
     * will be interrupted.
     */
    private void rebaseAndPush() {
        final Task.Backgroundable rebaseAndPushTask = new Task.Backgroundable(myProject,
                Bundle.getString("push.active.fetching")) {
            public void run(@NotNull ProgressIndicator indicator) {
                List<VcsException> exceptions = new ArrayList<VcsException>(1);
                do {
                    final RebaseInfo rebaseInfo = collectRebaseInfo();

                    final List<Root> roots = loadRoots(myProject, myVcsRoots, exceptions, true); // fetch
                    if (!exceptions.isEmpty()) {
                        notifyExceptionWhenClosed("Failed to fetch.", exceptions);
                        return;
                    }
                    updateTree(roots, rebaseInfo.uncheckedCommits);

                    executeRebase(exceptions, rebaseInfo);
                    if (!exceptions.isEmpty()) {
                        notifyExceptionWhenClosed("Failed to rebase.", exceptions);
                        return;
                    }
                    Util.refreshFiles(myProject, rebaseInfo.roots);
                } while (isRebaseNeeded());

                final Collection<Root> rootsToPush = getRootsToPush(); // collect roots from the dialog
                exceptions = executePushCommand(rootsToPush);
                if (!exceptions.isEmpty()) {
                    notifyExceptionWhenClosed("Failed to push", exceptions);
                    return;
                }
            }
        };
        myVcs.runInBackground(rebaseAndPushTask);
    }

    /**
     * Notifies about error, when 'rebase and push' task is executed, i.e. when the dialog is closed.
     */
    private void notifyExceptionWhenClosed(String title, Collection<VcsException> exceptions) {
        final String content = StringUtil.join(exceptions, new Function<VcsException, String>() {
            @Override
            public String fun(VcsException e) {
                return e.getLocalizedMessage();
            }
        }, "<br/>");
        Notifications.Bus.notify(
                new Notification(Vcs.NOTIFICATION_GROUP_ID, title, content, NotificationType.ERROR), myProject);
    }

    /**
     * Pushes selected commits synchronously in foreground.
     */
    private void push() {
        final Collection<Root> rootsToPush = getRootsToPush();
        final AtomicReference<Collection<VcsException>> errors = new AtomicReference<Collection<VcsException>>();

        ProgressManager.getInstance().runProcessWithProgressSynchronously(new Runnable() {
            public void run() {
                errors.set(executePushCommand(rootsToPush));
            }
        }, Bundle.getString("push.active.pushing"), true, myProject);
        if (errors.get() != null && !errors.get().isEmpty()) {
            UiUtil.showOperationErrors(myProject, errors.get(), Bundle.getString("push.active.pushing"));
        }
        refreshTree(false, null);
    }

    /**
     * Executes 'git push' for the given roots to push.
     * Returns the list of errors if there were any.
     */
    private List<VcsException> executePushCommand(final Collection<Root> rootsToPush) {
        final ArrayList<VcsException> errors = new ArrayList<VcsException>();
        for (Root r : rootsToPush) {
            LineHandler h = new LineHandler(myProject, r.root, Command.PUSH);
            String src = r.commitToPush != null ? r.commitToPush : r.branch;
            h.addParameters("-v", r.remote, src + ":" + r.remoteBranch);
            PushUtils.trackPushRejectedAsError(h, "Rejected push (" + r.root.getPresentableUrl() + "): ");
            errors.addAll(HandlerUtil.doSynchronouslyWithExceptions(h));
        }
        return errors;
    }

    /**
     * From the dialog collects roots and commits to be pushed.
     * @return roots to be pushed.
     */
    private Collection<Root> getRootsToPush() {
        final ArrayList<Root> rootsToPush = new ArrayList<Root>();
        for (int i = 0; i < myTreeRoot.getChildCount(); i++) {
            CheckedTreeNode node = (CheckedTreeNode) 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;
                }
            }
        }
        return rootsToPush;
    }

    /**
     * Executes when FETCH button is pressed.
     * Fetches repository in background. Then updates the commit tree.
     */
    private void fetch() {
        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;
            }
            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;
                        }
                    }
                }
            }
            if (seenCheckedNode && r.remoteCommits > 0) {
                return true;
            }
        }
        return false;
    }

    /**
     * This is called when rebase is pressed: executes rebase in background.
     */
    private void rebase() {
        final List<VcsException> exceptions = new ArrayList<VcsException>();
        final RebaseInfo rebaseInfo = collectRebaseInfo();

        ProgressManager.getInstance().runProcessWithProgressSynchronously(new Runnable() {
            public void run() {
                executeRebase(exceptions, rebaseInfo);
            }
        }, Bundle.getString("push.active.rebasing"), true, myProject);
        if (!exceptions.isEmpty()) {
            UiUtil.showOperationErrors(myProject, exceptions, "git rebase");
        }
        refreshTree(false, rebaseInfo.uncheckedCommits);
        Util.refreshFiles(myProject, rebaseInfo.roots);
    }

    private void executeRebase(final List<VcsException> exceptions, final RebaseInfo rebaseInfo) {
        PushRebaseProcess process = new PushRebaseProcess(Vcs.getInstance(myProject), myProject, exceptions,
                rebaseInfo.policy, rebaseInfo.reorderedCommits, rebaseInfo.rootsWithMerges);
        process.doUpdate(ProgressManager.getInstance().getProgressIndicator(), rebaseInfo.roots);
    }

    private static class RebaseInfo {
        final Set<VirtualFile> rootsWithMerges;
        private final Map<VirtualFile, Set<String>> uncheckedCommits;
        private final Set<VirtualFile> roots;
        private final VcsSettings.UpdateChangesPolicy policy;
        final Map<VirtualFile, List<String>> reorderedCommits;

        public RebaseInfo(Map<VirtualFile, List<String>> reorderedCommits, Set<VirtualFile> rootsWithMerges,
                Map<VirtualFile, Set<String>> uncheckedCommits, Set<VirtualFile> roots,
                VcsSettings.UpdateChangesPolicy policy) {

            this.reorderedCommits = reorderedCommits;
            this.rootsWithMerges = rootsWithMerges;
            this.uncheckedCommits = uncheckedCommits;
            this.roots = roots;
            this.policy = policy;
        }
    }

    private RebaseInfo collectRebaseInfo() {
        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) {
                if (r.remoteCommits > 0) {
                    roots.add(r.root);
                }
                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 || 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 VcsSettings.UpdateChangesPolicy p = UpdatePolicyUtils.getUpdatePolicy(myStashRadioButton,
                myShelveRadioButton, null);
        assert p == VcsSettings.UpdateChangesPolicy.STASH || p == VcsSettings.UpdateChangesPolicy.SHELVE;

        return new RebaseInfo(reorderedCommits, rootsWithMerges, uncheckedCommits, roots, p);
    }

    /**
     * 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, final Map<VirtualFile, Set<String>> unchecked) {
        myCommitTree.setPaintBusy(true);
        loadRootsInBackground(fetchData, new PushActiveBranchRunnable() {
            @Override
            void run(List<Root> roots) {
                updateTree(roots, unchecked);
                updateUI();
                myCommitTree.setPaintBusy(false);
            }
        });
    }

    /**
     * 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();
        if (roots == null) {
            roots = Collections.emptyList();
        }
        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) {
                CheckedTreeNode child = new CheckedTreeNode(c);
                rootNode.add(child);
                child.setChecked(r.remote != null && !unchecked.contains(c.commitId()));
            }
            myTreeRoot.add(rootNode);
        }
    }

    // Execute from AWT thread.
    private void updateUI() {
        ((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 = Bundle.getString("push.active.error.merges.unchecked");
            }
            if (seenMerges && reorderNeeded) {
                reorderMerges = true;
                error = Bundle.getString("push.active.error.reorder.merges");
            }
            if (reorderNeeded) {
                if (error == null) {
                    error = Bundle.getString("push.active.error.reorder.needed");
                }
            }
            if (r.branch == null) {
                if (error == null) {
                    error = Bundle.getString("push.active.error.no.branch");
                }
                break;
            }
            wasCheckedNode |= r.remoteBranch != null;
            if (r.remoteCommits != 0 && r.commits.size() != 0) {
                if (error == null) {
                    error = Bundle.getString("push.active.error.behind");
                }
                break;
            }
        }
        boolean rebaseNeeded = isRebaseNeeded();
        myPushButton.setEnabled(wasCheckedNode && error == null && !rebaseNeeded);
        setErrorText(error);
        myRebaseButton.setEnabled(rebaseNeeded && !reorderMerges);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected JComponent createCenterPanel() {
        return myRootPanel;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected String getDimensionServiceKey() {
        return getClass().getName();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected String getHelpId() {
        return "reference.VersionControl.Git.PushActiveBranches";
    }

    /**
     * Load VCS roots
     *
     * @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
     */
    private static List<Root> loadRoots(final Project project, final List<VirtualFile> roots,
            final Collection<VcsException> exceptions, final boolean fetchData) {
        final ArrayList<Root> rc = new ArrayList<Root>();
        for (VirtualFile root : roots) {
            try {
                Root r = new Root();
                rc.add(r);
                r.root = root;
                Branch b = Branch.current(project, root);
                if (b != null) {
                    r.branch = b.getFullName();
                    r.remote = b.getTrackedRemoteName(project, root);
                    r.remoteBranch = b.getTrackedBranchName(project, root);
                    if (r.remote != null) {
                        if (fetchData && !r.remote.equals(".")) {
                            LineHandler fetch = new LineHandler(project, root, Command.FETCH);
                            fetch.addParameters(r.remote, "-v");
                            Collection<VcsException> exs = HandlerUtil.doSynchronouslyWithExceptions(fetch);
                            exceptions.addAll(exs);
                        }
                        Branch tracked = b.tracked(project, root);
                        assert tracked != null : "Tracked branch cannot be null here";
                        SimpleHandler unmerged = new SimpleHandler(project, root, Command.LOG);
                        unmerged.addParameters("--pretty=format:%H", r.branch + ".." + tracked.getFullName());
                        unmerged.setRemote(true);
                        unmerged.setStdoutSuppressed(true);
                        StringScanner su = new StringScanner(unmerged.run());
                        while (su.hasMoreData()) {
                            if (su.line().trim().length() != 0) {
                                r.remoteCommits++;
                            }
                        }
                        SimpleHandler toPush = new SimpleHandler(project, root, Command.LOG);
                        toPush.addParameters("--pretty=format:%H%x20%ct%x20%at%x20%s%n%P",
                                tracked.getFullName() + ".." + r.branch);
                        toPush.setRemote(true);
                        toPush.setStdoutSuppressed(true);
                        StringScanner sp = new StringScanner(toPush.run());
                        while (sp.hasMoreData()) {
                            if (sp.isEol()) {
                                sp.line();
                                continue;
                            }
                            Commit c = new Commit();
                            c.root = r;
                            String hash = sp.spaceToken();
                            String time = sp.spaceToken();
                            c.revision = HistoryUtils.createUnvalidatedRevisionNumber(hash);
                            c.authorTime = sp.spaceToken();
                            c.message = sp.line();
                            c.isMerge = sp.line().indexOf(' ') != -1;
                            r.commits.add(c);
                        }
                    }
                }
            } catch (VcsException e) {
                exceptions.add(e);
            }
        }
        return rc;
    }

    /**
     * Loads roots (fetches) in background. When finished, executes the given task in the AWT thread.
     * @param postUiTask
     */
    private void loadRootsInBackground(final boolean fetchData,
            @Nullable final PushActiveBranchRunnable postUiTask) {
        Task.Backgroundable fetchTask = new Task.Backgroundable(myProject,
                Bundle.getString("push.active.fetching")) {
            public void run(@NotNull ProgressIndicator indicator) {
                final Collection<VcsException> exceptions = new HashSet<VcsException>(1);
                final List<Root> roots = loadRoots(myProject, myVcsRoots, exceptions, fetchData);
                if (!exceptions.isEmpty()) {
                    setErrorText(Bundle.getString("push.active.fetch.failed"));
                    return;
                }

                if (postUiTask != null) {
                    ApplicationManager.getApplication().invokeAndWait(new Runnable() {
                        @Override
                        public void run() {
                            postUiTask.run(roots);
                        }
                    }, ModalityState.stateForComponent(getRootPane()));
                }
            }
        };
        myVcs.runInBackground(fetchTask);
    }

    /**
     * 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) {
                // Fix GTK background
                if (UIUtil.isUnderGTKLookAndFeel()) {
                    final Color background = selected ? UIUtil.getTreeSelectionBackground()
                            : UIUtil.getTreeTextBackground();
                    UIUtil.changeBackGround(this, background);
                }
                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 extends Node {
        /**
         * The root
         */
        Root root;

        /**
         * {@inheritDoc}
         */
        @Override
        protected void render(ColoredTreeCellRenderer renderer) {
            renderer.append(Bundle.getString("push.active.status.status"));
            if (root.branch == null) {
                renderer.append(Bundle.message("push.active.status.no.branch"),
                        SimpleTextAttributes.ERROR_ATTRIBUTES);
            } else if (root.remote == null) {
                renderer.append(Bundle.message("push.active.status.no.tracked"),
                        SimpleTextAttributes.GRAYED_BOLD_ATTRIBUTES);
            } else if (root.remoteCommits != 0 && root.commits.size() == 0) {
                renderer.append(Bundle.message("push.active.status.no.commits.behind", root.remoteCommits),
                        SimpleTextAttributes.GRAYED_BOLD_ATTRIBUTES);
            } else if (root.commits.size() == 0) {
                renderer.append(Bundle.message("push.active.status.no.commits"),
                        SimpleTextAttributes.GRAYED_BOLD_ATTRIBUTES);
            } else if (root.remoteCommits != 0) {
                renderer.append(Bundle.message("push.active.status.behind", root.remoteCommits),
                        SimpleTextAttributes.ERROR_ATTRIBUTES);
            } else {
                renderer.append(Bundle.message("push.active.status.push", root.commits.size()));
            }
        }
    }

    /**
     * The commit descriptor
     */
    static class Commit extends Node {
        /**
         * The root
         */
        Root root;
        /**
         * The revision
         */
        VcsRevisionNumber revision;
        /**
         * The message
         */
        String message;
        /**
         * The author time
         */
        String authorTime;
        /**
         * If true, the commit is a merge
         */
        boolean isMerge;

        /**
         * {@inheritDoc}
         */
        @Override
        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(Bundle.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 extends Node {
        /**
         * if true, the update is required
         */
        int remoteCommits;
        /**
         * the path to vcs root
         */
        VirtualFile root;
        /**
         * the current branch
         */
        String branch;
        /**
         * the remote name
         */
        String remote;
        /**
         * the remote branch name
         */
        String remoteBranch;
        /**
         * The commit that will be actually pushed
         */
        String commitToPush;
        /**
         * the commit
         */
        List<Commit> commits = new ArrayList<Commit>();

        /**
         * {@inheritDoc}
         */
        @Override
        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;
            } else if (remote == null || commits.size() == 0) {
                rootAttributes = SimpleTextAttributes.GRAYED_BOLD_ATTRIBUTES;
                branchAttributes = SimpleTextAttributes.GRAYED_ATTRIBUTES;
            } else {
                branchAttributes = SimpleTextAttributes.REGULAR_ATTRIBUTES;
                rootAttributes = SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES;
            }
            renderer.append(root.getPresentableUrl(), rootAttributes);
            if (branch != null) {
                renderer.append(" [" + branch, branchAttributes);
                if (remote != null) {
                    renderer.append(" -> " + remote + "#" + remoteBranch, branchAttributes);
                }
                renderer.append("]", branchAttributes);
            }
        }
    }
}