[push] Improve UX of initially selected repositories to push

Rules:
1. While commits are initially loaded, checkboxes are not shown
   (the "loading" indicator is shown instead),
   and such roots are not ready to be pushed.
2. After outgoing commits from a repository has loaded,
   the checkbox appears back, and it selection is defined by following:
   A. If roots are synced, checkbox is selected iff there are commits
      OR something else to push (e.g. new branch is created).
   B. Otherwise, checkbox is selected iff (there are commits OR
      something else to push) AND this root was preselected from the
      context.
3. Diverged roots behave the same way as not-synced.
4. The Push button is always enabled for 1-repo setup.
   In multi-repo-sync setup, it is disabled until all repositories
   are initialized.
   In multi-repo-async setup, it is disabled until at least one
   repository with commits is loaded.
5.

Additional changes:
* Disable checkbox if target is null.
* Remove "There are no selected repositories to push" validation
  error message since it is obvious.
This commit is contained in:
Kirill Likhodedov
2014-09-23 16:15:00 +04:00
parent cad8e78c3b
commit 937b3b63f7
16 changed files with 247 additions and 137 deletions
@@ -20,4 +20,12 @@ package com.intellij.dvcs.push;
*/
public interface PushTarget {
/**
* Returns true if pushing to this target is guaranteed to introduce something new: e.g. new branch or tag.
* <p/>
* Returning false doesn't mean that this target has nothing to push (e.g. commits to push are calculated separately),
* it means rather that "we don't know".
*/
boolean hasSomethingToPush();
}
@@ -66,4 +66,10 @@ public interface RepositoryManager<T extends Repository> {
void updateAllRepositories();
void waitUntilInitialized();
/**
* Returns true if repositories under this repository manager are controlled synchronously.
*/
boolean isSyncEnabled();
}
@@ -18,19 +18,18 @@ package com.intellij.dvcs.push;
import com.intellij.dvcs.DvcsUtil;
import com.intellij.dvcs.push.ui.*;
import com.intellij.dvcs.repo.Repository;
import com.intellij.dvcs.repo.RepositoryManager;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.ValidationInfo;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.vcs.AbstractVcs;
import com.intellij.ui.CheckedTreeNode;
import com.intellij.util.Function;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.hash.HashMap;
import com.intellij.util.ui.UIUtil;
import com.intellij.vcs.log.VcsFullCommitDetails;
import org.jetbrains.annotations.NotNull;
@@ -45,6 +44,7 @@ import java.util.concurrent.atomic.AtomicReference;
public class PushController implements Disposable {
@NotNull private final Project myProject;
@NotNull private final List<? extends Repository> myPreselectedRepositories;
@NotNull private final List<PushSupport<?,?,?>> myPushSupports;
@NotNull private final PushLog myPushLog;
@NotNull private final VcsPushDialog myDialog;
@@ -59,16 +59,15 @@ public class PushController implements Disposable {
@NotNull VcsPushDialog dialog,
@NotNull List<? extends Repository> preselectedRepositories) {
myProject = project;
myPreselectedRepositories = preselectedRepositories;
myPushSupports = getAffectedSupports(myProject);
mySingleRepoProject = isSingleRepoProject(myPushSupports);
CheckedTreeNode rootNode = new CheckedTreeNode(null);
createTreeModel(rootNode, preselectedRepositories);
createTreeModel(rootNode);
myPushLog = new PushLog(myProject, rootNode);
myDialog = dialog;
myDialog.updateButtons();
startLoadingCommits();
Disposer.register(dialog.getDisposable(), this);
selectFirstChecked();
}
private static boolean isSingleRepoProject(@NotNull List<PushSupport<?,?,?>> pushSupports) {
@@ -107,39 +106,13 @@ public class PushController implements Disposable {
});
}
private void selectFirstChecked() {
Map.Entry<RepositoryNode, MyRepoModel> selected =
ContainerUtil.find(myView2Model.entrySet(), new Condition<Map.Entry<RepositoryNode, MyRepoModel>>() {
@Override
public boolean value(Map.Entry<RepositoryNode, MyRepoModel> entry) {
return entry.getValue().isSelected();
}
});
if (selected != null) {
myPushLog.selectNode(selected.getKey());
}
}
@Nullable
public ValidationInfo validate() {
ValidationInfo validInfo = new ValidationInfo("There are no selected repository to push!");
for (Map.Entry<RepositoryNode, MyRepoModel> entry : myView2Model.entrySet()) {
MyRepoModel model = entry.getValue();
if (model.isSelected()) {
if (model.hasError()) return new ValidationInfo(model.getError().getText());
validInfo = null;
}
}
return validInfo;
}
private void startLoadingCommits() {
//todo should be reworked
Map<RepositoryNode, MyRepoModel> priorityLoading = new HashMap<RepositoryNode, MyRepoModel>();
Map<RepositoryNode, MyRepoModel> others = new HashMap<RepositoryNode, MyRepoModel>();
Map<RepositoryNode, MyRepoModel> priorityLoading = ContainerUtil.newLinkedHashMap();
Map<RepositoryNode, MyRepoModel> others = ContainerUtil.newLinkedHashMap();
for (Map.Entry<RepositoryNode, MyRepoModel> entry : myView2Model.entrySet()) {
MyRepoModel model = entry.getValue();
if (model.isSelected()) {
if (myPreselectedRepositories.contains(model.getRepository())) {
priorityLoading.put(entry.getKey(), model);
}
else if (model.getSupport().shouldRequestIncomingChangesForNotCheckedRepositories()) {
@@ -157,30 +130,27 @@ public class PushController implements Disposable {
}
}
private void createTreeModel(@NotNull CheckedTreeNode rootNode, @NotNull List<? extends Repository> preselectedRepositories) {
private void createTreeModel(@NotNull CheckedTreeNode rootNode) {
for (PushSupport<? extends Repository, ? extends PushSource, ? extends PushTarget> support : myPushSupports) {
createNodesForVcs(support, rootNode, preselectedRepositories);
createNodesForVcs(support, rootNode);
}
}
private <R extends Repository, S extends PushSource, T extends PushTarget> void createNodesForVcs(
@NotNull PushSupport<R, S, T> pushSupport,
@NotNull CheckedTreeNode rootNode,
@NotNull List<? extends Repository> preselectedRepositories)
@NotNull PushSupport<R, S, T> pushSupport, @NotNull CheckedTreeNode rootNode)
{
for (R repository : pushSupport.getRepositoryManager().getRepositories()) {
createRepoNode(pushSupport, repository, rootNode, preselectedRepositories.contains(repository));
createRepoNode(pushSupport, repository, rootNode);
}
}
private <R extends Repository, S extends PushSource, T extends PushTarget> void createRepoNode(@NotNull final PushSupport<R, S, T> support,
@NotNull final R repository,
@NotNull CheckedTreeNode rootNode,
boolean isSelected) {
@NotNull CheckedTreeNode rootNode) {
T target = support.getDefaultTarget(repository);
String repoName = DvcsUtil.getShortRepositoryName(repository);
S source = support.getSource(repository);
final MyRepoModel<R, S, T> model = new MyRepoModel<R, S, T>(repository, support, mySingleRepoProject || isSelected,
final MyRepoModel<R, S, T> model = new MyRepoModel<R, S, T>(repository, support, mySingleRepoProject,
source, target,
DEFAULT_CHILDREN_PRESENTATION_NUMBER);
if (target == null) {
@@ -191,12 +161,13 @@ public class PushController implements Disposable {
source.getPresentation(), pushTargetPanel);
final RepositoryNode repoNode = mySingleRepoProject
? new SingleRepositoryNode(repoPanel)
: new RepositoryNode(repoPanel);
: new RepositoryNode(repoPanel, target != null);
myView2Model.put(repoNode, model);
repoNode.setChecked(model.isSelected());
repoPanel.addRepoNodeListener(new RepositoryNodeListener<T>() {
@Override
public void onTargetChanged(T newTarget) {
model.setSelected(true);
repoNode.setChecked(true);
model.setTarget(newTarget);
model.clearErrors();
loadCommits(model, repoNode, false);
@@ -216,27 +187,78 @@ public class PushController implements Disposable {
rootNode.add(repoNode);
}
public boolean isPushAllowed() {
return ContainerUtil.exists(myPushSupports, new Condition<PushSupport<?, ?, ?>>() {
@Override
public boolean value(PushSupport<?, ?, ?> support) {
return isPushAllowed(support);
}
});
}
private boolean isPushAllowed(@NotNull PushSupport<?, ?, ?> pushSupport) {
Collection<RepositoryNode> nodes = getNodesForSupport(pushSupport);
if (pushSupport.getRepositoryManager().isSyncEnabled()) {
return hasCheckedNode(nodes) && allNodesAreLoaded(nodes);
}
return hasCheckedNode(nodes);
}
private static boolean allNodesAreLoaded(@NotNull Collection<RepositoryNode> nodes) {
return !ContainerUtil.exists(nodes, new Condition<RepositoryNode>() {
@Override
public boolean value(@NotNull RepositoryNode node) {
return node.isLoading();
}
});
}
private static boolean hasCheckedNode(@NotNull Collection<RepositoryNode> nodes) {
return ContainerUtil.exists(nodes, new Condition<RepositoryNode>() {
@Override
public boolean value(@NotNull RepositoryNode node) {
return node.isChecked();
}
});
}
@NotNull
private Collection<RepositoryNode> getNodesForSupport(final PushSupport<?, ?, ?> support) {
return ContainerUtil.mapNotNull(myView2Model.entrySet(), new Function<Map.Entry<RepositoryNode,MyRepoModel>, RepositoryNode>() {
@Override
public RepositoryNode fun(Map.Entry<RepositoryNode, MyRepoModel> entry) {
return entry.getValue().getSupport().equals(support) ? entry.getKey() : null;
}
});
}
private <R extends Repository, S extends PushSource, T extends PushTarget> void loadCommits(@NotNull final MyRepoModel<R, S, T> model,
@NotNull final RepositoryNode node,
final boolean initial) {
node.stopLoading();
node.cancelLoading();
final T target = model.getTarget();
if (target == null) return; //todo should be removed when commit loader executor will be modified
myPushLog.startLoading(node);
if (target == null) {
node.stopLoading();
return;
}
node.setEnabled(true);
final PushSupport<R, S, T> support = model.getSupport();
final AtomicReference<OutgoingResult> result = new AtomicReference<OutgoingResult>();
Runnable task = new Runnable() {
@Override
public void run() {
final R repository = model.getRepository();
OutgoingResult outgoing = support.getOutgoingCommitsProvider()
.getOutgoingCommits(model.getRepository(), new PushSpec<S, T>(model.getSource(), model.getTarget()), initial);
.getOutgoingCommits(repository, new PushSpec<S, T>(model.getSource(), model.getTarget()), initial);
result.compareAndSet(null, outgoing);
UIUtil.invokeAndWaitIfNeeded(new Runnable() {
@Override
public void run() {
OutgoingResult outgoing = result.get();
List<VcsError> errors = outgoing.getErrors();
boolean shouldBeSelected;
if (!errors.isEmpty()) {
shouldBeSelected = false;
myPushLog.setChildren(node, ContainerUtil.map(errors, new Function<VcsError, DefaultMutableTreeNode>() {
@Override
public DefaultMutableTreeNode fun(final VcsError error) {
@@ -256,16 +278,38 @@ public class PushController implements Disposable {
}));
}
else {
model.setLoadedCommits(outgoing.getCommits());
List<? extends VcsFullCommitDetails> commits = outgoing.getCommits();
shouldBeSelected = shouldSelectInitially(target, repository, model.getSupport().getRepositoryManager(), !commits.isEmpty());
model.setLoadedCommits(commits);
myPushLog.setChildren(node,
getPresentationForCommits(PushController.this.myProject, model.getLoadedCommits(),
model.getNumberOfShownCommits()));
}
node.stopLoading();
if (shouldBeSelected) { // never remove selection; initially all checkboxes are not selected
node.setChecked(true);
}
node.fireOnSelectionChange(shouldBeSelected);
}
});
}
};
node.startLoading(myExecutorService.submit(task, result));
node.startLoading(myPushLog.getTree(), myExecutorService.submit(task, result));
}
private boolean shouldSelectInitially(@NotNull PushTarget target, @NotNull Repository repository,
@NotNull RepositoryManager repositoryManager, boolean hasCommits) {
boolean shouldBeSelected;
if (mySingleRepoProject) {
shouldBeSelected = true;
}
else if (repositoryManager.isSyncEnabled()) {
shouldBeSelected = hasCommits || target.hasSomethingToPush();
}
else {
shouldBeSelected = (hasCommits || target.hasSomethingToPush()) && myPreselectedRepositories.contains(repository);
}
return shouldBeSelected;
}
public PushLog getPushPanelLog() {
@@ -18,6 +18,7 @@ package com.intellij.dvcs.push.ui;
import com.intellij.dvcs.push.OutgoingResult;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicReference;
@@ -29,7 +30,7 @@ public interface EditableTreeNode extends CustomRenderedTreeNode {
void fireOnSelectionChange(boolean isSelected);
void stopLoading();
void cancelLoading();
void startLoading(@NotNull Future<AtomicReference<OutgoingResult>> future);
void startLoading(@NotNull JTree tree, @NotNull Future<AtomicReference<OutgoingResult>> future);
}
@@ -20,27 +20,29 @@ import com.intellij.ui.JBColor;
import com.intellij.ui.SimpleTextAttributes;
import com.intellij.util.ImageLoader;
import com.intellij.util.ui.JBImageIcon;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
import javax.swing.tree.DefaultMutableTreeNode;
import java.awt.*;
import java.net.URL;
import java.awt.image.BufferedImage;
public class LoadingTreeNode extends DefaultMutableTreeNode implements CustomRenderedTreeNode {
@NotNull protected ImageIcon myLoadingIcon;
private static final String LOADING_ICON = "/icons/loading.gif";
@NotNull
public ImageIcon getIcon() {
return myLoadingIcon;
}
private static final String LOADING_ICON = "/icons/loading.gif";
private static final JBImageIcon EMPTY_ICON = new JBImageIcon(UIUtil.createImage(18, 18, BufferedImage.TYPE_3BYTE_BGR));
@NotNull private ImageIcon myLoadingIcon;
public LoadingTreeNode() {
super(null, false);
URL loadingIconUrl = getClass().getResource(LOADING_ICON);
Image image = ImageLoader.loadFromUrl(loadingIconUrl);
myLoadingIcon = new JBImageIcon(image);
myLoadingIcon = getLoadingIcon();
}
@NotNull
public static JBImageIcon getLoadingIcon() {
Image image = ImageLoader.loadFromResource(LOADING_ICON);
return image == null ? EMPTY_ICON : new JBImageIcon(image);
}
@Override
@@ -50,4 +52,9 @@ public class LoadingTreeNode extends DefaultMutableTreeNode implements CustomRen
renderer.setIconOnTheRight(false);
renderer.append("Loading Commits...", new SimpleTextAttributes(SimpleTextAttributes.STYLE_SMALLER, JBColor.GRAY));
}
@NotNull
public ImageIcon getIcon() {
return myLoadingIcon;
}
}
@@ -242,13 +242,12 @@ public class PushLog extends JPanel implements TypeSafeDataProvider {
return super.processKeyBinding(ks, e, condition, pressed);
}
public void startLoading(DefaultMutableTreeNode parentNode) {
LoadingTreeNode loading = new LoadingTreeNode();
loading.getIcon().setImageObserver(new NodeImageObserver(myTree, loading));
setChildren(parentNode, Collections.singleton(loading));
public JComponent getPreferredFocusedComponent() {
return myTree;
}
public JComponent getPreferredFocusedComponent() {
@NotNull
public JTree getTree() {
return myTree;
}
@@ -341,9 +340,7 @@ public class PushLog extends JPanel implements TypeSafeDataProvider {
if (node.getChildCount() <= 0) return;
if (node instanceof RepositoryNode) {
TreePath path = TreeUtil.getPathFromRoot(node);
if (((RepositoryNode)node).isChecked()) {
myTree.expandPath(path);
}
myTree.expandPath(path);
return;
}
for (DefaultMutableTreeNode childNode = (DefaultMutableTreeNode)node.getFirstChild();
@@ -21,31 +21,48 @@ import com.intellij.ui.CheckedTreeNode;
import com.intellij.ui.ColoredTreeCellRenderer;
import com.intellij.ui.SimpleTextAttributes;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
public class RepositoryNode extends CheckedTreeNode implements EditableTreeNode, Comparable<RepositoryNode> {
@NotNull private final RepositoryWithBranchPanel myRepositoryPanel;
private Future<AtomicReference<OutgoingResult>> myFuture;
public RepositoryNode(@NotNull RepositoryWithBranchPanel repositoryPanel) {
private static final int CHECKBOX_WIDTH = new JCheckBox().getPreferredSize().width;
@NotNull protected final ImageIcon myLoadingIcon;
@NotNull protected final AtomicBoolean myLoading = new AtomicBoolean(true);
@NotNull private final RepositoryWithBranchPanel myRepositoryPanel;
@Nullable private Future<AtomicReference<OutgoingResult>> myFuture;
public RepositoryNode(@NotNull RepositoryWithBranchPanel repositoryPanel, boolean enabled) {
super(repositoryPanel);
setChecked(false);
setEnabled(enabled);
myRepositoryPanel = repositoryPanel;
myLoadingIcon = LoadingTreeNode.getLoadingIcon();
}
public boolean isCheckboxVisible() {
return true;
return !myLoading.get();
}
@Override
public void render(@NotNull ColoredTreeCellRenderer renderer) {
String repositoryPath = myRepositoryPanel.getRepositoryName();
renderer.append(repositoryPath, SimpleTextAttributes.GRAY_ATTRIBUTES);
renderer.appendFixedTextFragmentWidth(120);
int repoFixedWidth = 120;
if (myLoading.get()) {
renderer.setIcon(myLoadingIcon);
renderer.setIconOnTheRight(false);
renderer.setIconTextGap(CHECKBOX_WIDTH - myLoadingIcon.getIconWidth());
repoFixedWidth += CHECKBOX_WIDTH;
}
renderer.append(myRepositoryPanel.getRepositoryName(), SimpleTextAttributes.GRAY_ATTRIBUTES);
renderer.appendFixedTextFragmentWidth(repoFixedWidth);
renderer.append(myRepositoryPanel.getSourceName(), SimpleTextAttributes.REGULAR_ATTRIBUTES);
renderer.append(myRepositoryPanel.getArrow(), SimpleTextAttributes.REGULAR_ATTRIBUTES);
PushTargetPanel pushTargetPanel = myRepositoryPanel.getTargetPanel();
@@ -75,15 +92,17 @@ public class RepositoryNode extends CheckedTreeNode implements EditableTreeNode,
}
@Override
public void stopLoading() {
public void cancelLoading() {
if (myFuture != null && !myFuture.isDone()) {
myFuture.cancel(true);
}
}
@Override
public void startLoading(@NotNull Future<AtomicReference<OutgoingResult>> future) {
public void startLoading(@NotNull JTree tree, @NotNull Future<AtomicReference<OutgoingResult>> future) {
myFuture = future;
myLoading.set(true);
myLoadingIcon.setImageObserver(new NodeImageObserver(tree, this));
}
public int compareTo(@NotNull RepositoryNode repositoryNode) {
@@ -91,4 +110,13 @@ public class RepositoryNode extends CheckedTreeNode implements EditableTreeNode,
RepositoryWithBranchPanel panel = (RepositoryWithBranchPanel)repositoryNode.getUserObject();
return name.compareTo(panel.getRepositoryName());
}
public void stopLoading() {
myLoading.set(false);
}
public boolean isLoading() {
return myLoading.get();
}
}
@@ -18,7 +18,6 @@ package com.intellij.dvcs.push.ui;
import com.intellij.dvcs.push.PushTargetPanel;
import com.intellij.ui.ColoredTreeCellRenderer;
import com.intellij.ui.SimpleTextAttributes;
import com.intellij.util.ui.EmptyIcon;
import org.jetbrains.annotations.NotNull;
public class SingleRepositoryNode extends RepositoryNode {
@@ -26,7 +25,7 @@ public class SingleRepositoryNode extends RepositoryNode {
@NotNull private final RepositoryWithBranchPanel myRepositoryPanel;
public SingleRepositoryNode(@NotNull RepositoryWithBranchPanel repositoryPanel) {
super(repositoryPanel);
super(repositoryPanel, true);
myRepositoryPanel = repositoryPanel;
}
@@ -37,13 +36,14 @@ public class SingleRepositoryNode extends RepositoryNode {
@Override
public void render(@NotNull ColoredTreeCellRenderer renderer) {
if (myLoading.get()) {
renderer.setIcon(myLoadingIcon);
renderer.setIconOnTheRight(false);
}
renderer.append(myRepositoryPanel.getSourceName(), SimpleTextAttributes.REGULAR_ATTRIBUTES);
renderer.append(myRepositoryPanel.getArrow(), SimpleTextAttributes.REGULAR_ATTRIBUTES);
PushTargetPanel pushTargetPanel = myRepositoryPanel.getTargetPanel();
pushTargetPanel.render(renderer);
// hack to fix vertical size of the editor
renderer.setIcon(EmptyIcon.ICON_18);
renderer.setIconOnTheRight(true);
}
}
@@ -16,13 +16,15 @@
package com.intellij.dvcs.push.ui;
import com.intellij.CommonBundle;
import com.intellij.dvcs.push.*;
import com.intellij.dvcs.push.PushController;
import com.intellij.dvcs.push.PushSupport;
import com.intellij.dvcs.push.VcsPushOptionValue;
import com.intellij.dvcs.push.VcsPushOptionsPanel;
import com.intellij.dvcs.repo.Repository;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.ui.OptionAction;
import com.intellij.openapi.ui.ValidationInfo;
import net.miginfocom.swing.MigLayout;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -54,6 +56,7 @@ public class VcsPushDialog extends DialogWrapper {
myListPanel = myController.getPushPanelLog();
init();
updateButtons();
setOKButtonText("Push");
setOKButtonMnemonic('P');
setTitle("Push Dialog");
@@ -107,27 +110,17 @@ public class VcsPushDialog extends DialogWrapper {
return myPushAction;
}
@Nullable
@Override
protected ValidationInfo doValidate() {
return myController.validate();
}
@Override
protected String getHelpId() {
return "reference.mercurial.push.dialog";
}
public void updateButtons() {
boolean pushAllowed = myController.isPushAllowed();
myPushAction.setEnabled(pushAllowed);
if (myForcePushAction != null) {
myForcePushAction.setEnabled(myController.isForcePushAllowed());
myForcePushAction.setEnabled(pushAllowed && myController.isForcePushAllowed());
}
initValidation();
}
@Override
protected boolean postponeValidation() {
return false;
}
@Nullable
@@ -15,7 +15,6 @@
*/
package git4idea.actions;
import com.intellij.dvcs.branch.DvcsSyncSettings;
import com.intellij.dvcs.push.ui.VcsPushDialog;
import com.intellij.dvcs.repo.RepositoryUtil;
import com.intellij.openapi.actionSystem.AnActionEvent;
@@ -26,55 +25,41 @@ import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.containers.ContainerUtil;
import git4idea.GitUtil;
import git4idea.branch.GitBranchUtil;
import git4idea.config.GitVcsSettings;
import git4idea.repo.GitRepository;
import git4idea.repo.GitRepositoryManager;
import git4idea.ui.branch.GitMultiRootBranchConfig;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
public class GitPushAction extends DumbAwareAction {
@Override
public void actionPerformed(AnActionEvent e) {
public void actionPerformed(@NotNull AnActionEvent e) {
Project project = e.getRequiredData(CommonDataKeys.PROJECT);
Collection<GitRepository> repositories = collectRepositories(project, e.getData(CommonDataKeys.VIRTUAL_FILE_ARRAY));
new VcsPushDialog(project, RepositoryUtil.sortRepositories(repositories)).show();
}
@NotNull
private static Collection<GitRepository> collectRepositories(@NotNull Project project, @Nullable VirtualFile[] files) {
if (files == null) {
return Collections.singletonList(GitBranchUtil.getCurrentRepository(project));
}
GitRepositoryManager manager = GitUtil.getRepositoryManager(project);
Collection<GitRepository> repositories;
if (GitVcsSettings.getInstance(project).getSyncSetting() == DvcsSyncSettings.Value.SYNC && !diverged(manager.getRepositories())) {
repositories = manager.getRepositories();
}
else if (files == null) {
repositories = Collections.singletonList(GitBranchUtil.getCurrentRepository(project));
}
else {
repositories = ContainerUtil.newHashSet();
for (VirtualFile file : files) {
GitRepository repo = manager.getRepositoryForFile(file);
if (repo != null) {
repositories.add(repo);
}
Collection<GitRepository> repositories = ContainerUtil.newHashSet();
for (VirtualFile file : files) {
GitRepository repo = manager.getRepositoryForFile(file);
if (repo != null) {
repositories.add(repo);
}
}
return repositories;
}
private static boolean diverged(List<GitRepository> repositories) {
return new GitMultiRootBranchConfig(repositories).diverged();
}
@Override
public void update(AnActionEvent e) {
public void update(@NotNull AnActionEvent e) {
super.update(e);
Project project = e.getProject();
e.getPresentation().setEnabledAndVisible(project != null && !GitUtil.getRepositoryManager(project).getRepositories().isEmpty());
@@ -49,13 +49,22 @@ class GitPushTarget implements PushTarget {
return myRemoteBranch;
}
@Override
public boolean hasSomethingToPush() {
return isNewBranchCreated();
}
boolean isNewBranchCreated() {
return myIsNewBranchCreated;
}
@NotNull
static GitPushTarget parse(@NotNull GitRepository repository, @NotNull String remoteName, @NotNull String branchName) throws
static GitPushTarget parse(@NotNull GitRepository repository, @Nullable String remoteName, @NotNull String branchName) throws
ParseException {
if (remoteName == null) {
throw new ParseException("No remotes defined", -1);
}
if (!GitRefNameValidator.getInstance().checkInput(branchName)) {
throw new ParseException("Invalid destination branch name: " + branchName, -1);
}
@@ -52,6 +52,7 @@ class GitPushTargetPanel extends PushTargetPanel<GitPushTarget> {
private static final Comparator<GitRemoteBranch> REMOTE_BRANCH_COMPARATOR = new MyRemoteBranchComparator();
public static final String SEPARATOR = " \u25BE ";
public static final String NO_REMOTES = "No remotes" + SEPARATOR;
private final GitRepository myRepository;
private final PushTargetTextField myTargetTextField;
@@ -69,7 +70,7 @@ class GitPushTargetPanel extends PushTargetPanel<GitPushTarget> {
if (defaultTarget == null) {
// TODO
initialBranch = "";
initialRemote = "No remotes";
initialRemote = NO_REMOTES;
}
else {
initialBranch = getTextFieldText(defaultTarget);
@@ -150,7 +151,7 @@ class GitPushTargetPanel extends PushTargetPanel<GitPushTarget> {
@Override
public void fireOnChange() {
String remoteName = myRemoteLabel.getText().replace(SEPARATOR, "");
String remoteName = getEnteredRemote();
String branchName = myTargetTextField.getText();
try {
myCurrentTarget = GitPushTarget.parse(myRepository, remoteName, branchName);
@@ -164,7 +165,7 @@ class GitPushTargetPanel extends PushTargetPanel<GitPushTarget> {
@Override
public ValidationInfo verify() {
try {
String remoteLabel = myRemoteLabel.getText().replace(SEPARATOR, "");
String remoteLabel = getEnteredRemote();
GitPushTarget.parse(myRepository, remoteLabel, myTargetTextField.getText());
return null;
}
@@ -173,6 +174,12 @@ class GitPushTargetPanel extends PushTargetPanel<GitPushTarget> {
}
}
@Nullable
private String getEnteredRemote() {
String text = myRemoteLabel.getText();
return text.equals(NO_REMOTES) ? null : text.replace(SEPARATOR, "");
}
@NotNull
public static List<String> getTargetNames(@NotNull GitRepository repository) {
List<GitRemoteBranch> remoteBranches = ContainerUtil.sorted(repository.getBranches().getRemoteBranches(), REMOTE_BRANCH_COMPARATOR);
@@ -15,17 +15,16 @@
*/
package git4idea.repo;
import com.intellij.dvcs.branch.DvcsSyncSettings;
import com.intellij.dvcs.repo.AbstractRepositoryManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vcs.ProjectLevelVcsManager;
import com.intellij.openapi.vfs.VirtualFile;
import git4idea.GitPlatformFacade;
import git4idea.GitUtil;
import git4idea.ui.branch.GitMultiRootBranchConfig;
import org.jetbrains.annotations.NotNull;
/**
* @author Kirill Likhodedov
*/
public class GitRepositoryManager extends AbstractRepositoryManager<GitRepository> {
@NotNull private final GitPlatformFacade myPlatformFacade;
@@ -41,5 +40,11 @@ public class GitRepositoryManager extends AbstractRepositoryManager<GitRepositor
protected GitRepository createRepository(@NotNull VirtualFile root) {
return GitRepositoryImpl.getFullInstance(root, myProject, myPlatformFacade, this);
}
@Override
public boolean isSyncEnabled() {
return myPlatformFacade.getSettings(myProject).getSyncSetting() == DvcsSyncSettings.Value.SYNC &&
!new GitMultiRootBranchConfig(getRepositories()).diverged();
}
}
@@ -25,9 +25,6 @@ import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
/**
* @author Kirill Likhodedov
*/
public class GitMockRepositoryManager implements RepositoryManager<GitRepository> {
private final List<GitRepository> myRepositories = new ArrayList<GitRepository>();
@@ -81,4 +78,8 @@ public class GitMockRepositoryManager implements RepositoryManager<GitRepository
public void waitUntilInitialized() {
}
@Override
public boolean isSyncEnabled() {
return false;
}
}
@@ -30,4 +30,10 @@ public class HgTarget implements PushTarget {
public String getPresentation() {
return HgUtil.removePasswordIfNeeded(myTarget);
}
@Override
public boolean hasSomethingToPush() {
return false;
}
}
@@ -1,11 +1,15 @@
package org.zmlx.hg4idea.repo;
import com.intellij.dvcs.branch.DvcsSyncSettings;
import com.intellij.dvcs.repo.AbstractRepositoryManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vcs.ProjectLevelVcsManager;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.ObjectUtils;
import org.jetbrains.annotations.NotNull;
import org.zmlx.hg4idea.HgProjectSettings;
import org.zmlx.hg4idea.HgVcs;
import org.zmlx.hg4idea.branch.HgMultiRootBranchConfig;
import org.zmlx.hg4idea.util.HgUtil;
/**
@@ -13,9 +17,12 @@ import org.zmlx.hg4idea.util.HgUtil;
*/
public class HgRepositoryManager extends AbstractRepositoryManager<HgRepository> {
private final HgProjectSettings mySettings;
public HgRepositoryManager(@NotNull Project project,
@NotNull ProjectLevelVcsManager vcsManager) {
super(project, vcsManager, HgVcs.getInstance(project), HgUtil.DOT_HG);
mySettings = ObjectUtils.assertNotNull(HgVcs.getInstance(project)).getProjectSettings();
}
@NotNull
@@ -23,4 +30,10 @@ public class HgRepositoryManager extends AbstractRepositoryManager<HgRepository>
protected HgRepository createRepository(@NotNull VirtualFile root) {
return HgRepositoryImpl.getInstance(root, myProject, this);
}
@Override
public boolean isSyncEnabled() {
return mySettings.getSyncSetting() == DvcsSyncSettings.Value.SYNC && !new HgMultiRootBranchConfig(getRepositories()).diverged();
}
}