[git] Rewrite Git push procedure & use new VcsPushDialog

This commit is contained in:
Kirill Likhodedov
2014-09-23 16:14:50 +04:00
parent 3374608b7b
commit 98d5287aad
38 changed files with 2564 additions and 2651 deletions
@@ -25,13 +25,13 @@ import org.jetbrains.annotations.Nullable;
public class VcsNotifier {
private static final NotificationGroup NOTIFICATION_GROUP_ID = NotificationGroup.toolWindowGroup(
public static final NotificationGroup NOTIFICATION_GROUP_ID = NotificationGroup.toolWindowGroup(
"Vcs Messages", ChangesViewContentManager.TOOLWINDOW_ID);
private static final NotificationGroup IMPORTANT_ERROR_NOTIFICATION = new NotificationGroup(
public static final NotificationGroup IMPORTANT_ERROR_NOTIFICATION = new NotificationGroup(
"Vcs Important Messages", NotificationDisplayType.STICKY_BALLOON, true);
private static final NotificationGroup MINOR_NOTIFICATION = new NotificationGroup(
public static final NotificationGroup MINOR_NOTIFICATION = new NotificationGroup(
"Vcs Minor Notifications", NotificationDisplayType.BALLOON, true);
private static final NotificationGroup SILENT_NOTIFICATION = new NotificationGroup(
public static final NotificationGroup SILENT_NOTIFICATION = new NotificationGroup(
"Vcs Silent Notifications", NotificationDisplayType.NONE, true);
private final @NotNull Project myProject;
@@ -127,6 +127,10 @@ public class Executor {
FileUtil.writeToFile(file, content.getBytes(), true);
}
public static void append(@NotNull String fileName, @NotNull String content) throws IOException {
append(child(fileName), content);
}
@NotNull
public static File mkdir(@NotNull String dirName) {
File file = child(dirName);
+1
View File
@@ -141,6 +141,7 @@
<checkinHandlerFactory implementation="git4idea.checkin.UnresolvedMergeCheckFactory"/>
<logProvider implementation="git4idea.log.GitLogProvider"/>
<vcsChangesViewRefresher implementation="git4idea.changes.GitChangesViewRefresher" />
<pushSupport implementation="git4idea.push.GitPushSupport"/>
<projectService
serviceInterface="git4idea.history.wholeTree.LogFactoryService"
@@ -15,33 +15,65 @@
*/
package git4idea.actions;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.dvcs.branch.DvcsBranchSync;
import com.intellij.dvcs.push.ui.VcsPushDialog;
import com.intellij.dvcs.repo.RepositoryUtil;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.project.DumbAwareAction;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vfs.VirtualFile;
import git4idea.GitPlatformFacade;
import git4idea.push.GitPusher;
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 org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Set;
import java.util.Collection;
import java.util.Collections;
/**
* The action that pushes active branches
*/
public class GitPushAction extends GitRepositoryAction {
public class GitPushAction extends DumbAwareAction {
@Override
@NotNull
protected String getActionName() {
return "Push";
public void actionPerformed(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();
}
protected void perform(@NotNull final Project project,
@NotNull final List<VirtualFile> gitRoots,
@NotNull final VirtualFile defaultRoot,
final Set<VirtualFile> affectedRoots,
final List<VcsException> exceptions) throws VcsException {
GitPusher.showPushDialogAndPerformPush(project, ServiceManager.getService(project, GitPlatformFacade.class));
@NotNull
private static Collection<GitRepository> collectRepositories(@NotNull Project project,
@Nullable VirtualFile[] files) {
GitRepositoryManager manager = GitUtil.getRepositoryManager(project);
Collection<GitRepository> repositories;
if (files == null) {
if (GitVcsSettings.getInstance(project).getSyncSetting() == DvcsBranchSync.SYNC) {
repositories = manager.getRepositories();
}
else {
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);
}
}
}
return repositories;
}
@Override
public void update(AnActionEvent e) {
super.update(e);
Project project = e.getProject();
e.getPresentation().setEnabledAndVisible(project != null && !GitUtil.getRepositoryManager(project).getRepositories().isEmpty());
}
}
@@ -27,7 +27,6 @@ import git4idea.GitPlatformFacade;
import git4idea.commands.Git;
import git4idea.commands.GitCommandResult;
import git4idea.commands.GitCompoundResult;
import git4idea.push.GitSimplePushResult;
import git4idea.repo.GitRemote;
import git4idea.repo.GitRepository;
import git4idea.ui.branch.GitMultiRootBranchConfig;
@@ -36,7 +35,6 @@ import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
@@ -173,13 +171,6 @@ class GitDeleteRemoteBranchOperation extends GitBranchOperation {
return myGit.push(repository, remoteName, url,":" + branchName);
}
@NotNull
private static GitCommandResult convertSimplePushResultToCommandResult(@NotNull GitSimplePushResult result) {
boolean success = result.getType() == GitSimplePushResult.Type.SUCCESS;
return new GitCommandResult(success, -1, success ? Collections.<String>emptyList() : Collections.singletonList(result.getOutput()),
success ? Collections.singletonList(result.getOutput()) : Collections.<String>emptyList(), null);
}
@Nullable
private static GitRemote getRemoteByName(@NotNull GitRepository repository, @NotNull String remoteName) {
for (GitRemote remote : repository.getRemotes()) {
@@ -18,6 +18,7 @@ package git4idea.checkin;
import com.intellij.CommonBundle;
import com.intellij.dvcs.DvcsCommitAdditionalComponent;
import com.intellij.dvcs.DvcsUtil;
import com.intellij.dvcs.push.ui.VcsPushDialog;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.ex.EditorEx;
@@ -47,7 +48,6 @@ import com.intellij.vcs.log.VcsUser;
import com.intellij.vcs.log.VcsUserRegistry;
import com.intellij.vcsUtil.VcsFileUtil;
import com.intellij.vcsUtil.VcsUtil;
import git4idea.GitPlatformFacade;
import git4idea.GitUtil;
import git4idea.GitVcs;
import git4idea.commands.GitCommand;
@@ -56,7 +56,7 @@ import git4idea.config.GitConfigUtil;
import git4idea.config.GitVcsSettings;
import git4idea.config.GitVersionSpecialty;
import git4idea.i18n.GitBundle;
import git4idea.push.GitPusher;
import git4idea.repo.GitRepository;
import git4idea.repo.GitRepositoryFiles;
import git4idea.repo.GitRepositoryManager;
import git4idea.util.GitFileUtils;
@@ -213,10 +213,12 @@ public class GitCheckinEnvironment implements CheckinEnvironment {
}
}
if (myNextCommitIsPushed != null && myNextCommitIsPushed.booleanValue() && exceptions.isEmpty()) {
// push
GitRepositoryManager manager = GitUtil.getRepositoryManager(myProject);
Collection<GitRepository> repositories = GitUtil.getRepositoriesFromRoots(manager, sortedChanges.keySet());
final List<GitRepository> preselectedRepositories = ContainerUtil.newArrayList(repositories);
UIUtil.invokeLaterIfNeeded(new Runnable() {
public void run() {
GitPusher.showPushDialogAndPerformPush(myProject, ServiceManager.getService(myProject, GitPlatformFacade.class));
new VcsPushDialog(myProject, preselectedRepositories).show();
}
});
}
@@ -20,7 +20,8 @@ import com.intellij.openapi.util.Computable;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vfs.VirtualFile;
import git4idea.GitCommit;
import git4idea.push.GitPushSpec;
import git4idea.GitLocalBranch;
import git4idea.GitRemoteBranch;
import git4idea.repo.GitRepository;
import git4idea.reset.GitResetMode;
import org.jetbrains.annotations.NotNull;
@@ -31,9 +32,6 @@ import java.util.Collection;
import java.util.List;
import java.util.Set;
/**
* @author Kirill Likhodedov
*/
public interface Git {
/**
@@ -104,16 +102,20 @@ public interface Git {
GitCommandResult tip(@NotNull GitRepository repository, @NotNull String branchName);
@NotNull
GitCommandResult push(@NotNull GitRepository repository, @NotNull String remote, @NotNull String url, @NotNull String spec,
GitCommandResult push(@NotNull GitRepository repository, @NotNull String remote, @Nullable String url, @NotNull String spec,
boolean updateTracking, @NotNull GitLineHandlerListener... listeners);
@NotNull
GitCommandResult push(@NotNull GitRepository repository, @NotNull String remote, @NotNull String url, @NotNull String spec,
GitCommandResult push(@NotNull GitRepository repository, @NotNull String remote, @Nullable String url, @NotNull String spec,
@NotNull GitLineHandlerListener... listeners);
@NotNull
GitCommandResult push(@NotNull GitRepository repository, @NotNull GitPushSpec spec, @NotNull String url,
@NotNull GitLineHandlerListener... listeners);
GitCommandResult push(GitRepository repository,
GitLocalBranch source,
GitRemoteBranch target,
boolean force,
boolean updateTracking,
GitLineHandlerListener... listeners);
@NotNull
GitCommandResult show(@NotNull GitRepository repository, @NotNull String... params);
@@ -23,13 +23,9 @@ import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.vcsUtil.VcsFileUtil;
import git4idea.GitBranch;
import git4idea.GitCommit;
import git4idea.GitExecutionException;
import git4idea.GitVcs;
import git4idea.*;
import git4idea.config.GitVersionSpecialty;
import git4idea.history.GitHistoryUtils;
import git4idea.push.GitPushSpec;
import git4idea.repo.GitRemote;
import git4idea.repo.GitRepository;
import git4idea.reset.GitResetMode;
@@ -377,23 +373,35 @@ public class GitImpl implements Git {
@Override
@NotNull
public GitCommandResult push(@NotNull final GitRepository repository, @NotNull final String remote, @NotNull final String url,
@NotNull final String spec, final boolean updateTracking,
public GitCommandResult push(@NotNull GitRepository repository, @NotNull String remote, @Nullable String url, @NotNull String spec,
boolean updateTracking, @NotNull GitLineHandlerListener... listeners) {
return doPush(repository, remote, url, spec, false, updateTracking, listeners);
}
@NotNull
private GitCommandResult doPush(@NotNull final GitRepository repository, @NotNull final String remote, @Nullable final String url,
@NotNull final String spec, final boolean force, final boolean updateTracking,
@NotNull final GitLineHandlerListener... listeners) {
return runCommand(new Computable<GitLineHandler>() {
@Override
public GitLineHandler compute() {
final GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.PUSH);
h.setUrl(url);
if (url != null) {
h.setUrl(url);
}
h.setSilent(false);
h.setStdoutSuppressed(false);
addListeners(h, listeners);
h.addProgressParameter();
h.addParameters("--porcelain");
h.addParameters(remote);
h.addParameters(spec);
if (updateTracking) {
h.addParameters("--set-upstream");
}
if (force) {
h.addParameters("--force");
}
return h;
}
});
@@ -401,19 +409,27 @@ public class GitImpl implements Git {
@Override
@NotNull
public GitCommandResult push(@NotNull GitRepository repository, @NotNull String remote, @NotNull String url, @NotNull String spec,
public GitCommandResult push(@NotNull GitRepository repository, @NotNull String remote, @Nullable String url, @NotNull String spec,
@NotNull GitLineHandlerListener... listeners) {
return push(repository, remote, url, spec, false, listeners);
}
@Override
@NotNull
public GitCommandResult push(@NotNull GitRepository repository, @NotNull GitPushSpec pushSpec, @NotNull String url,
@NotNull GitLineHandlerListener... listeners) {
GitRemote remote = pushSpec.getRemote();
GitBranch remoteBranch = pushSpec.getDest();
String destination = remoteBranch.getName().replaceFirst(remote.getName() + "/", "");
return push(repository, remote.getName(), url, pushSpec.getSource().getName() + ":" + destination, listeners);
public GitCommandResult push(GitRepository repository, GitLocalBranch source, GitRemoteBranch target,
boolean force, boolean updateTracking, GitLineHandlerListener... listeners) {
GitRemote remote = target.getRemote();
Collection<String> pushUrls = remote.getPushUrls(); // TODO handle the case with multiple pushurls with different protocols
String url;
if (pushUrls.isEmpty()) {
LOG.error("No urls or pushUrls are defined for " + remote);
url = null;
}
else {
url = pushUrls.iterator().next();
}
String spec = source.getFullName() + ":" + target.getNameForRemoteOperations();
return doPush(repository, remote.getName(), url, spec, force, updateTracking, listeners);
}
@NotNull
@@ -1,89 +0,0 @@
/*
* Copyright 2000-2011 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package git4idea.push;
import git4idea.GitBranch;
import git4idea.GitCommit;
import org.jetbrains.annotations.NotNull;
import java.util.*;
/**
* Holds Git commits made in a single repository grouped by branches.
*
* @author Kirill Likhodedov
*/
final class GitCommitsByBranch {
private final Map<GitBranch, GitPushBranchInfo> myCommitsByBranch;
GitCommitsByBranch(@NotNull Map<GitBranch, GitPushBranchInfo> commitsByBranch) {
myCommitsByBranch = new HashMap<GitBranch, GitPushBranchInfo>(commitsByBranch);
}
GitCommitsByBranch(GitCommitsByBranch commitsByBranch) {
this(new HashMap<GitBranch, GitPushBranchInfo>(commitsByBranch.myCommitsByBranch));
}
boolean isEmpty() {
for (GitPushBranchInfo info : myCommitsByBranch.values()) {
if (!info.isEmpty()) {
return false;
}
}
return true;
}
int commitsNumber() {
int sum = 0;
for (GitPushBranchInfo branchInfo : myCommitsByBranch.values()) {
sum += branchInfo.getCommits().size();
}
return sum;
}
@NotNull
Collection<GitBranch> getBranches() {
return new HashSet<GitBranch>(myCommitsByBranch.keySet());
}
@NotNull
GitPushBranchInfo get(@NotNull GitBranch branch) {
return new GitPushBranchInfo(myCommitsByBranch.get(branch));
}
/**
* Returns new GitCommitsByBranch that contains commits only from the given branch (or nothing, if the given branch didn't exist in
* the original structure).
*/
@NotNull
GitCommitsByBranch retain(@NotNull GitBranch branch) {
Map<GitBranch, GitPushBranchInfo> res = new HashMap<GitBranch, GitPushBranchInfo>();
if (myCommitsByBranch.containsKey(branch)) {
res.put(branch, myCommitsByBranch.get(branch));
}
return new GitCommitsByBranch(res);
}
@NotNull
public Collection<GitCommit> getAllCommits() {
Collection<GitCommit> commits = new ArrayList<GitCommit>();
for (GitPushBranchInfo branchInfo : myCommitsByBranch.values()) {
commits.addAll(branchInfo.getCommits());
}
return commits;
}
}
@@ -1,94 +0,0 @@
/*
* Copyright 2000-2011 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package git4idea.push;
import git4idea.GitBranch;
import git4idea.GitCommit;
import git4idea.repo.GitRepository;
import org.jetbrains.annotations.NotNull;
import java.util.*;
/**
* Holds Git commits grouped by repositories and by branches.
* Actually, it is just a map (of maps of lists of commits) encapsulated in a separated class with some handy methods.
*
* @author Kirill Likhodedov
*/
final class GitCommitsByRepoAndBranch {
private final Map<GitRepository, GitCommitsByBranch> myCommitsByRepository;
GitCommitsByRepoAndBranch(@NotNull Map<GitRepository, GitCommitsByBranch> commitsByRepository) {
myCommitsByRepository = commitsByRepository;
}
@NotNull
static GitCommitsByRepoAndBranch empty() {
return new GitCommitsByRepoAndBranch(new HashMap<GitRepository, GitCommitsByBranch>());
}
@NotNull
Collection<GitRepository> getRepositories() {
return new HashSet<GitRepository>(myCommitsByRepository.keySet());
}
@NotNull
GitCommitsByBranch get(@NotNull GitRepository repository) {
return new GitCommitsByBranch(myCommitsByRepository.get(repository));
}
/**
* Creates new GitCommitByRepoAndBranch structure with only those repositories, which exist in the specified collection.
*/
@NotNull
GitCommitsByRepoAndBranch retainAll(@NotNull Collection<GitRepository> repositories) {
Map<GitRepository, GitCommitsByBranch> commits = new HashMap<GitRepository, GitCommitsByBranch>();
for (GitRepository selectedRepository : repositories) {
GitCommitsByBranch value = myCommitsByRepository.get(selectedRepository);
if (value != null) {
commits.put(selectedRepository, value);
}
}
return new GitCommitsByRepoAndBranch(commits);
}
/**
* Creates new GitCommitByRepoAndBranch structure with only those pairs repository-branch, which exist in the specified map.
*/
@NotNull
GitCommitsByRepoAndBranch retainAll(@NotNull Map<GitRepository, GitBranch> repositoriesBranches) {
Map<GitRepository, GitCommitsByBranch> commits = new HashMap<GitRepository, GitCommitsByBranch>();
for (GitRepository repository : repositoriesBranches.keySet()) {
GitCommitsByBranch commitsByBranch = myCommitsByRepository.get(repository);
if (commitsByBranch != null) {
commits.put(repository, commitsByBranch.retain(repositoriesBranches.get(repository)));
}
}
return new GitCommitsByRepoAndBranch(commits);
}
@NotNull
public Collection<GitCommit> getAllCommits() {
Collection<GitCommit> commits = new ArrayList<GitCommit>();
for (GitCommitsByBranch commitsByBranch : myCommitsByRepository.values()) {
commits.addAll(commitsByBranch.getAllCommits());
}
return commits;
}
}
@@ -1,245 +0,0 @@
/*
* Copyright 2000-2012 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package git4idea.push;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.actionSystem.impl.ActionButton;
import com.intellij.openapi.project.DumbAwareAction;
import com.intellij.ui.ListCellRendererWrapper;
import com.intellij.ui.components.JBLabel;
import com.intellij.util.ui.GridBag;
import com.intellij.util.ui.UIUtil;
import git4idea.repo.GitRemote;
import git4idea.repo.GitRepository;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.*;
import java.util.List;
/**
* @author Kirill Likhodedov
*/
class GitManualPushToBranch extends JPanel {
private final Collection<GitRepository> myRepositories;
private final JCheckBox myManualPush;
private final JTextField myDestBranchTextField;
private final JBLabel myComment;
private final GitPushLogRefreshAction myRefreshAction;
private final JComponent myRefreshButton;
private final RemoteSelector myRemoteSelector;
private final JComponent myRemoteSelectorComponent;
GitManualPushToBranch(@NotNull Collection<GitRepository> repositories,
@NotNull final Runnable performOnRefresh) {
super();
myRepositories = repositories;
myManualPush = new JCheckBox("Push current branch to alternative branch: ", false);
myManualPush.setMnemonic('b');
myDestBranchTextField = new JTextField(20);
myComment = new JBLabel("This will apply to all selected repositories", UIUtil.ComponentStyle.SMALL);
myRefreshAction = new GitPushLogRefreshAction() {
@Override public void actionPerformed(AnActionEvent e) {
performOnRefresh.run();
}
};
myRefreshButton = new ActionButton(myRefreshAction, myRefreshAction.getTemplatePresentation(), myRefreshAction.getTemplatePresentation().getText(), ActionToolbar.DEFAULT_MINIMUM_BUTTON_SIZE);
myRefreshButton.setFocusable(true);
final ShortcutSet shortcutSet = ActionManager.getInstance().getAction(IdeActions.ACTION_REFRESH).getShortcutSet();
myRefreshAction.registerCustomShortcutSet(shortcutSet, myRefreshButton);
myRemoteSelector = new RemoteSelector(getRemotesWithCommonNames(repositories));
myRemoteSelectorComponent = myRemoteSelector.createComponent();
setDefaultComponentsEnabledState(myManualPush.isSelected());
myManualPush.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
boolean isManualPushSelected = myManualPush.isSelected();
setDefaultComponentsEnabledState(isManualPushSelected);
if (isManualPushSelected) {
myDestBranchTextField.requestFocus();
myDestBranchTextField.selectAll();
}
}
});
layoutComponents();
}
private void setDefaultComponentsEnabledState(boolean selected) {
myDestBranchTextField.setEnabled(selected);
myComment.setEnabled(selected);
myRemoteSelector.setEnabled(selected);
}
private void layoutComponents() {
JPanel panel = new JPanel();
GridBagLayout layout = new GridBagLayout();
panel.setLayout(layout);
GridBag g = new GridBag()
.setDefaultFill(GridBagConstraints.NONE)
.setDefaultAnchor(GridBagConstraints.BASELINE_LEADING)
.setDefaultWeightX(1, 1)
.setDefaultInsets(new Insets(0, 0, UIUtil.DEFAULT_VGAP, 5))
;
panel.add(myManualPush, g.nextLine().next());
panel.add(myRemoteSelectorComponent, g.next());
panel.add(myDestBranchTextField, g.next());
panel.add(myRefreshButton, g.next());
if (myRepositories.size() > 1) {
panel.add(myComment, g.nextLine().insets(0, 28, 0, 0).coverLine());
}
setLayout(new BorderLayout());
add(panel, BorderLayout.WEST);
}
boolean turnedOn() {
return myManualPush.isSelected() && !myDestBranchTextField.getText().isEmpty();
}
@NotNull
String getBranchToPush() {
return myDestBranchTextField.getText();
}
void setBranchToPushIfNotSet(@NotNull String text) {
if (myDestBranchTextField.getText().isEmpty()) {
myDestBranchTextField.setText(text);
}
}
@NotNull
GitRemote getSelectedRemote() {
return myRemoteSelector.getSelectedValue();
}
public void selectRemote(String remoteName) {
myRemoteSelector.selectRemote(remoteName);
}
@NotNull
public static Collection<GitRemote> getRemotesWithCommonNames(@NotNull Collection<GitRepository> repositories) {
if (repositories.isEmpty()) {
return Collections.emptyList();
}
Iterator<GitRepository> iterator = repositories.iterator();
List<GitRemote> commonRemotes = new ArrayList<GitRemote>(iterator.next().getRemotes());
while (iterator.hasNext()) {
GitRepository repository = iterator.next();
Collection<String> remoteNames = getRemoteNames(repository);
for (Iterator<GitRemote> commonIter = commonRemotes.iterator(); commonIter.hasNext(); ) {
GitRemote remote = commonIter.next();
if (!remoteNames.contains(remote.getName())) {
commonIter.remove();
}
}
}
return commonRemotes;
}
@NotNull
private static Collection<String> getRemoteNames(@NotNull GitRepository repository) {
Collection<String> names = new ArrayList<String>(repository.getRemotes().size());
for (GitRemote remote : repository.getRemotes()) {
names.add(remote.getName());
}
return names;
}
/**
* Component to select remotes.
* Just a JCombobox actually, but more flexible: if there is only one remote, we could use JLabel or something like that.
*/
private static class RemoteSelector {
private final Collection<GitRemote> myRemotes;
private JComboBox myRemoteCombobox;
private RemoteSelector(@NotNull Collection<GitRemote> remotes) {
myRemotes = remotes;
}
@NotNull
JComponent createComponent() {
myRemoteCombobox = new JComboBox();
myRemoteCombobox.setRenderer(new RemoteCellRenderer(myRemoteCombobox.getRenderer()));
for (GitRemote remote : myRemotes) {
myRemoteCombobox.addItem(remote);
}
myRemoteCombobox.setToolTipText("Select remote");
if (myRemotes.size() == 1) {
myRemoteCombobox.setEnabled(false);
}
return myRemoteCombobox;
}
@NotNull
GitRemote getSelectedValue() {
return (GitRemote)myRemoteCombobox.getSelectedItem();
}
void setEnabled(boolean selected) {
if (myRemotes.size() > 1) {
myRemoteCombobox.setEnabled(selected);
}
}
public void selectRemote(@NotNull String remoteName) {
for (GitRemote remote : myRemotes) {
if (remote.getName().equals(remoteName)) {
myRemoteCombobox.setSelectedItem(remote);
return;
}
}
myRemoteCombobox.setSelectedIndex(0);
}
private static class RemoteCellRenderer extends ListCellRendererWrapper {
public RemoteCellRenderer(final ListCellRenderer listCellRenderer) {
super();
}
@Override
public void customize(JList list, Object value, int index, boolean selected, boolean hasFocus) {
if (value instanceof GitRemote) {
setText(((GitRemote)value).getName());
}
}
}
}
private abstract static class GitPushLogRefreshAction extends DumbAwareAction {
GitPushLogRefreshAction() {
super("Refresh commit list", "Refresh commit list", AllIcons.Actions.Refresh);
}
}
}
@@ -0,0 +1,63 @@
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package git4idea.push;
import com.intellij.dvcs.push.*;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.vcs.log.VcsFullCommitDetails;
import git4idea.GitCommit;
import git4idea.GitUtil;
import git4idea.history.GitHistoryUtils;
import git4idea.repo.GitRepository;
import org.jetbrains.annotations.NotNull;
import java.util.Collections;
import java.util.List;
public class GitOutgoingCommitsProvider extends OutgoingCommitsProvider<GitRepository, GitPushSource, GitPushTarget> {
@NotNull private final Project myProject;
public GitOutgoingCommitsProvider(@NotNull Project project) {
myProject = project;
}
@NotNull
@Override
public OutgoingResult getOutgoingCommits(@NotNull GitRepository repository, @NotNull PushSpec<GitPushSource, GitPushTarget> pushSpec,
boolean initial) {
String source = pushSpec.getSource().getBranch().getFullName();
GitPushTarget target = pushSpec.getTarget();
assert target != null;
String destination = target.getBranch().getFullName();
try {
List<GitCommit> commits;
if (!target.isNewBranchCreated()) {
commits = GitHistoryUtils.history(myProject, repository.getRoot(), destination + ".." + source);
}
else {
commits = GitHistoryUtils.history(myProject, repository.getRoot(),
source, "--not", "--remotes=" + target.getBranch().getRemote().getName(), "--max-count=" + 1000);
}
return new OutgoingResult(commits, Collections.<VcsError>emptyList());
}
catch (VcsException e) {
return new OutgoingResult(Collections.<VcsFullCommitDetails>emptyList(),
Collections.singletonList(new VcsError(GitUtil.cleanupErrorPrefixes(e.getMessage()))));
}
}
}
@@ -1,84 +0,0 @@
/*
* Copyright 2000-2011 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package git4idea.push;
import git4idea.GitCommit;
import git4idea.GitLocalBranch;
import git4idea.GitRemoteBranch;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
/**
* Holds the list of commits and the destination branch (which a branch associated with this GitPushBranchInfo is going to be pushed to).
*
* @author Kirill Likhodedov
*/
final class GitPushBranchInfo {
private final GitLocalBranch mySourceBranch;
private final GitRemoteBranch myDestBranch;
private final Type myType;
private final List<GitCommit> myCommits;
enum Type {
STANDARD, // the branch this branch is targeted, exists (and is either the tracked/matched branch or manually specified)
NEW_BRANCH, // the source branch will be pushed to a new branch
NO_TRACKED_OR_TARGET // the branch has no tracked/matched, and target was not manually specified
}
GitPushBranchInfo(@NotNull GitLocalBranch sourceBranch, @NotNull GitRemoteBranch destBranch,
@NotNull List<GitCommit> commits, @NotNull Type type) {
mySourceBranch = sourceBranch;
myCommits = commits;
myDestBranch = destBranch;
myType = type;
}
GitPushBranchInfo(@NotNull GitPushBranchInfo pushBranchInfo) {
this(pushBranchInfo.getSourceBranch(), pushBranchInfo.getDestBranch(), pushBranchInfo.getCommits(), pushBranchInfo.getType());
}
@NotNull
Type getType() {
return myType;
}
boolean isNewBranchCreated() {
return myType == Type.NEW_BRANCH;
}
@NotNull
GitRemoteBranch getDestBranch() {
return myDestBranch;
}
@NotNull
List<GitCommit> getCommits() {
return new ArrayList<GitCommit>(myCommits);
}
@NotNull
public GitLocalBranch getSourceBranch() {
return mySourceBranch;
}
boolean isEmpty() {
return myCommits.isEmpty();
}
}
@@ -1,86 +0,0 @@
/*
* Copyright 2000-2011 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package git4idea.push;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Result of pushing a single branch.
*
* @author Kirill Likhodedov
*/
final class GitPushBranchResult {
private final Type myType;
private final int myNumberOfPushedCommits;
private final String myTargetBranchName;
enum Type {
SUCCESS,
NEW_BRANCH,
REJECTED,
ERROR
}
private GitPushBranchResult(Type type, int numberOfPushedCommits, @Nullable String targetBranchName) {
myType = type;
myNumberOfPushedCommits = numberOfPushedCommits;
myTargetBranchName = targetBranchName;
}
static GitPushBranchResult success(int numberOfPushedCommits) {
return new GitPushBranchResult(Type.SUCCESS, numberOfPushedCommits, null);
}
static GitPushBranchResult newBranch(String targetBranchName) {
return new GitPushBranchResult(Type.NEW_BRANCH, 0, targetBranchName);
}
static GitPushBranchResult rejected() {
return new GitPushBranchResult(Type.REJECTED, 0, null);
}
static GitPushBranchResult error() {
return new GitPushBranchResult(Type.ERROR, 0, null);
}
int getNumberOfPushedCommits() {
return myNumberOfPushedCommits;
}
boolean isSuccess() {
return myType == Type.SUCCESS;
}
boolean isRejected() {
return myType == Type.REJECTED;
}
boolean isError() {
return myType == Type.ERROR;
}
boolean isNewBranch() {
return myType == Type.NEW_BRANCH;
}
@NotNull
String getTargetBranchName() {
return myTargetBranchName != null ? myTargetBranchName : "";
}
}
@@ -1,337 +0,0 @@
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package git4idea.push;
import com.intellij.dvcs.DvcsUtil;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.EmptyProgressIndicator;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.util.Couple;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.impl.VcsGlobalMessageManager;
import com.intellij.ui.components.JBLoadingPanel;
import com.intellij.util.Consumer;
import com.intellij.util.ui.UIUtil;
import git4idea.*;
import git4idea.branch.GitBranchUtil;
import git4idea.repo.GitRemote;
import git4idea.repo.GitRepository;
import git4idea.repo.GitRepositoryManager;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
import java.util.*;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
/**
* @author Kirill Likhodedov
*/
public class GitPushDialog extends DialogWrapper {
private static final Logger LOG = Logger.getInstance(GitPushDialog.class);
private static final String DEFAULT_REMOTE = "origin";
private Project myProject;
private final GitRepositoryManager myRepositoryManager;
private final GitPusher myPusher;
private final GitPushLog myListPanel;
private GitCommitsByRepoAndBranch myGitCommitsToPush;
private Map<GitRepository, GitPushSpec> myPushSpecs;
private final Collection<GitRepository> myRepositories;
private final JBLoadingPanel myLoadingPanel;
private final Object COMMITS_LOADING_LOCK = new Object();
private final GitManualPushToBranch myRefspecPanel;
private final AtomicReference<String> myDestBranchInfoOnRefresh = new AtomicReference<String>();
private final boolean myPushPossible;
public GitPushDialog(@NotNull Project project) {
super(project);
myProject = project;
myPusher = new GitPusher(myProject, ServiceManager.getService(project, GitPlatformFacade.class), new EmptyProgressIndicator());
myRepositoryManager = GitUtil.getRepositoryManager(myProject);
myRepositories = getRepositoriesWithRemotes();
myLoadingPanel = new JBLoadingPanel(new BorderLayout(), this.getDisposable());
myListPanel = new GitPushLog(myProject, myRepositories, new RepositoryCheckboxListener());
myRefspecPanel = new GitManualPushToBranch(myRepositories, new RefreshButtonListener());
if (GitManualPushToBranch.getRemotesWithCommonNames(myRepositories).isEmpty()) {
myRefspecPanel.setVisible(false);
setErrorText("Can't push, because no remotes are defined");
setOKActionEnabled(false);
myPushPossible = false;
} else {
myPushPossible = true;
}
init();
setOKButtonText("Push");
setOKButtonMnemonic('P');
setTitle("Git Push");
}
@NotNull
private List<GitRepository> getRepositoriesWithRemotes() {
List<GitRepository> repositories = new ArrayList<GitRepository>();
for (GitRepository repository : myRepositoryManager.getRepositories()) {
if (!repository.getRemotes().isEmpty()) {
repositories.add(repository);
}
}
return repositories;
}
@Nullable
@Override
protected JComponent createNorthPanel() {
final JComponent banner = VcsGlobalMessageManager.getInstance(myProject).getMessageBanner();
return banner != null ? banner : super.createNorthPanel();
}
@Override
protected JComponent createCenterPanel() {
JPanel optionsPanel = new JPanel(new BorderLayout());
optionsPanel.add(myRefspecPanel);
JComponent rootPanel = new JPanel(new BorderLayout(0, 15));
rootPanel.add(createCommitListPanel(), BorderLayout.CENTER);
rootPanel.add(optionsPanel, BorderLayout.SOUTH);
return rootPanel;
}
@Override
protected String getHelpId() {
return "reference.VersionControl.Git.PushDialog";
}
private JComponent createCommitListPanel() {
myLoadingPanel.add(myListPanel, BorderLayout.CENTER);
if (myPushPossible) {
loadCommitsInBackground();
} else {
myLoadingPanel.startLoading();
myLoadingPanel.stopLoading();
}
JPanel commitListPanel = new JPanel(new BorderLayout());
commitListPanel.add(myLoadingPanel, BorderLayout.CENTER);
return commitListPanel;
}
private void loadCommitsInBackground() {
myLoadingPanel.startLoading();
ApplicationManager.getApplication().executeOnPooledThread(new Runnable() {
public void run() {
final AtomicReference<String> error = new AtomicReference<String>();
synchronized (COMMITS_LOADING_LOCK) {
error.set(collectInfoToPush());
}
final Couple<String> remoteAndBranch = getRemoteAndTrackedBranchForCurrentBranch();
UIUtil.invokeLaterIfNeeded(new Runnable() {
@Override
public void run() {
if (error.get() != null) {
myListPanel.displayError(error.get());
} else {
myListPanel.setCommits(myGitCommitsToPush);
}
if (!myRefspecPanel.turnedOn()) {
myRefspecPanel.selectRemote(remoteAndBranch.getFirst());
myRefspecPanel.setBranchToPushIfNotSet(remoteAndBranch.getSecond());
}
myLoadingPanel.stopLoading();
}
});
}
});
}
@NotNull
private Couple<String> getRemoteAndTrackedBranchForCurrentBranch() {
if (myGitCommitsToPush != null) {
Collection<GitRepository> repositories = myGitCommitsToPush.getRepositories();
if (!repositories.isEmpty()) {
GitRepository repository = repositories.iterator().next();
GitBranch currentBranch = repository.getCurrentBranch();
assert currentBranch != null;
if (myGitCommitsToPush.get(repository).get(currentBranch).getDestBranch() == GitPusher.NO_TARGET_BRANCH) { // push to branch with the same name
return Couple.of(DEFAULT_REMOTE, currentBranch.getName());
}
String remoteName;
try {
remoteName = GitBranchUtil.getTrackedRemoteName(myProject, repository.getRoot(), currentBranch.getName());
if (remoteName == null) {
remoteName = DEFAULT_REMOTE;
}
}
catch (VcsException e) {
LOG.info("Couldn't retrieve tracked branch for current branch " + currentBranch, e);
remoteName = DEFAULT_REMOTE;
}
String targetBranch = myGitCommitsToPush.get(repository).get(currentBranch).getDestBranch().getNameForRemoteOperations();
return Couple.of(remoteName, targetBranch);
}
}
return Couple.of(DEFAULT_REMOTE, "");
}
@Nullable
private String collectInfoToPush() {
try {
LOG.info("collectInfoToPush...");
myPushSpecs = pushSpecsForCurrentOrEnteredBranches();
myGitCommitsToPush = myPusher.collectCommitsToPush(myPushSpecs);
LOG.info(String.format("collectInfoToPush | Collected commits to push. Push spec: %s, commits: %s",
myPushSpecs, logMessageForCommits(myGitCommitsToPush)));
return null;
}
catch (VcsException e) {
myGitCommitsToPush = GitCommitsByRepoAndBranch.empty();
LOG.error("collectInfoToPush | Couldn't collect commits to push. Push spec: " + myPushSpecs, e);
return e.getMessage();
}
}
private static String logMessageForCommits(GitCommitsByRepoAndBranch commitsToPush) {
StringBuilder logMessage = new StringBuilder();
for (GitCommit commit : commitsToPush.getAllCommits()) {
logMessage.append(DvcsUtil.getShortHash(commit.getId().toString()));
}
return logMessage.toString();
}
private Map<GitRepository, GitPushSpec> pushSpecsForCurrentOrEnteredBranches() throws VcsException {
Map<GitRepository, GitPushSpec> defaultSpecs = new HashMap<GitRepository, GitPushSpec>();
for (GitRepository repository : myRepositories) {
GitLocalBranch currentBranch = repository.getCurrentBranch();
if (currentBranch == null) {
continue;
}
String remoteName = GitBranchUtil.getTrackedRemoteName(repository.getProject(), repository.getRoot(), currentBranch.getName());
String trackedBranchName = GitBranchUtil.getTrackedBranchName(repository.getProject(), repository.getRoot(), currentBranch.getName());
GitRemote remote = GitUtil.findRemoteByName(repository, remoteName);
GitRemoteBranch targetBranch;
if (remote != null && trackedBranchName != null) {
targetBranch = GitBranchUtil.findRemoteBranchByName(trackedBranchName, remote.getName(),
repository.getBranches().getRemoteBranches());
}
else {
Pair<GitRemote, GitRemoteBranch> remoteAndBranch = GitUtil.findMatchingRemoteBranch(repository, currentBranch);
if (remoteAndBranch == null) {
targetBranch = GitPusher.NO_TARGET_BRANCH;
} else {
targetBranch = remoteAndBranch.getSecond();
}
}
if (myRefspecPanel.turnedOn()) {
String manualBranchName = myRefspecPanel.getBranchToPush();
remote = myRefspecPanel.getSelectedRemote();
GitRemoteBranch manualBranch = GitBranchUtil.findRemoteBranchByName(manualBranchName, remote.getName(),
repository.getBranches().getRemoteBranches());
if (manualBranch == null) {
manualBranch = new GitStandardRemoteBranch(remote, manualBranchName, GitBranch.DUMMY_HASH);
}
targetBranch = manualBranch;
}
GitPushSpec pushSpec = new GitPushSpec(currentBranch, targetBranch == null ? GitPusher.NO_TARGET_BRANCH : targetBranch);
defaultSpecs.put(repository, pushSpec);
}
return defaultSpecs;
}
@Override
public JComponent getPreferredFocusedComponent() {
return myListPanel.getPreferredFocusComponent();
}
@Override
protected String getDimensionServiceKey() {
return GitPushDialog.class.getName();
}
@NotNull
public GitPushInfo getPushInfo() {
// waiting for commit list loading, because this information is needed to correctly handle rejected push situation and correctly
// notify about pushed commits
// TODO optimize: don't refresh: information about pushed commits can be achieved from the successful push output
LOG.info("getPushInfo start");
synchronized (COMMITS_LOADING_LOCK) {
GitCommitsByRepoAndBranch selectedCommits;
if (myGitCommitsToPush == null) {
LOG.info("getPushInfo | myGitCommitsToPush == null. collecting...");
collectInfoToPush();
selectedCommits = myGitCommitsToPush;
}
else {
if (refreshNeeded()) {
LOG.info("getPushInfo | refresh is needed, collecting...");
collectInfoToPush();
}
Collection<GitRepository> selectedRepositories = myListPanel.getSelectedRepositories();
selectedCommits = myGitCommitsToPush.retainAll(selectedRepositories);
}
LOG.info("getPushInfo | selectedCommits: " + logMessageForCommits(selectedCommits));
return new GitPushInfo(selectedCommits, myPushSpecs);
}
}
private boolean refreshNeeded() {
String currentDestBranchValue = myRefspecPanel.turnedOn() ? myRefspecPanel.getBranchToPush(): null;
String savedValue = myDestBranchInfoOnRefresh.get();
if (savedValue == null) {
return currentDestBranchValue != null;
}
return !savedValue.equals(currentDestBranchValue);
}
private class RepositoryCheckboxListener implements Consumer<Boolean> {
@Override public void consume(Boolean checked) {
if (checked) {
setOKActionEnabled(true);
} else {
Collection<GitRepository> repositories = myListPanel.getSelectedRepositories();
if (repositories.isEmpty()) {
setOKActionEnabled(false);
} else {
setOKActionEnabled(true);
}
}
}
}
private class RefreshButtonListener implements Runnable {
@Override
public void run() {
myDestBranchInfoOnRefresh.set(myRefspecPanel.turnedOn() ? myRefspecPanel.getBranchToPush(): null);
loadCommitsInBackground();
}
}
}
@@ -1,58 +0,0 @@
/*
* Copyright 2000-2011 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package git4idea.push;
import com.intellij.util.containers.HashMap;
import git4idea.GitBranch;
import git4idea.repo.GitRepository;
import org.jetbrains.annotations.NotNull;
import java.util.Map;
/**
* Holds information about a single Git push action which is to be executed or has been executed.
*
* @author Kirill Likhodedov
*/
public final class GitPushInfo {
@NotNull private final GitCommitsByRepoAndBranch myCommits;
@NotNull private final Map<GitRepository, GitPushSpec> myPushSpecs;
/**
* We pass the complex {@link GitCommitsByRepoAndBranch} structure here instead of just the list of repositories,
* because later (after successful push, for example) it may be needed for showing useful notifications, such as number of commits pushed.
*/
public GitPushInfo(@NotNull GitCommitsByRepoAndBranch commits, @NotNull Map<GitRepository, GitPushSpec> pushSpecs) {
myCommits = commits;
myPushSpecs = pushSpecs;
}
@NotNull
public Map<GitRepository, GitPushSpec> getPushSpecs() {
return myPushSpecs;
}
@NotNull
public GitCommitsByRepoAndBranch getCommits() {
return myCommits;
}
@NotNull
public GitPushInfo retain(Map<GitRepository, GitBranch> repoBranchMap) {
return new GitPushInfo(myCommits.retainAll(repoBranchMap), new HashMap<GitRepository, GitPushSpec>(myPushSpecs));
}
}
@@ -1,418 +0,0 @@
/*
* Copyright 2000-2011 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package git4idea.push;
import com.intellij.dvcs.DvcsUtil;
import com.intellij.openapi.actionSystem.CommonShortcuts;
import com.intellij.openapi.actionSystem.DataKey;
import com.intellij.openapi.actionSystem.DataSink;
import com.intellij.openapi.actionSystem.TypeSafeDataProvider;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Splitter;
import com.intellij.openapi.vcs.VcsDataKeys;
import com.intellij.openapi.vcs.changes.Change;
import com.intellij.openapi.vcs.changes.issueLinks.IssueLinkHtmlRenderer;
import com.intellij.openapi.vcs.changes.ui.ChangesBrowser;
import com.intellij.ui.*;
import com.intellij.util.ArrayUtil;
import com.intellij.util.Consumer;
import com.intellij.util.ui.UIUtil;
import com.intellij.util.ui.tree.TreeUtil;
import git4idea.GitBranch;
import git4idea.GitCommit;
import git4idea.GitUtil;
import git4idea.repo.GitRepository;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.*;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.util.*;
import java.util.List;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* The list of commits from multiple repositories and branches, with diff panel at the right.
*
* @author Kirill Likhodedov
*/
class GitPushLog extends JPanel implements TypeSafeDataProvider {
private final Project myProject;
private final Collection<GitRepository> myAllRepositories;
private final ChangesBrowser myChangesBrowser;
private final CheckboxTree myTree;
private final DefaultTreeModel myTreeModel;
private final CheckedTreeNode myRootNode;
private final ReentrantReadWriteLock TREE_CONSTRUCTION_LOCK = new ReentrantReadWriteLock();
private boolean myTreeWasConstructed;
private final MyTreeCellRenderer myTreeCellRenderer;
GitPushLog(@NotNull Project project, @NotNull Collection<GitRepository> repositories, @NotNull final Consumer<Boolean> checkboxListener) {
myProject = project;
myAllRepositories = repositories;
myRootNode = new CheckedTreeNode(null);
myRootNode.add(new DefaultMutableTreeNode(new FakeCommit()));
myTreeModel = new DefaultTreeModel(myRootNode);
myTreeCellRenderer = new MyTreeCellRenderer();
myTree = new CheckboxTree(myTreeCellRenderer, myRootNode) {
@Override
protected void onNodeStateChanged(CheckedTreeNode node) {
Object userObject = node.getUserObject();
if (userObject instanceof GitRepository) {
checkboxListener.consume(node.isChecked());
}
}
@Override
public String getToolTipText(MouseEvent event) {
final TreePath path = myTree.getPathForLocation(event.getX(), event.getY());
if (path == null) {
return "";
}
Object node = path.getLastPathComponent();
if (node == null || (!(node instanceof DefaultMutableTreeNode))) {
return "";
}
Object userObject = ((DefaultMutableTreeNode)node).getUserObject();
if (userObject instanceof GitCommit) {
GitCommit commit = (GitCommit)userObject;
return getHashString(commit) + " " + getDateString(commit) + " by " + commit.getAuthor().getName() + "\n\n" +
IssueLinkHtmlRenderer.formatTextWithLinks(myProject, commit.getFullMessage());
}
return "";
}
};
myTree.setRootVisible(false);
TreeUtil.expandAll(myTree);
myTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
myTree.addTreeSelectionListener(new TreeSelectionListener() {
@Override
public void valueChanged(TreeSelectionEvent e) {
DefaultMutableTreeNode node = (DefaultMutableTreeNode) myTree.getLastSelectedPathComponent();
if (node != null) {
Object nodeInfo = node.getUserObject();
if (nodeInfo instanceof GitCommit) {
myChangesBrowser.getViewer().setEmptyText("No differences");
myChangesBrowser.setChangesToDisplay(new ArrayList<Change>(((GitCommit)nodeInfo).getChanges()));
return;
}
}
setDefaultEmptyText();
myChangesBrowser.setChangesToDisplay(Collections.<Change>emptyList());
}
});
ToolTipManager.sharedInstance().registerComponent(myTree);
myChangesBrowser = new ChangesBrowser(project, null, Collections.<Change>emptyList(), null, false, true, null, ChangesBrowser.MyUseCase.LOCAL_CHANGES, null);
myChangesBrowser.getDiffAction().registerCustomShortcutSet(CommonShortcuts.getDiff(), myTree);
setDefaultEmptyText();
Splitter splitter = new Splitter(false, 0.7f);
splitter.setFirstComponent(ScrollPaneFactory.createScrollPane(myTree));
splitter.setSecondComponent(myChangesBrowser);
setLayout(new BorderLayout());
add(splitter);
}
private void setDefaultEmptyText() {
myChangesBrowser.getViewer().setEmptyText("No commits selected");
}
// Make changes available for diff action
@Override
public void calcData(DataKey key, DataSink sink) {
if (VcsDataKeys.CHANGES.equals(key)) {
DefaultMutableTreeNode[] selectedNodes = myTree.getSelectedNodes(DefaultMutableTreeNode.class, null);
if (selectedNodes.length == 0) {
return;
}
Object object = selectedNodes[0].getUserObject();
if (object instanceof GitCommit) {
sink.put(key, ArrayUtil.toObjectArray(((GitCommit)object).getChanges(), Change.class));
}
}
}
@NotNull
public JComponent getPreferredFocusComponent() {
return myTree;
}
void setCommits(@NotNull GitCommitsByRepoAndBranch commits) {
try {
TREE_CONSTRUCTION_LOCK.writeLock().lock();
myRootNode.removeAllChildren();
createNodes(commits);
myTreeModel.nodeStructureChanged(myRootNode);
myTree.setModel(myTreeModel); // TODO: why doesn't it repaint otherwise?
TreeUtil.expandAll(myTree);
selectFirstCommit();
collapseEmptyRepoNodes(commits);
myTreeWasConstructed = true;
}
finally {
TREE_CONSTRUCTION_LOCK.writeLock().unlock();
}
}
private void selectFirstCommit() {
DefaultMutableTreeNode firstLeaf = myRootNode.getFirstLeaf();
if (firstLeaf == null) {
return;
}
Enumeration enumeration = myRootNode.depthFirstEnumeration();
DefaultMutableTreeNode node = null;
while (enumeration.hasMoreElements()) {
node = (DefaultMutableTreeNode) enumeration.nextElement();
if (node.isLeaf() && node.getUserObject() instanceof GitCommit) {
break;
}
}
if (node == null) {
node = firstLeaf;
}
myTree.setSelectionPath(new TreePath(node.getPath()));
}
private void collapseEmptyRepoNodes(GitCommitsByRepoAndBranch commits) {
Enumeration enumeration = myRootNode.breadthFirstEnumeration();
while (enumeration.hasMoreElements()) {
DefaultMutableTreeNode node = (DefaultMutableTreeNode) enumeration.nextElement();
Object userObject = node.getUserObject();
if (userObject == null) {
// root object => seeking further
}
else if (userObject instanceof GitRepository) {
if (commits.get((GitRepository)userObject).isEmpty()) {
myTree.collapsePath(new TreePath(node.getPath()));
}
}
else {
// we're looking to the breadthFirstEnumeration => all repository nodes have already been enumerated
return;
}
}
}
private void createNodes(@NotNull GitCommitsByRepoAndBranch commits) {
for (GitRepository repository : sortRepositories(commits)) {
GitCommitsByBranch commitsByBranch = commits.get(repository);
createRepoNode(repository, commitsByBranch, myRootNode);
}
}
@NotNull
private static List<GitRepository> sortRepositories(@NotNull final GitCommitsByRepoAndBranch commits) {
List<GitRepository> repos = new ArrayList<GitRepository>(commits.getRepositories());
Collections.sort(repos, new Comparator<GitRepository>() {
@Override public int compare(GitRepository r1, GitRepository r2) {
// empty repositories - to the end
if (commits.get(r1).isEmpty() && !commits.get(r2).isEmpty()) {
return 1;
}
if (commits.get(r2).isEmpty() && !commits.get(r1).isEmpty()) {
return -1;
}
return r1.getPresentableUrl().compareTo(r2.getPresentableUrl());
}
});
return repos;
}
/**
* Creates the node with subnodes for a repository and adds it to the rootNode.
* If there is only one repo in the project, doesn't create a node for the repository, and adds subnodes directly to the rootNode.
*/
private void createRepoNode(@NotNull GitRepository repository, @NotNull GitCommitsByBranch commitsByBranch,
@NotNull DefaultMutableTreeNode rootNode) {
DefaultMutableTreeNode parentNode;
if (GitUtil.justOneGitRepository(myProject)) {
parentNode = rootNode;
} else {
parentNode = new CheckedTreeNode(repository);
if (commitsByBranch.isEmpty()) {
((CheckedTreeNode)parentNode).setChecked(false);
}
rootNode.add(parentNode);
}
for (GitBranch branch : sortBranches(commitsByBranch.getBranches())) {
DefaultMutableTreeNode branchNode = createBranchNode(branch, commitsByBranch.get(branch));
parentNode.add(branchNode);
}
}
private static List<GitBranch> sortBranches(@NotNull Collection<GitBranch> branches) {
List<GitBranch> sortedBranches = new ArrayList<GitBranch>(branches);
Collections.sort(sortedBranches, new Comparator<GitBranch>() {
@Override public int compare(GitBranch o1, GitBranch o2) {
return o1.getName().compareTo(o2.getName());
}
});
return sortedBranches;
}
private static DefaultMutableTreeNode createBranchNode(@NotNull GitBranch branch, @NotNull GitPushBranchInfo branchInfo) {
DefaultMutableTreeNode branchNode = new DefaultMutableTreeNode(branchInfo);
for (GitCommit commit : branchInfo.getCommits()) {
branchNode.add(new DefaultMutableTreeNode(commit));
}
if (branchInfo.isNewBranchCreated()) {
branchNode.add(new DefaultMutableTreeNode(new MoreCommitsToShow()));
}
return branchNode;
}
void displayError(String message) {
DefaultMutableTreeNode titleNode = new DefaultMutableTreeNode("Error: couldn't collect commits to be pushed");
DefaultMutableTreeNode detailNode = new DefaultMutableTreeNode(message);
myRootNode.add(titleNode);
myRootNode.add(detailNode);
myTreeModel.reload(myRootNode);
TreeUtil.expandAll(myTree);
repaint();
}
/**
* @return repositories selected (via checkboxes) to be pushed.
*/
Collection<GitRepository> getSelectedRepositories() {
if (myAllRepositories.size() == 1) {
return myAllRepositories;
}
try {
TREE_CONSTRUCTION_LOCK.readLock().lock(); // wait for tree to be constructed
if (!myTreeWasConstructed) {
return myAllRepositories;
}
else {
Collection<GitRepository> selectedRepositories = new ArrayList<GitRepository>(myAllRepositories.size());
if (myRootNode.getChildCount() == 0) { // the method is requested before tree construction began => returning all repos.
return myAllRepositories;
}
for (int i = 0; i < myRootNode.getChildCount(); i++) {
TreeNode child = myRootNode.getChildAt(i);
if (child instanceof CheckedTreeNode) {
CheckedTreeNode node = (CheckedTreeNode)child;
if (node.isChecked()) {
if (node.getUserObject() instanceof GitRepository) {
selectedRepositories.add((GitRepository)node.getUserObject());
}
}
}
}
return selectedRepositories;
}
}
finally {
TREE_CONSTRUCTION_LOCK.readLock().unlock();
}
}
@NotNull
private static String getDateString(@NotNull GitCommit commit) {
return DvcsUtil.getDateString(commit);
}
@NotNull
private static String getHashString(@NotNull GitCommit commit) {
return DvcsUtil.getShortHash(commit.getId().toString());
}
private static class MyTreeCellRenderer extends CheckboxTree.CheckboxTreeCellRenderer {
@Override
public void customizeRenderer(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
Object userObject;
if (value instanceof CheckedTreeNode) {
userObject = ((CheckedTreeNode)value).getUserObject();
} else if (value instanceof DefaultMutableTreeNode) {
userObject = ((DefaultMutableTreeNode)value).getUserObject();
} else {
return;
}
ColoredTreeCellRenderer renderer = getTextRenderer();
if (userObject instanceof GitCommit) {
GitCommit commit = (GitCommit)userObject;
renderer.append(commit.getSubject(), new SimpleTextAttributes(SimpleTextAttributes.STYLE_SMALLER, getTextRenderer().getForeground()));
renderer.setToolTipText(getHashString(commit) + " " + getDateString(commit));
}
else if (userObject instanceof GitRepository) {
String repositoryPath = DvcsUtil.getShortRepositoryName((GitRepository)userObject);
renderer.append(repositoryPath, SimpleTextAttributes.GRAY_ATTRIBUTES);
}
else if (userObject instanceof GitPushBranchInfo) {
GitPushBranchInfo branchInfo = (GitPushBranchInfo) userObject;
GitBranch fromBranch = branchInfo.getSourceBranch();
GitBranch dest = branchInfo.getDestBranch();
GitPushBranchInfo.Type type = branchInfo.getType();
final String showingRecentCommits = ", showing " + GitPusher.RECENT_COMMITS_NUMBER + " recent commits";
String text = fromBranch.getName();
SimpleTextAttributes attrs = SimpleTextAttributes.REGULAR_ATTRIBUTES;
String additionalText = "";
switch (type) {
case STANDARD:
text += " -> " + dest.getName();
if (branchInfo.getCommits().isEmpty()) {
additionalText = " nothing to push";
}
break;
case NEW_BRANCH:
text += " -> +" + dest.getName();
attrs = SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES;
additionalText = " new branch will be created" + showingRecentCommits;
break;
case NO_TRACKED_OR_TARGET:
attrs = SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES;
additionalText = " no tracked branch. Use checkbox below to push branch to manually specified" + showingRecentCommits;
break;
}
renderer.append(text, attrs);
renderer.append(additionalText, new SimpleTextAttributes(SimpleTextAttributes.STYLE_SMALLER, UIUtil.getInactiveTextColor()));
}
else if (userObject instanceof FakeCommit) {
int spaces = 6 + 15 + 3 + 30;
String s = String.format("%" + spaces + "s", " ");
renderer.append(s, new SimpleTextAttributes(SimpleTextAttributes.STYLE_PLAIN, renderer.getBackground()));
}
else if (userObject instanceof MoreCommitsToShow) {
renderer.append("...");
}
else {
renderer.append(userObject == null ? "" : userObject.toString());
}
}
}
private static class FakeCommit {
}
private static class MoreCommitsToShow {
}
}
@@ -0,0 +1,77 @@
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package git4idea.push;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Push result as reported by {@code git push} command.
*
* @see GitPushNativeResultParser
* @see GitPushRepoResult
*/
class GitPushNativeResult {
enum Type {
SUCCESS,
FORCED_UPDATE,
NEW_REF,
REJECTED,
DELETED,
UP_TO_DATE,
ERROR
}
@NotNull private final Type myType;
@Nullable private final String myRange;
@Nullable private final String myErrorOutput;
private GitPushNativeResult(@NotNull Type type, @Nullable String range, @Nullable String errorOutput) {
myType = type;
myRange = range;
myErrorOutput = errorOutput;
}
public GitPushNativeResult(@NotNull Type type, @Nullable String range) {
this(type, range, null);
}
@NotNull
public static GitPushNativeResult error(@NotNull String errorOutput) {
return new GitPushNativeResult(Type.ERROR, null, errorOutput);
}
@NotNull
public Type getType() {
return myType;
}
@Nullable
public String getRange() {
return myRange;
}
@Nullable
public String getErrorOutput() {
return myErrorOutput;
}
@Override
public String toString() {
return String.format("%s: '%s', stderr:[%s]", myType, myRange, myErrorOutput);
}
}
@@ -0,0 +1,132 @@
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package git4idea.push;
import com.intellij.openapi.diagnostic.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Parses the output received from git push and returns a result.
* NB: It is assumed that only one ref is pushed => there is only one result in the output.
*
* Output format described by git-push man:
* <pre>
* The status of the push is output in tabular form, with each line representing the status of a single ref.
* If --porcelain is used, then each line of the output is of the form:
*
* &lt;flag&gt; \t &lt;from&gt;:&lt;to&gt; \t &lt;summary&gt; (&lt;reason&gt;)
*
* The status of up-to-date refs is shown only if --porcelain or --verbose option is used.
*
* flag
* A single character indicating the status of the ref:
* (space)
* for a successfully pushed fast-forward;
* +
* for a successful forced update;
* -
* for a successfully deleted ref;
* *
* for a successfully pushed new ref;
* !
* for a ref that was rejected or failed to push; and
* =
* for a ref that was up to date and did not need pushing.
*
* summary
* For a successfully pushed ref, the summary shows the old and new values of the ref in a form
* suitable for using as an argument to git log (this is <old>..<new> in most cases, and
* <old>...<new> for forced non-fast-forward updates).
*
* For a failed update, more details are given:
* rejected
* Git did not try to send the ref at all, typically because it is not a fast-forward and you
* did not force the update.
*
* remote rejected
* The remote end refused the update. Usually caused by a hook on the remote side, or because
* the remote repository has one of the following safety options in effect:
* receive.denyCurrentBranch (for pushes to the checked out branch), receive.denyNonFastForwards
* (for forced non-fast-forward updates), receive.denyDeletes or receive.denyDeleteCurrent. See
* git-config(1).
*
* remote failure
* The remote end did not report the successful update of the ref, perhaps because of a
* temporary error on the remote side, a break in the network connection, or other transient
* error.
*
* from
* The name of the local ref being pushed, minus its refs/<type>/ prefix. In the case of deletion,
* the name of the local ref is omitted.
*
* to
* The name of the remote ref being updated, minus its refs/<type>/ prefix.
*
* reason
* A human-readable explanation. In the case of successfully pushed refs, no explanation is needed.
* For a failed ref, the reason for failure is described.
* </pre>
*/
public class GitPushNativeResultParser {
private static final Logger LOG = Logger.getInstance(GitPushNativeResultParser.class);
private static final Pattern PATTERN = Pattern.compile("^.*([ +\\-\\*!=])\\s(\\S+):(\\S+)\\s(\\S+).*$");
private static final Pattern RANGE = Pattern.compile("[0-9a-f]+[\\.]{2,3}[0-9a-f]+");
@Nullable
public static GitPushNativeResult parse(@NotNull List<String> output) {
for (String line : output) {
Matcher matcher = PATTERN.matcher(line);
if (matcher.matches()) {
return parseRefResult(matcher, line);
}
}
return null;
}
@Nullable
private static GitPushNativeResult parseRefResult(Matcher matcher, String line) {
String flag = matcher.group(1);
String from = matcher.group(2);
String to = matcher.group(3);
String summary = matcher.group(4);
GitPushNativeResult.Type type = parseType(flag);
if (type == null) {
LOG.error("Couldn't parse push result type from flag [" + flag + "] in [" + line + "]");
return null;
}
String range = RANGE.matcher(summary).matches() ? summary : null;
return new GitPushNativeResult(type, range);
}
private static GitPushNativeResult.Type parseType(String flag) {
switch(flag.charAt(0)) {
case ' ' : return GitPushNativeResult.Type.SUCCESS;
case '+' : return GitPushNativeResult.Type.FORCED_UPDATE;
case '-' : return GitPushNativeResult.Type.DELETED;
case '*' : return GitPushNativeResult.Type.NEW_REF;
case '!' : return GitPushNativeResult.Type.REJECTED;
case '=' : return GitPushNativeResult.Type.UP_TO_DATE;
}
return null;
}
}
@@ -0,0 +1,370 @@
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package git4idea.push;
import com.intellij.dvcs.DvcsUtil;
import com.intellij.dvcs.push.PushSpec;
import com.intellij.history.Label;
import com.intellij.history.LocalHistory;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.EmptyProgressIndicator;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.update.UpdatedFiles;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.Function;
import com.intellij.util.ObjectUtils;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.ui.UIUtil;
import git4idea.*;
import git4idea.branch.GitBranchUtil;
import git4idea.commands.Git;
import git4idea.commands.GitCommandResult;
import git4idea.commands.GitLineHandlerListener;
import git4idea.commands.GitStandardProgressAnalyzer;
import git4idea.config.GitVcsSettings;
import git4idea.config.UpdateMethod;
import git4idea.history.GitHistoryUtils;
import git4idea.merge.MergeChangeCollector;
import git4idea.repo.GitBranchTrackInfo;
import git4idea.repo.GitRepository;
import git4idea.settings.GitPushSettings;
import git4idea.update.GitUpdateProcess;
import git4idea.update.GitUpdateResult;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
/**
* Executes git push operation:
* <ul>
* <li>Calls push for the given repositories with given parameters;</li>
* <li>Collects results;</li>
* <li>If push is rejected, proposes to update via merge or rebase;</li>
* <li>Shows a notification about push result</li>
* </ul>
*/
public class GitPushOperation {
private static final Logger LOG = Logger.getInstance(GitPushOperation.class);
private static final int MAX_PUSH_ATTEMPTS = 10;
private final Project myProject;
private final Map<GitRepository, PushSpec<GitPushSource, GitPushTarget>> myPushSpecs;
private final boolean myForce;
private final Git myGit;
private final ProgressIndicator myProgressIndicator;
private final GitVcsSettings mySettings;
private final GitPushSettings myPushSettings;
private final GitPlatformFacade myPlatformFacade;
public GitPushOperation(Project project, Map<GitRepository, PushSpec<GitPushSource, GitPushTarget>> pushSpecs, boolean force) {
myProject = project;
myPushSpecs = pushSpecs;
myForce = force;
myGit = ServiceManager.getService(Git.class);
myProgressIndicator = ObjectUtils.notNull(ProgressManager.getInstance().getProgressIndicator(), new EmptyProgressIndicator());
mySettings = GitVcsSettings.getInstance(myProject);
myPushSettings = GitPushSettings.getInstance(myProject);
myPlatformFacade = ServiceManager.getService(project, GitPlatformFacade.class);
Map<GitRepository, GitRevisionNumber> currentHeads = ContainerUtil.newHashMap();
for (GitRepository repository : pushSpecs.keySet()) {
repository.update();
String head = repository.getCurrentRevision();
if (head == null) {
LOG.error("This repository has no commits");
}
else {
currentHeads.put(repository, new GitRevisionNumber(head));
}
}
}
@NotNull
public GitPushResult execute() {
PushUpdateSettings updateSettings = readPushUpdateSettings();
Label beforePushLabel = null;
Label afterPushLabel = null;
Map<GitRepository, String> preUpdatePositions = updateRootInfoAndRememberPositions();
final Map<GitRepository, GitPushRepoResult> results = ContainerUtil.newHashMap();
Map<GitRepository, GitUpdateResult> updatedRoots = ContainerUtil.newHashMap();
try {
Collection<GitRepository> remainingRoots = myPushSpecs.keySet();
for (int pushAttempt = 0;
pushAttempt < MAX_PUSH_ATTEMPTS && !remainingRoots.isEmpty();
pushAttempt++, remainingRoots = getRejectedAndNotPushed(results)) {
Map<GitRepository, GitPushRepoResult> resultMap = push(remainingRoots);
results.putAll(resultMap);
GroupedPushResult result = GroupedPushResult.group(resultMap);
// stop on first error
if (!result.errors.isEmpty()) {
break;
}
// propose to update if rejected
if (!result.rejected.isEmpty()) {
boolean shouldUpdate = true;
if (pushingToNotTrackedBranch(result.rejected)) {
shouldUpdate = false;
}
else if (pushAttempt == 0 && !mySettings.autoUpdateIfPushRejected()) {
updateSettings = showDialogAndGetExitCode(result.rejected.keySet(), updateSettings);
if (updateSettings != null) {
savePushUpdateSettings(updateSettings);
}
else {
shouldUpdate = false;
}
}
if (!shouldUpdate) {
break;
}
if (beforePushLabel == null) { // put the label only before the very first update
beforePushLabel = LocalHistory.getInstance().putSystemLabel(myProject, "Before push");
}
Collection<GitRepository> rootsToUpdate = updateSettings.shouldUpdateAllRoots() ?
myPushSpecs.keySet() :
result.rejected.keySet();
GitUpdateResult updateResult = update(rootsToUpdate, updateSettings.getUpdateMethod());
for (GitRepository repository : rootsToUpdate) {
updatedRoots.put(repository, updateResult); // TODO update result in GitUpdateProcess is a single for several roots
}
if (!updateResult.isSuccess() ||
updateResult == GitUpdateResult.SUCCESS_WITH_RESOLVED_CONFLICTS || updateResult == GitUpdateResult.INCOMPLETE) {
break;
}
}
}
}
finally {
if (beforePushLabel != null) {
afterPushLabel = LocalHistory.getInstance().putSystemLabel(myProject, "After push");
}
}
return prepareCombinedResult(results, updatedRoots, preUpdatePositions, beforePushLabel, afterPushLabel);
}
private static boolean pushingToNotTrackedBranch(@NotNull Map<GitRepository, GitPushRepoResult> rejected) {
return ContainerUtil.exists(rejected.entrySet(), new Condition<Map.Entry<GitRepository, GitPushRepoResult>>() {
@Override
public boolean value(Map.Entry<GitRepository, GitPushRepoResult> entry) {
GitRepository repository = entry.getKey();
GitLocalBranch currentBranch = repository.getCurrentBranch();
assert currentBranch != null;
GitBranchTrackInfo trackInfo = GitBranchUtil.getTrackInfoForBranch(repository, currentBranch);
return trackInfo == null || !trackInfo.getRemoteBranch().getFullName().equals(entry.getValue().getTargetBranch());
}
});
}
@NotNull
private static List<GitRepository> getRejectedAndNotPushed(@NotNull final Map<GitRepository, GitPushRepoResult> results) {
return ContainerUtil.filter(results.keySet(), new Condition<GitRepository>() {
@Override
public boolean value(GitRepository repository) {
return results.get(repository).getType() == GitPushRepoResult.Type.REJECTED ||
results.get(repository).getType() == GitPushRepoResult.Type.NOT_PUSHED;
}
});
}
@NotNull
private Map<GitRepository, String> updateRootInfoAndRememberPositions() {
return ContainerUtil.map2Map(myPushSpecs.keySet(),
new Function<GitRepository, Pair<GitRepository, String>>() {
@Override
public Pair<GitRepository, String> fun(GitRepository repository) {
repository.update();
return Pair.create(repository, repository.getCurrentRevision());
}
});
}
private GitPushResult prepareCombinedResult(final Map<GitRepository, GitPushRepoResult> allRoots,
final Map<GitRepository, GitUpdateResult> updatedRoots,
final Map<GitRepository, String> preUpdatePositions,
Label beforeUpdateLabel,
Label afterUpdateLabel) {
Map<GitRepository, GitPushRepoResult> results = ContainerUtil.newHashMap();
UpdatedFiles updatedFiles = UpdatedFiles.create();
for (Map.Entry<GitRepository, GitPushRepoResult> entry : allRoots.entrySet()) {
GitRepository repository = entry.getKey();
GitPushRepoResult simpleResult = entry.getValue();
GitUpdateResult updateResult = updatedRoots.get(repository);
if (updateResult == null) {
results.put(repository, simpleResult);
}
else {
collectUpdatedFiles(updatedFiles, repository, preUpdatePositions.get(repository));
results.put(repository, GitPushRepoResult.addUpdateResult(simpleResult, updateResult));
}
}
return new GitPushResult(results, updatedFiles, beforeUpdateLabel, afterUpdateLabel);
}
@NotNull
private Map<GitRepository, GitPushRepoResult> push(@NotNull Collection<GitRepository> repositories) {
Map<GitRepository, GitPushRepoResult> results = ContainerUtil.newLinkedHashMap();
for (GitRepository repository : repositories) {
PushSpec<GitPushSource, GitPushTarget> spec = myPushSpecs.get(repository);
GitPushNativeResult result = doPush(repository, spec);
LOG.debug("Pushed to " + DvcsUtil.getShortRepositoryName(repository) + ": " + result);
int commits = collectNumberOfPushedCommits(repository.getRoot(), result);
GitPushTarget target = spec.getTarget();
assert target != null;
GitPushRepoResult repoResult = GitPushRepoResult.convertFromNative(result, commits, spec.getSource().getBranch(), target.getBranch());
LOG.debug("Converted result: " + repoResult);
results.put(repository, repoResult);
}
// fill other not-processed repositories as not-pushed
for (GitRepository repository : repositories) {
if (!results.containsKey(repository)) {
PushSpec<GitPushSource, GitPushTarget> spec = myPushSpecs.get(repository);
results.put(repository, GitPushRepoResult.notPushed(spec.getSource().getBranch(), spec.getTarget().getBranch()));
}
}
return results;
}
private int collectNumberOfPushedCommits(@NotNull VirtualFile root, @NotNull GitPushNativeResult result) {
if (result.getType() != GitPushNativeResult.Type.SUCCESS) {
return -1;
}
String range = result.getRange();
if (range == null) {
LOG.error("Range of pushed commits not reported in " + result);
return -1;
}
try {
return GitHistoryUtils.history(myProject, root, range).size();
}
catch (VcsException e) {
LOG.error("Couldn't collect commits from range " + range);
return -1;
}
}
private void collectUpdatedFiles(@NotNull UpdatedFiles updatedFiles, @NotNull GitRepository repository,
@NotNull String preUpdatePosition) {
MergeChangeCollector collector = new MergeChangeCollector(myProject, repository.getRoot(), new GitRevisionNumber(preUpdatePosition));
ArrayList<VcsException> exceptions = new ArrayList<VcsException>();
collector.collect(updatedFiles, exceptions);
for (VcsException exception : exceptions) {
LOG.info(exception);
}
}
@NotNull
private GitPushNativeResult doPush(@NotNull GitRepository repository, @NotNull PushSpec<GitPushSource, GitPushTarget> pushSpec) {
GitPushTarget target = pushSpec.getTarget();
assert target != null;
GitLocalBranch sourceBranch = pushSpec.getSource().getBranch();
GitRemoteBranch targetBranch = target.getBranch();
GitLineHandlerListener progressListener = GitStandardProgressAnalyzer.createListener(myProgressIndicator);
boolean setUpstream = pushSpec.getTarget().isNewBranchCreated() && !branchTrackingInfoIsSet(repository, sourceBranch);
GitCommandResult res = myGit.push(repository, sourceBranch, targetBranch, myForce, setUpstream, progressListener);
GitPushNativeResult result = GitPushNativeResultParser.parse(res.getOutput());
if (result == null) {
result = GitPushNativeResult.error(res.getErrorOutputAsJoinedString());
}
return result;
}
private static boolean branchTrackingInfoIsSet(@NotNull GitRepository repository, @NotNull final GitLocalBranch source) {
return ContainerUtil.exists(repository.getBranchTrackInfos(), new Condition<GitBranchTrackInfo>() {
@Override
public boolean value(GitBranchTrackInfo info) {
return info.getLocalBranch().equals(source);
}
});
}
private void savePushUpdateSettings(@NotNull PushUpdateSettings PushUpdateSettings) {
UpdateMethod updateMethod = PushUpdateSettings.getUpdateMethod();
myPushSettings.setUpdateAllRoots(PushUpdateSettings.shouldUpdateAllRoots());
myPushSettings.setUpdateMethod(updateMethod);
}
@NotNull
private PushUpdateSettings readPushUpdateSettings() {
boolean updateAllRoots = myPushSettings.shouldUpdateAllRoots();
UpdateMethod updateMethod = myPushSettings.getUpdateMethod();
return new PushUpdateSettings(updateAllRoots, updateMethod);
}
@Nullable
private PushUpdateSettings showDialogAndGetExitCode(@NotNull final Set<GitRepository> repositories,
@NotNull final PushUpdateSettings initialSettings) {
final Ref<PushUpdateSettings> updateSettings = Ref.create();
UIUtil.invokeAndWaitIfNeeded(new Runnable() {
@Override
public void run() {
GitRejectedPushUpdateDialog dialog = new GitRejectedPushUpdateDialog(myProject, repositories, initialSettings);
DialogManager.show(dialog);
int exitCode = dialog.getExitCode();
if (exitCode != DialogWrapper.CANCEL_EXIT_CODE) {
mySettings.setAutoUpdateIfPushRejected(dialog.shouldAutoUpdateInFuture());
updateSettings.set(new PushUpdateSettings(dialog.shouldUpdateAll(), convertUpdateMethodFromDialogExitCode(exitCode)));
}
}
});
return updateSettings.get();
}
@NotNull
private static UpdateMethod convertUpdateMethodFromDialogExitCode(int exitCode) {
switch (exitCode) {
case GitRejectedPushUpdateDialog.MERGE_EXIT_CODE: return UpdateMethod.MERGE;
case GitRejectedPushUpdateDialog.REBASE_EXIT_CODE: return UpdateMethod.REBASE;
default: throw new IllegalStateException("Unexpected exit code: " + exitCode);
}
}
@NotNull
protected GitUpdateResult update(@NotNull Collection<GitRepository> rootsToUpdate, @NotNull UpdateMethod updateMethod) {
GitUpdateProcess.UpdateMethod um = updateMethod == UpdateMethod.MERGE ?
GitUpdateProcess.UpdateMethod.MERGE :
GitUpdateProcess.UpdateMethod.REBASE;
GitUpdateResult updateResult = new GitUpdateProcess(myProject, myPlatformFacade, myProgressIndicator,
new HashSet<GitRepository>(rootsToUpdate), UpdatedFiles.create()).update(um);
for (GitRepository repository : rootsToUpdate) {
repository.getRoot().refresh(true, true);
repository.update();
}
return updateResult;
}
}
@@ -1,74 +0,0 @@
/*
* Copyright 2000-2011 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package git4idea.push;
import com.intellij.openapi.util.Key;
import git4idea.commands.GitLineHandlerListener;
import java.util.ArrayList;
import java.util.Collection;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author Kirill Likhodedov
*/
class GitPushRejectedDetector implements GitLineHandlerListener {
private static final Pattern REJECTED_PATTERN = Pattern.compile("\\s+! \\[rejected\\]\\s+(\\S+) -> (\\S+) .*");
private final Collection<RejectedRef> myRejectedRefs = new ArrayList<RejectedRef>();
@Override
public void onLineAvailable(String line, Key outputType) {
Matcher matcher = REJECTED_PATTERN.matcher(line);
if (matcher.matches()) {
String src = matcher.group(1);
String dst = matcher.group(2);
myRejectedRefs.add(new RejectedRef(src, dst));
}
}
@Override
public void processTerminated(int exitCode) {
}
@Override
public void startFailed(Throwable exception) {
}
public boolean rejected() {
return !myRejectedRefs.isEmpty();
}
public Collection<String> getRejectedBranches() {
Collection<String> branches = new ArrayList<String>(myRejectedRefs.size());
for (RejectedRef rejectedRef : myRejectedRefs) {
branches.add(rejectedRef.mySource);
}
return branches;
}
static class RejectedRef {
private final String mySource;
private final String myDestination;
RejectedRef(String source, String destination) {
myDestination = destination;
mySource = source;
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2000-2011 JetBrains s.r.o.
* Copyright 2000-2014 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.
@@ -15,150 +15,134 @@
*/
package git4idea.push;
import com.intellij.openapi.util.text.StringUtil;
import git4idea.GitBranch;
import git4idea.GitLocalBranch;
import git4idea.GitRemoteBranch;
import git4idea.update.GitUpdateResult;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import static git4idea.util.GitUIUtil.bold;
import static git4idea.util.GitUIUtil.code;
import java.util.Comparator;
/**
* If an error happens, all push is unsuccessful, for all branches.
* Otherwise we've got separate results for branches.
* Result of pushing one repository.
* <p/>
* Includes information about the number of pushed commits (or -1 if undefined),
* and tells whether the repository was updated after the push was rejected.
*
* @see git4idea.push.GitPushNativeResult
*/
final class GitPushRepoResult {
class GitPushRepoResult {
enum Type {
NOT_PUSHING,
SUCCESS,
SOME_REJECTED,
NEW_BRANCH,
FORCED,
REJECTED,
ERROR,
CANCEL,
NOT_AUTHORIZED
NOT_PUSHED;
}
private final Type myType;
private final String myOutput;
private final Map<GitBranch, GitPushBranchResult> myBranchResults;
static Comparator<Type> TYPE_COMPARATOR = new Comparator<Type>() {
@Override
public int compare(Type o1, Type o2) {
return o1.ordinal() - o2.ordinal();
}
};
GitPushRepoResult(@NotNull Type type, @NotNull Map<GitBranch, GitPushBranchResult> resultsByBranch, @NotNull String output) {
@NotNull private final Type myType;
private final int myCommits;
@NotNull private final String mySourceBranch;
@NotNull private final String myTargetBranch;
@Nullable private final String myError;
@Nullable private final GitUpdateResult myUpdateResult;
@NotNull
static GitPushRepoResult convertFromNative(@NotNull GitPushNativeResult result,
int commits, @NotNull GitLocalBranch source, @NotNull GitRemoteBranch target) {
return new GitPushRepoResult(convertType(result.getType()), commits, source.getFullName(), target.getFullName(),
result.getErrorOutput(), null);
}
@NotNull
static GitPushRepoResult notPushed(GitLocalBranch source, GitRemoteBranch target) {
return new GitPushRepoResult(Type.NOT_PUSHED, -1, source.getFullName(), target.getFullName(), null, null);
}
@NotNull
static GitPushRepoResult addUpdateResult(GitPushRepoResult original, GitUpdateResult updateResult) {
return new GitPushRepoResult(original.getType(), original.getNumberOfPushedCommits(), original.getSourceBranch(),
original.getTargetBranch(), original.getError(), updateResult);
}
private GitPushRepoResult(@NotNull Type type, int pushedCommits, @NotNull String sourceBranch, @NotNull String targetBranch,
@Nullable String error, @Nullable GitUpdateResult result) {
myType = type;
myBranchResults = resultsByBranch;
myOutput = output;
}
@NotNull
static GitPushRepoResult success(@NotNull Map<GitBranch, GitPushBranchResult> resultsByBranch, @NotNull String output) {
return new GitPushRepoResult(Type.SUCCESS, resultsByBranch, output);
}
@NotNull
static GitPushRepoResult error(@NotNull Map<GitBranch, GitPushBranchResult> resultsByBranch, @NotNull String output) {
return new GitPushRepoResult(Type.ERROR, resultsByBranch, output);
}
@NotNull
static GitPushRepoResult someRejected(@NotNull Map<GitBranch, GitPushBranchResult> resultsByBranch, @NotNull String output) {
return new GitPushRepoResult(Type.SOME_REJECTED, resultsByBranch, output);
}
@NotNull
public static GitPushRepoResult cancelled(@NotNull String output) {
return new GitPushRepoResult(Type.CANCEL, Collections.<GitBranch, GitPushBranchResult>emptyMap(), output);
}
@NotNull
public static GitPushRepoResult notAuthorized(@NotNull String output) {
return new GitPushRepoResult(Type.NOT_AUTHORIZED, Collections.<GitBranch, GitPushBranchResult>emptyMap(), output);
}
@NotNull
static GitPushRepoResult notPushed() {
return new GitPushRepoResult(Type.NOT_PUSHING, Collections.<GitBranch, GitPushBranchResult>emptyMap(), "");
myCommits = pushedCommits;
mySourceBranch = sourceBranch;
myTargetBranch = targetBranch;
myError = error;
myUpdateResult = result;
}
@NotNull
Type getType() {
return myType;
}
boolean isOneOfErrors() {
return myType == Type.ERROR || myType == Type.CANCEL || myType == Type.NOT_AUTHORIZED;
@Nullable
GitUpdateResult getUpdateResult() {
return myUpdateResult;
}
@NotNull
String getOutput() {
return myOutput;
}
@NotNull
Map<GitBranch, GitPushBranchResult> getBranchResults() {
return myBranchResults;
}
@NotNull
GitPushRepoResult remove(@NotNull GitBranch branch) {
Map<GitBranch, GitPushBranchResult> resultsByBranch = new HashMap<GitBranch, GitPushBranchResult>();
for (Map.Entry<GitBranch, GitPushBranchResult> entry : myBranchResults.entrySet()) {
GitBranch b = entry.getKey();
if (!b.equals(branch)) {
resultsByBranch.put(b, entry.getValue());
}
}
return new GitPushRepoResult(myType, resultsByBranch, myOutput);
}
boolean isEmpty() {
return myBranchResults.isEmpty();
int getNumberOfPushedCommits() {
return myCommits;
}
/**
* Merges the given results to this result.
* In the case of conflict (i.e. different results for a branch), current result is preferred over the previous one.
* Returns the branch we were pushing from, in the full-name format, e.g. {@code refs/heads/master}.
*/
void mergeFrom(@NotNull GitPushRepoResult repoResult) {
for (Map.Entry<GitBranch, GitPushBranchResult> entry : repoResult.myBranchResults.entrySet()) {
GitBranch branch = entry.getKey();
GitPushBranchResult branchResult = entry.getValue();
if (!myBranchResults.containsKey(branch)) { // otherwise current result is preferred
myBranchResults.put(branch, branchResult);
}
}
@NotNull
String getSourceBranch() {
return mySourceBranch;
}
/**
* Returns the branch we were pushing to, in the full-name format, e.g. {@code refs/remotes/origin/master}.
*/
@NotNull
String getTargetBranch() {
return myTargetBranch;
}
@Nullable
String getError() {
return myError;
}
@NotNull
String getPerBranchesNonErrorReport() {
StringBuilder sb = new StringBuilder();
int i = 0;
for (Map.Entry<GitBranch, GitPushBranchResult> entry : myBranchResults.entrySet()) {
GitBranch branch = entry.getKey();
GitPushBranchResult branchResult = entry.getValue();
if (branchResult.isSuccess()) {
sb.append(bold(branch.getName()) + ": pushed " + commits(branchResult.getNumberOfPushedCommits()));
}
else if (branchResult.isNewBranch()) {
sb.append(bold(branch.getName()) + " pushed to new branch " + bold(branchResult.getTargetBranchName()));
}
else {
sb.append(code(branch.getName())).append(": rejected");
}
if (i < myBranchResults.size() - 1) {
sb.append("<br/>");
}
private static Type convertType(@NotNull GitPushNativeResult.Type nativeType) {
switch (nativeType) {
case SUCCESS:
return Type.SUCCESS;
case FORCED_UPDATE:
return Type.FORCED;
case NEW_REF:
return Type.NEW_BRANCH;
case REJECTED:
return Type.REJECTED;
case UP_TO_DATE:
return Type.SUCCESS;
case ERROR:
return Type.ERROR;
case DELETED:
default:
throw new IllegalArgumentException("Conversion is not supported: " + nativeType);
}
return sb.toString();
}
@NotNull
private static String commits(int commitNum) {
return commitNum + " " + StringUtil.pluralize("commit", commitNum);
@Override
public String toString() {
return String.format("%s (%d, '%s'), update: %s}", myType, myCommits, mySourceBranch, myUpdateResult);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2000-2011 JetBrains s.r.o.
* Copyright 2000-2014 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.
@@ -16,367 +16,50 @@
package git4idea.push;
import com.intellij.history.Label;
import com.intellij.history.LocalHistory;
import com.intellij.ide.BrowserUtil;
import com.intellij.notification.Notification;
import com.intellij.notification.NotificationListener;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.VcsNotifier;
import com.intellij.openapi.vcs.ex.ProjectLevelVcsManagerEx;
import com.intellij.openapi.vcs.update.ActionInfo;
import com.intellij.openapi.vcs.update.UpdateInfoTree;
import com.intellij.openapi.vcs.update.UpdatedFiles;
import git4idea.GitBranch;
import git4idea.GitRevisionNumber;
import git4idea.GitUtil;
import git4idea.branch.GitBranchUtil;
import git4idea.merge.MergeChangeCollector;
import git4idea.repo.GitRepository;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.event.HyperlinkEvent;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
/**
* Combined push result for all affected repositories in the project.
*/
class GitPushResult {
private static final Logger LOG = Logger.getInstance(GitPushResult.class);
@NotNull private final Map<GitRepository, GitPushRepoResult> myResults;
@NotNull private final UpdatedFiles myUpdatedFiles;
@Nullable private final Label myBeforeUpdateLabel;
@Nullable private final Label myAfterUpdateLabel;
private final Project myProject;
private final Map<GitRepository, GitPushRepoResult> myResults = new HashMap<GitRepository, GitPushRepoResult>();
private final Map<GitRepository, GitRevisionNumber> myUpdateStarts = new HashMap<GitRepository, GitRevisionNumber>();
private Label myBeforeUpdateLabel;
GitPushResult(@NotNull Project project) {
this(project, null, new HashMap<GitRepository, GitRevisionNumber>());
}
private GitPushResult(@NotNull Project project, @Nullable Label beforeUpdateLabel, @NotNull Map<GitRepository, GitRevisionNumber> updateStarts) {
myProject = project;
myBeforeUpdateLabel = beforeUpdateLabel;
for (Map.Entry<GitRepository, GitRevisionNumber> entry : updateStarts.entrySet()) {
myUpdateStarts.put(entry.getKey(), entry.getValue());
}
}
void append(@NotNull GitRepository repository, @NotNull GitPushRepoResult result) {
myResults.put(repository, result);
}
/**
* For the specified repositories remembers the revision when update process (actually, auto-update inside the push process) has started.
*/
void markUpdateStartIfNotMarked(@NotNull Collection<GitRepository> repositories) {
if (myUpdateStarts.isEmpty()) {
myBeforeUpdateLabel = LocalHistory.getInstance().putSystemLabel(myProject, "Before push");
}
for (GitRepository repository : repositories) {
if (!myUpdateStarts.containsKey(repository)) {
String currentRevision = repository.getCurrentRevision();
if (currentRevision != null) {
myUpdateStarts.put(repository, new GitRevisionNumber(currentRevision));
}
}
}
}
boolean wasErrorCancelOrNotAuthorized() {
for (GitPushRepoResult repoResult : myResults.values()) {
if (repoResult.isOneOfErrors()) {
return true;
}
}
return false;
GitPushResult(@NotNull Map<GitRepository, GitPushRepoResult> results,
@NotNull UpdatedFiles files,
@Nullable Label beforeUpdateLabel,
@Nullable Label afterUpdateLabel) {
myResults = results;
myUpdatedFiles = files;
myAfterUpdateLabel = beforeUpdateLabel;
myBeforeUpdateLabel = afterUpdateLabel;
}
@NotNull
private GroupedResult group() {
final Map<GitRepository, GitPushRepoResult> successfulResults = new HashMap<GitRepository, GitPushRepoResult>();
final Map<GitRepository, GitPushRepoResult> errorResults = new HashMap<GitRepository, GitPushRepoResult>();
final Map<GitRepository, GitPushRepoResult> rejectedResults = new HashMap<GitRepository, GitPushRepoResult>();
for (Map.Entry<GitRepository, GitPushRepoResult> entry : myResults.entrySet()) {
GitRepository repository = entry.getKey();
GitPushRepoResult repoResult = entry.getValue();
switch (repoResult.getType()) {
case SUCCESS:
successfulResults.put(repository, repoResult);
break;
case ERROR:
case CANCEL:
case NOT_AUTHORIZED:
errorResults.put(repository, repoResult);
break;
case NOT_PUSHING:
break;
case SOME_REJECTED:
rejectedResults.put(repository, repoResult);
break;
}
}
return new GroupedResult(successfulResults, errorResults, rejectedResults);
}
boolean isEmpty() {
return myResults.isEmpty();
public Map<GitRepository, GitPushRepoResult> getResults() {
return myResults;
}
@NotNull
GitPushResult remove(@NotNull Map<GitRepository, GitBranch> repoBranchPairs) {
GitPushResult result = new GitPushResult(myProject, myBeforeUpdateLabel, myUpdateStarts);
for (Map.Entry<GitRepository, GitPushRepoResult> entry : myResults.entrySet()) {
GitRepository repository = entry.getKey();
GitPushRepoResult repoResult = entry.getValue();
if (repoBranchPairs.containsKey(repository)) {
GitPushRepoResult adjustedResult = repoResult.remove(repoBranchPairs.get(repository));
if (!repoResult.isEmpty()) {
result.append(repository, adjustedResult);
}
} else {
result.append(repository, repoResult);
}
}
return result;
public UpdatedFiles getUpdatedFiles() {
return myUpdatedFiles;
}
/**
* Merges the given results to this result.
* In the case of conflict (i.e. different results for a repository-branch pair), current result is preferred over the previous one.
*/
void mergeFrom(@Nullable GitPushResult previousResult) {
if (previousResult == null) {
return;
}
for (Map.Entry<GitRepository, GitPushRepoResult> entry : previousResult.myResults.entrySet()) {
GitRepository repository = entry.getKey();
GitPushRepoResult repoResult = entry.getValue();
if (myResults.containsKey(repository)) {
myResults.get(repository).mergeFrom(previousResult.myResults.get(repository));
} else {
append(repository, repoResult);
}
}
for (Map.Entry<GitRepository, GitRevisionNumber> entry : previousResult.myUpdateStarts.entrySet()) {
myUpdateStarts.put(entry.getKey(), entry.getValue());
}
myBeforeUpdateLabel = previousResult.myBeforeUpdateLabel;
@Nullable
public Label getBeforeUpdateLabel() {
return myBeforeUpdateLabel;
}
@NotNull
Map<GitRepository, GitBranch> getRejectedPushesFromCurrentBranchToTrackedBranch(GitPushInfo pushInfo) {
final Map<GitRepository, GitBranch> rejectedPushesForCurrentBranch = new HashMap<GitRepository, GitBranch>();
for (Map.Entry<GitRepository, GitPushRepoResult> entry : group().myRejectedResults.entrySet()) {
GitRepository repository = entry.getKey();
GitBranch currentBranch = repository.getCurrentBranch();
if (currentBranch == null) {
continue;
}
GitPushRepoResult repoResult = entry.getValue();
GitPushBranchResult curBranchResult = repoResult.getBranchResults().get(currentBranch);
if (curBranchResult == null) {
continue;
}
String trackedBranchName;
try {
String simpleName = GitBranchUtil.getTrackedBranchName(myProject, repository.getRoot(), currentBranch.getName());
if (simpleName == null) {
continue;
}
if (simpleName.startsWith(GitBranch.REFS_HEADS_PREFIX)) {
simpleName = simpleName.substring(GitBranch.REFS_HEADS_PREFIX.length());
}
String remote = GitBranchUtil.getTrackedRemoteName(myProject, repository.getRoot(), currentBranch.getName());
if (remote == null) {
continue;
}
trackedBranchName = remote + "/" + simpleName;
}
catch (VcsException e) {
LOG.info("Couldn't get tracked branch for branch " + currentBranch, e);
continue;
}
if (!pushInfo.getPushSpecs().get(repository).getDest().getName().equals(trackedBranchName)) {
// push from current branch was rejected, but it was a push not to the tracked branch => ignore
continue;
}
if (curBranchResult.isRejected()) {
rejectedPushesForCurrentBranch.put(repository, currentBranch);
}
}
return rejectedPushesForCurrentBranch;
}
/**
* Constructs the HTML-formatted message from error outputs of failed repositories and notify.
* If there is only 1 repository in the project, just returns the error without writing the repository url (to avoid confusion for people
* with only 1 root ever).
* Otherwise adds repository URL to the error that repository produced.
*
* The procedure also includes collecting for updated files (if an auto-update was performed during the push), which may be lengthy.
*/
void createPushNotificationAndNotify() {
final UpdatedFiles updatedFiles = collectUpdatedFiles();
GroupedResult groupedResult = group();
boolean error = !groupedResult.myErrorResults.isEmpty();
boolean rejected = !groupedResult.myRejectedResults.isEmpty();
boolean success = !groupedResult.mySuccessfulResults.isEmpty();
boolean onlyError = error && !rejected && !success;
boolean onlyRejected = rejected && !error && !success;
final boolean onlySuccess = success && !rejected && !error;
int pushedCommitsNumber = calcPushedCommitTotalNumber(myResults);
String title;
if (error) {
if (onlyError) {
title = "Push failed";
} else {
title = "Push partially failed";
if (success) {
title += ", " + commits(pushedCommitsNumber) + " pushed";
}
}
} else if (rejected) {
if (onlyRejected) {
title = "Push rejected";
} else {
title = "Push partially rejected, " + commits(pushedCommitsNumber) + " pushed";
}
} else {
title = "Push successful";
}
String errorReport = reportForGroup(groupedResult.myErrorResults, GroupedResult.Type.ERROR);
String successReport = reportForGroup(groupedResult.mySuccessfulResults, GroupedResult.Type.SUCCESS);
String rejectedReport = reportForGroup(groupedResult.myRejectedResults, GroupedResult.Type.REJECT);
StringBuilder sb = new StringBuilder();
sb.append(errorReport);
sb.append(rejectedReport);
sb.append(successReport);
if (!updatedFiles.isEmpty()) {
sb.append("<a href='UpdatedFiles'>View files updated during the push</a>");
}
NotificationListener viewUpdateFilesListener = new ViewUpdatedFilesNotificationListener(updatedFiles);
VcsNotifier vcsNotifier = VcsNotifier.getInstance(myProject);
if (onlySuccess) {
vcsNotifier.notifySuccess(title, sb.toString(), viewUpdateFilesListener);
}
else if (error) {
vcsNotifier.notifyError(title, sb.toString(), viewUpdateFilesListener);
}
else {
vcsNotifier.notifyImportantWarning(title, sb.toString(), viewUpdateFilesListener);
}
}
@NotNull
private UpdatedFiles collectUpdatedFiles() {
UpdatedFiles updatedFiles = UpdatedFiles.create();
for (Map.Entry<GitRepository, GitRevisionNumber> updatedRepository : myUpdateStarts.entrySet()) {
GitRepository repository = updatedRepository.getKey();
final MergeChangeCollector collector = new MergeChangeCollector(myProject, repository.getRoot(), updatedRepository.getValue());
final ArrayList<VcsException> exceptions = new ArrayList<VcsException>();
collector.collect(updatedFiles, exceptions);
for (VcsException exception : exceptions) {
LOG.info(exception);
}
}
return updatedFiles;
}
private static int calcPushedCommitTotalNumber(@NotNull Map<GitRepository, GitPushRepoResult> successfulResults) {
int sum = 0;
for (GitPushRepoResult pushRepoResult : successfulResults.values()) {
for (GitPushBranchResult branchResult : pushRepoResult.getBranchResults().values()) {
sum += branchResult.getNumberOfPushedCommits();
}
}
return sum;
}
@NotNull
private String reportForGroup(@NotNull Map<GitRepository, GitPushRepoResult> groupResult, @NotNull GroupedResult.Type resultType) {
StringBuilder sb = new StringBuilder();
for (Map.Entry<GitRepository, GitPushRepoResult> entry : groupResult.entrySet()) {
GitRepository repository = entry.getKey();
GitPushRepoResult result = entry.getValue();
if (!GitUtil.justOneGitRepository(myProject)) {
sb.append("<code>" + repository.getPresentableUrl() + "</code>:<br/>");
}
if (resultType == GroupedResult.Type.SUCCESS || resultType == GroupedResult.Type.REJECT) {
sb.append(result.getPerBranchesNonErrorReport());
} else {
sb.append(result.getOutput());
}
sb.append("<br/>");
}
return sb.toString();
}
private static class GroupedResult {
enum Type {
SUCCESS,
REJECT,
ERROR
}
private final Map<GitRepository, GitPushRepoResult> mySuccessfulResults;
private final Map<GitRepository, GitPushRepoResult> myErrorResults;
private final Map<GitRepository, GitPushRepoResult> myRejectedResults;
GroupedResult(@NotNull Map<GitRepository, GitPushRepoResult> successfulResults,
@NotNull Map<GitRepository, GitPushRepoResult> errorResults,
@NotNull Map<GitRepository, GitPushRepoResult> rejectedResults) {
mySuccessfulResults = successfulResults;
myErrorResults = errorResults;
myRejectedResults = rejectedResults;
}
}
@NotNull
private static String commits(int commitNum) {
return commitNum + " " + StringUtil.pluralize("commit", commitNum);
}
private class ViewUpdatedFilesNotificationListener implements NotificationListener {
private final UpdatedFiles myUpdatedFiles;
public ViewUpdatedFilesNotificationListener(UpdatedFiles updatedFiles) {
myUpdatedFiles = updatedFiles;
}
@Override
public void hyperlinkUpdate(@NotNull Notification notification, @NotNull HyperlinkEvent event) {
if (event.getEventType().equals(HyperlinkEvent.EventType.ACTIVATED)) {
if (event.getDescription().equals("UpdatedFiles")) {
ProjectLevelVcsManagerEx vcsManager = ProjectLevelVcsManagerEx.getInstanceEx(myProject);
UpdateInfoTree tree = vcsManager.showUpdateProjectInfo(myUpdatedFiles, "Update", ActionInfo.UPDATE, false);
tree.setBefore(myBeforeUpdateLabel);
tree.setAfter(LocalHistory.getInstance().putSystemLabel(myProject, "After push"));
}
else {
BrowserUtil.browse(event.getDescription());
}
}
}
@Nullable
public Label getAfterUpdateLabel() {
return myAfterUpdateLabel;
}
}
@@ -0,0 +1,219 @@
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package git4idea.push;
import com.intellij.dvcs.DvcsUtil;
import com.intellij.dvcs.repo.RepositoryUtil;
import com.intellij.history.Label;
import com.intellij.ide.BrowserUtil;
import com.intellij.notification.Notification;
import com.intellij.notification.NotificationGroup;
import com.intellij.notification.NotificationListener;
import com.intellij.notification.NotificationType;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.VcsNotifier;
import com.intellij.openapi.vcs.ex.ProjectLevelVcsManagerEx;
import com.intellij.openapi.vcs.update.ActionInfo;
import com.intellij.openapi.vcs.update.UpdateInfoTree;
import com.intellij.openapi.vcs.update.UpdatedFiles;
import com.intellij.util.Function;
import com.intellij.util.containers.ContainerUtil;
import git4idea.branch.GitBranchUtil;
import git4idea.repo.GitRepository;
import git4idea.update.GitUpdateResult;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.event.HyperlinkEvent;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
class GitPushResultNotification extends Notification {
public static final String VIEW_FILES_UPDATED_DURING_THE_PUSH = "<a href='UpdatedFiles'>View files updated during the push</a>";
public static final String UPDATE_WITH_RESOLVED_CONFLICTS = "push has been cancelled, because there were conflicts during update.<br/>" +
"Check that conflicts were resolved correctly, and invoke push again.";
public static final String INCOMPLETE_UPDATE = "push has been cancelled, because not all conflicts were resolved during update.<br/>" +
"Resolve the conflicts and invoke push again.";
public static final String UPDATE_WITH_ERRORS = "push was rejected, and update failed with error";
private static final Logger LOG = Logger.getInstance(GitPushResultNotification.class);
public GitPushResultNotification(@NotNull String groupDisplayId,
@NotNull String title,
@NotNull String content,
@NotNull NotificationType type,
@Nullable NotificationListener listener) {
super(groupDisplayId, title, content, type, listener);
}
static GitPushResultNotification create(Project project, GitPushResult pushResult, boolean multiRepoProject) {
GroupedPushResult grouped = GroupedPushResult.group(pushResult.getResults());
String title;
NotificationType type;
if (!grouped.errors.isEmpty()) {
if (!grouped.successful.isEmpty()) {
title = "Push partially failed";
}
else {
title = "Push failed";
}
type = NotificationType.ERROR;
}
else if (!grouped.rejected.isEmpty()) {
if (!grouped.successful.isEmpty()) {
title = "Push partially rejected";
}
else {
title = "Push rejected";
}
type = NotificationType.WARNING;
}
else {
title = "Push successful";
type = NotificationType.INFORMATION;
}
String description = formDescription(pushResult.getResults(), multiRepoProject);
ViewUpdatedFilesNotificationListener listener = null;
UpdatedFiles updatedFiles = pushResult.getUpdatedFiles();
if (!updatedFiles.isEmpty()) {
description += "<br/>" + VIEW_FILES_UPDATED_DURING_THE_PUSH;
listener = new ViewUpdatedFilesNotificationListener(project, updatedFiles,
pushResult.getBeforeUpdateLabel(), pushResult.getAfterUpdateLabel());
}
NotificationGroup group = type == NotificationType.INFORMATION ?
VcsNotifier.NOTIFICATION_GROUP_ID :
VcsNotifier.IMPORTANT_ERROR_NOTIFICATION;
return new GitPushResultNotification(group.getDisplayId(), title, description, type, listener);
}
private static String formDescription(Map<GitRepository, GitPushRepoResult> results, final boolean multiRepoProject) {
List<Map.Entry<GitRepository, GitPushRepoResult>> entries = ContainerUtil.sorted(results.entrySet(),
new Comparator<Map.Entry<GitRepository, GitPushRepoResult>>() {
@Override
public int compare(Map.Entry<GitRepository, GitPushRepoResult> o1, Map.Entry<GitRepository, GitPushRepoResult> o2) {
// successful first
int compareResultTypes = GitPushRepoResult.TYPE_COMPARATOR.compare(o1.getValue().getType(), o2.getValue().getType());
if (compareResultTypes != 0) {
return compareResultTypes;
}
return RepositoryUtil.REPOSITORY_COMPARATOR.compare(o1.getKey(), o2.getKey());
}
});
return StringUtil.join(entries, new Function<Map.Entry<GitRepository, GitPushRepoResult>, String>() {
@Override
public String fun(Map.Entry<GitRepository, GitPushRepoResult> entry) {
GitRepository repository = entry.getKey();
GitPushRepoResult result = entry.getValue();
String description = formRepoDescription(result);
if (!multiRepoProject) {
description = StringUtil.capitalize(description);
}
else {
description = DvcsUtil.getShortRepositoryName(repository) + ": " + description;
}
return description;
}
}, "<br/>");
}
private static String formRepoDescription(GitPushRepoResult result) {
String description;
String sourceBranch = GitBranchUtil.stripRefsPrefix(result.getSourceBranch());
String targetBranch = GitBranchUtil.stripRefsPrefix(result.getTargetBranch());
switch (result.getType()) {
case SUCCESS:
int commits = result.getNumberOfPushedCommits();
description = String.format("pushed %d %s to %s", commits, StringUtil.pluralize("commit", commits), targetBranch);
break;
case NEW_BRANCH:
description = String.format("pushed %s to new branch %s", sourceBranch, targetBranch);
break;
case FORCED:
description = String.format("force pushed %s to %s", sourceBranch, targetBranch);
break;
case REJECTED:
description = formDescriptionBasedOnUpdateResult(result.getUpdateResult(), targetBranch);
break;
case ERROR:
description = String.format("failed with error: " + result.getError());
break;
default:
LOG.error("Unexpected push result: " + result);
description = "";
break;
}
return description;
}
private static String formDescriptionBasedOnUpdateResult(GitUpdateResult updateResult, String targetBranch) {
if (updateResult == null || updateResult == GitUpdateResult.SUCCESS || updateResult == GitUpdateResult.NOTHING_TO_UPDATE) {
return String.format("push to %s was rejected", targetBranch);
}
else if (updateResult == GitUpdateResult.SUCCESS_WITH_RESOLVED_CONFLICTS) {
return UPDATE_WITH_RESOLVED_CONFLICTS;
}
else if (updateResult == GitUpdateResult.INCOMPLETE) {
return INCOMPLETE_UPDATE;
}
else {
return UPDATE_WITH_ERRORS;
}
}
private static class ViewUpdatedFilesNotificationListener implements NotificationListener {
private final Project myProject;
private final UpdatedFiles myUpdatedFiles;
private final Label myBeforeUpdateLabel;
private final Label myAfterUpdateLabel;
public ViewUpdatedFilesNotificationListener(@NotNull Project project, @NotNull UpdatedFiles updatedFiles, Label label, Label label1) {
myProject = project;
myUpdatedFiles = updatedFiles;
myBeforeUpdateLabel = label;
myAfterUpdateLabel = label1;
}
@Override
public void hyperlinkUpdate(@NotNull Notification notification, @NotNull HyperlinkEvent event) {
if (event.getEventType().equals(HyperlinkEvent.EventType.ACTIVATED)) {
if (event.getDescription().equals("UpdatedFiles")) {
ProjectLevelVcsManagerEx vcsManager = ProjectLevelVcsManagerEx.getInstanceEx(myProject);
UpdateInfoTree tree = vcsManager.showUpdateProjectInfo(myUpdatedFiles, "Update", ActionInfo.UPDATE, false);
tree.setBefore(myBeforeUpdateLabel);
tree.setAfter(myAfterUpdateLabel);
}
else {
BrowserUtil.browse(event.getDescription());
}
}
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2000-2012 JetBrains s.r.o.
* Copyright 2000-2014 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.
@@ -15,41 +15,26 @@
*/
package git4idea.push;
import com.intellij.dvcs.push.PushSource;
import git4idea.GitLocalBranch;
import git4idea.GitRemoteBranch;
import git4idea.repo.GitRemote;
import org.jetbrains.annotations.NotNull;
/**
* @author Kirill Likhodedov
*/
public class GitPushSpec {
class GitPushSource implements PushSource {
@NotNull private final GitLocalBranch mySource;
@NotNull private final GitRemoteBranch myDest;
@NotNull private final GitLocalBranch myBranch;
GitPushSpec(@NotNull GitLocalBranch source, @NotNull GitRemoteBranch dest) {
myDest = dest;
mySource = source;
GitPushSource(@NotNull GitLocalBranch branch) {
myBranch = branch;
}
@NotNull
public GitRemote getRemote() {
return myDest.getRemote();
}
@NotNull
public GitLocalBranch getSource() {
return mySource;
}
@NotNull
public GitRemoteBranch getDest() {
return myDest;
}
@Override
public String toString() {
return mySource + "->" + myDest;
public String getPresentation() {
return myBranch.getName();
}
@NotNull
public GitLocalBranch getBranch() {
return myBranch;
}
}
@@ -0,0 +1,134 @@
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package git4idea.push;
import com.intellij.dvcs.push.OutgoingCommitsProvider;
import com.intellij.dvcs.push.PushSupport;
import com.intellij.dvcs.push.PushTargetPanel;
import com.intellij.dvcs.push.Pusher;
import com.intellij.dvcs.repo.RepositoryManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vcs.AbstractVcs;
import com.intellij.util.ObjectUtils;
import git4idea.*;
import git4idea.branch.GitBranchUtil;
import git4idea.repo.GitBranchTrackInfo;
import git4idea.repo.GitRemote;
import git4idea.repo.GitRepository;
import git4idea.repo.GitRepositoryManager;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
public class GitPushSupport extends PushSupport<GitRepository, GitPushSource, GitPushTarget> {
private static final Logger LOG = Logger.getInstance(GitPushSupport.class);
@NotNull private final GitRepositoryManager myRepositoryManager;
@NotNull private final GitVcs myVcs;
@NotNull private final Pusher<GitRepository, GitPushSource, GitPushTarget> myPusher;
@NotNull private final OutgoingCommitsProvider<GitRepository, GitPushSource, GitPushTarget> myOutgoingCommitsProvider;
// instantiated from plugin.xml
@SuppressWarnings("UnusedDeclaration")
private GitPushSupport(@NotNull Project project, @NotNull GitRepositoryManager repositoryManager) {
myRepositoryManager = repositoryManager;
myVcs = ObjectUtils.assertNotNull(GitVcs.getInstance(project));
myPusher = new GitPusher(project);
myOutgoingCommitsProvider = new GitOutgoingCommitsProvider(project);
}
@NotNull
@Override
public AbstractVcs getVcs() {
return myVcs;
}
@NotNull
@Override
public Pusher<GitRepository, GitPushSource, GitPushTarget> getPusher() {
return myPusher;
}
@NotNull
@Override
public OutgoingCommitsProvider<GitRepository, GitPushSource, GitPushTarget> getOutgoingCommitsProvider() {
return myOutgoingCommitsProvider;
}
@Nullable
@Override
public GitPushTarget getDefaultTarget(@NotNull GitRepository repository) {
GitLocalBranch currentBranch = repository.getCurrentBranch();
if (currentBranch == null) {
return null;
}
GitBranchTrackInfo trackInfo = GitBranchUtil.getTrackInfoForBranch(repository, currentBranch);
if (trackInfo != null) {
return new GitPushTarget(trackInfo.getRemoteBranch(), false);
}
return proposeTargetForNewBranch(repository, currentBranch);
}
private static GitPushTarget proposeTargetForNewBranch(GitRepository repository, GitLocalBranch currentBranch) {
Collection<GitRemote> remotes = repository.getRemotes();
if (remotes.isEmpty()) {
return null; // TODO need to propose to declare new remote
}
else if (remotes.size() == 1) {
return makeTargetForNewBranch(repository, remotes.iterator().next(), currentBranch);
}
else {
GitRemote remote = GitUtil.getDefaultRemote(remotes);
if (remote == null) {
remote = remotes.iterator().next();
}
return makeTargetForNewBranch(repository, remote, currentBranch);
}
}
@NotNull
private static GitPushTarget makeTargetForNewBranch(@NotNull GitRepository repository,
@NotNull GitRemote remote,
@NotNull GitLocalBranch currentBranch) {
GitRemoteBranch existingRemoteBranch = GitPushTarget.findRemoteBranch(repository, remote, currentBranch.getName());
if (existingRemoteBranch != null) {
return new GitPushTarget(existingRemoteBranch, false);
}
return new GitPushTarget(new GitStandardRemoteBranch(remote, currentBranch.getName(), GitBranch.DUMMY_HASH), true);
}
@NotNull
@Override
public GitPushSource getSource(@NotNull GitRepository repository) {
return new GitPushSource(repository.getCurrentBranch()); // TODO assert: detached head => not possible to push
}
@NotNull
@Override
public RepositoryManager<GitRepository> getRepositoryManager() {
return myRepositoryManager;
}
@NotNull
@Override
public PushTargetPanel<GitPushTarget> createTargetPanel(@NotNull GitRepository repository, @Nullable GitPushTarget defaultTarget) {
return new GitPushTargetPanel(repository, defaultTarget);
}
}
@@ -0,0 +1,99 @@
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package git4idea.push;
import com.intellij.dvcs.push.PushTarget;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Condition;
import com.intellij.util.containers.ContainerUtil;
import git4idea.GitBranch;
import git4idea.GitRemoteBranch;
import git4idea.GitStandardRemoteBranch;
import git4idea.branch.GitBranchUtil;
import git4idea.repo.GitRemote;
import git4idea.repo.GitRepository;
import git4idea.validators.GitRefNameValidator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.text.ParseException;
import java.util.Collection;
class GitPushTarget implements PushTarget {
private static final Logger LOG = Logger.getInstance(GitPushTarget.class);
@NotNull private final GitRemoteBranch myRemoteBranch;
private final boolean myIsNewBranchCreated;
GitPushTarget(@NotNull GitRemoteBranch remoteBranch, boolean isNewBranchCreated) {
myRemoteBranch = remoteBranch;
myIsNewBranchCreated = isNewBranchCreated;
}
@NotNull
GitRemoteBranch getBranch() {
return myRemoteBranch;
}
boolean isNewBranchCreated() {
return myIsNewBranchCreated;
}
@NotNull
static GitPushTarget parse(@NotNull GitRepository repository, @NotNull String remoteName, @NotNull String branchName) throws
ParseException {
if (!GitRefNameValidator.getInstance().checkInput(branchName)) {
throw new ParseException("Invalid destination branch name: " + branchName, -1);
}
GitRemote remote = findRemote(repository.getRemotes(), remoteName);
if (remote == null) {
LOG.error("Remote [" + remoteName + "] is not found among " + repository.getRemotes());
throw new ParseException("Invalid remote: " + remoteName, -1);
}
GitRemoteBranch existingRemoteBranch = findRemoteBranch(repository, remote, branchName);
if (existingRemoteBranch != null) {
return new GitPushTarget(existingRemoteBranch, false);
}
GitRemoteBranch rb = new GitStandardRemoteBranch(remote, branchName, GitBranch.DUMMY_HASH);
return new GitPushTarget(rb, true);
}
@Nullable
private static GitRemote findRemote(@NotNull Collection<GitRemote> remotes, @NotNull final String candidate) {
return ContainerUtil.find(remotes, new Condition<GitRemote>() {
@Override
public boolean value(GitRemote remote) {
return remote.getName().equals(candidate);
}
});
}
@Nullable
static GitRemoteBranch findRemoteBranch(@NotNull GitRepository repository, @NotNull final GitRemote remote,
@NotNull final String nameAtRemote) {
return ContainerUtil.find(repository.getBranches().getRemoteBranches(), new Condition<GitRemoteBranch>() {
@Override
public boolean value(GitRemoteBranch remoteBranch) {
return remoteBranch.getRemote().equals(remote) &&
remoteBranch.getNameForRemoteOperations().equals(GitBranchUtil.stripRefsPrefix(nameAtRemote));
}
});
}
}
@@ -0,0 +1,205 @@
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package git4idea.push;
import com.intellij.dvcs.push.PushTargetPanel;
import com.intellij.dvcs.push.ui.PushTargetTextField;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.ui.ValidationInfo;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.openapi.ui.popup.ListPopup;
import com.intellij.openapi.ui.popup.PopupStep;
import com.intellij.openapi.ui.popup.util.BaseListPopupStep;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.ui.ClickListener;
import com.intellij.ui.ColoredTreeCellRenderer;
import com.intellij.ui.SimpleTextAttributes;
import com.intellij.ui.awt.RelativePoint;
import com.intellij.ui.components.JBLabel;
import com.intellij.util.Function;
import com.intellij.util.ObjectUtils;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.ui.UIUtil;
import git4idea.GitRemoteBranch;
import git4idea.repo.GitRemote;
import git4idea.repo.GitRepository;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.text.ParseException;
import java.util.Comparator;
import java.util.List;
class GitPushTargetPanel extends PushTargetPanel<GitPushTarget> {
private static final Logger LOG = Logger.getInstance(GitPushTargetPanel.class);
private static final Comparator<GitRemoteBranch> REMOTE_BRANCH_COMPARATOR = new MyRemoteBranchComparator();
public static final String SEPARATOR = " \u25BE ";
private final GitRepository myRepository;
private final PushTargetTextField myTargetTextField;
private final JLabel myRemoteLabel;
@Nullable private GitPushTarget myCurrentTarget;
public GitPushTargetPanel(@NotNull GitRepository repository, @Nullable GitPushTarget defaultTarget) {
myRepository = repository;
myCurrentTarget = defaultTarget;
String initialBranch;
String initialRemote;
if (defaultTarget == null) {
// TODO
initialBranch = "";
initialRemote = "No remotes";
}
else {
initialBranch = getTextFieldText(defaultTarget);
initialRemote = getRemoteLabelText(defaultTarget.getBranch().getRemote().getName());
}
myTargetTextField = new PushTargetTextField(repository.getProject(), getTargetNames(myRepository), initialBranch);
myRemoteLabel = new JBLabel(initialRemote);
new ClickListener() {
@Override
public boolean onClick(@NotNull MouseEvent event, int clickCount) {
showRemoteSelector();
return true;
}
}.installOn(myRemoteLabel);
setLayout(new BorderLayout());
setOpaque(false);
add(myRemoteLabel, BorderLayout.WEST);
add(myTargetTextField, BorderLayout.CENTER);
}
private void showRemoteSelector() {
final List<String> remotes = ContainerUtil.map(myRepository.getRemotes(), new Function<GitRemote, String>() {
@Override
public String fun(GitRemote remote) {
return remote.getName();
}
});
ListPopup popup = JBPopupFactory.getInstance().createListPopup(new BaseListPopupStep<String>(null, remotes) {
@Override
public PopupStep onChosen(String selectedValue, boolean finalChoice) {
myRemoteLabel.setText(getRemoteLabelText(selectedValue));
return super.onChosen(selectedValue, finalChoice);
}
});
// underneath, but so that list popup items are aligned with the label
popup.show(new RelativePoint(myRemoteLabel, new Point(-UIUtil.getListCellHPadding(), myRemoteLabel.getHeight())));
}
@Override
public void render(@NotNull ColoredTreeCellRenderer renderer) {
String targetName = myTargetTextField.getText();
if (StringUtil.isEmptyOrSpaces(targetName)) {
renderer.append("Specify remote branch to push", SimpleTextAttributes.ERROR_ATTRIBUTES, this);
}
else {
GitPushTarget target = getValue();
renderer.append(myRemoteLabel.getText(), SimpleTextAttributes.SYNTHETIC_ATTRIBUTES, this);
if (target.isNewBranchCreated()) {
renderer.append("+", SimpleTextAttributes.SYNTHETIC_ATTRIBUTES, this);
}
renderer.append(target.getBranch().getNameForRemoteOperations() + SEPARATOR, SimpleTextAttributes.SYNTHETIC_ATTRIBUTES, this);
}
}
@NotNull
@Override
public GitPushTarget getValue() {
return ObjectUtils.assertNotNull(myCurrentTarget);
}
@NotNull
private static String getTextFieldText(@Nullable GitPushTarget target) {
return (target != null ? target.getBranch().getNameForRemoteOperations() : "");
}
private static String getRemoteLabelText(@NotNull String selectedValue) {
return selectedValue + SEPARATOR;
}
@Override
public void fireOnCancel() {
myTargetTextField.setText(getTextFieldText(myCurrentTarget));
}
@Override
public void fireOnChange() {
String remoteName = myRemoteLabel.getText().replace(SEPARATOR, "");
String branchName = myTargetTextField.getText();
try {
myCurrentTarget = GitPushTarget.parse(myRepository, remoteName, branchName);
}
catch (ParseException e) {
LOG.error("Invalid remote name shouldn't be allowed. [" + remoteName + ", " + branchName + "]", e);
}
}
@Nullable
@Override
public ValidationInfo verify() {
try {
String remoteLabel = myRemoteLabel.getText().replace(SEPARATOR, "");
GitPushTarget.parse(myRepository, remoteLabel, myTargetTextField.getText());
return null;
}
catch (ParseException e) {
return new ValidationInfo(e.getMessage(), myTargetTextField);
}
}
@NotNull
public static List<String> getTargetNames(@NotNull GitRepository repository) {
List<GitRemoteBranch> remoteBranches = ContainerUtil.sorted(repository.getBranches().getRemoteBranches(), REMOTE_BRANCH_COMPARATOR);
return ContainerUtil.map(remoteBranches, new Function<GitRemoteBranch, String>() {
@Override
public String fun(GitRemoteBranch branch) {
return branch.getNameForRemoteOperations();
}
});
}
private static class MyRemoteBranchComparator implements Comparator<GitRemoteBranch> {
@Override
public int compare(GitRemoteBranch o1, GitRemoteBranch o2) {
String remoteName1 = o1.getRemote().getName();
String remoteName2 = o2.getRemote().getName();
int remoteComparison = remoteName1.compareTo(remoteName2);
if (remoteComparison != 0) {
if (remoteName1.equals(GitRemote.ORIGIN_NAME)) {
return -1;
}
if (remoteName2.equals(GitRemote.ORIGIN_NAME)) {
return 1;
}
return remoteComparison;
}
return o1.getNameForLocalOperations().compareTo(o2.getNameForLocalOperations());
}
}
}
+13 -529
View File
@@ -15,549 +15,33 @@
*/
package git4idea.push;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.Task;
import com.intellij.dvcs.push.PushSpec;
import com.intellij.dvcs.push.Pusher;
import com.intellij.dvcs.push.VcsPushOptionValue;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.VcsNotifier;
import com.intellij.openapi.vcs.update.UpdatedFiles;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.ui.UIUtil;
import git4idea.*;
import git4idea.branch.GitBranchPair;
import git4idea.commands.Git;
import git4idea.commands.GitCommandResult;
import git4idea.commands.GitLineHandlerListener;
import git4idea.commands.GitStandardProgressAnalyzer;
import git4idea.config.GitConfigUtil;
import git4idea.config.GitVcsSettings;
import git4idea.config.UpdateMethod;
import git4idea.history.GitHistoryUtils;
import git4idea.repo.GitBranchTrackInfo;
import git4idea.repo.GitRemote;
import git4idea.GitUtil;
import git4idea.repo.GitRepository;
import git4idea.repo.GitRepositoryManager;
import git4idea.settings.GitPushSettings;
import git4idea.update.GitUpdateProcess;
import git4idea.update.GitUpdateResult;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.Map;
/**
* Collects information to push and performs the push.
*/
public final class GitPusher {
/**
* if diff-log is not available (new branch is created, for example), we show a few recent commits made on the branch
*/
static final int RECENT_COMMITS_NUMBER = 5;
@Deprecated
static final GitRemoteBranch NO_TARGET_BRANCH = new GitStandardRemoteBranch(GitRemote.DOT, "", GitBranch.DUMMY_HASH);
private static final Logger LOG = Logger.getInstance(GitPusher.class);
private static final String INDICATOR_TEXT = "Pushing";
private static final int MAX_PUSH_ATTEMPTS = 10;
class GitPusher extends Pusher<GitRepository, GitPushSource, GitPushTarget> {
@NotNull private final Project myProject;
@NotNull private final GitRepositoryManager myRepositoryManager;
@NotNull private final ProgressIndicator myProgressIndicator;
@NotNull private final Collection<GitRepository> myRepositories;
@NotNull private final GitVcsSettings mySettings;
@NotNull private final GitPushSettings myPushSettings;
@NotNull private final Git myGit;
@NotNull private final GitPlatformFacade myPlatformFacade;
public static void showPushDialogAndPerformPush(@NotNull Project project, @NotNull GitPlatformFacade facade) {
final GitPushDialog dialog = new GitPushDialog(project);
dialog.show();
if (dialog.isOK()) {
runPushInBackground(project, facade, dialog);
}
}
private static void runPushInBackground(@NotNull final Project project, @NotNull final GitPlatformFacade facade,
@NotNull final GitPushDialog dialog) {
Task.Backgroundable task = new Task.Backgroundable(project, INDICATOR_TEXT, false) {
@Override
public void run(@NotNull ProgressIndicator indicator) {
new GitPusher(project, facade, indicator).push(dialog.getPushInfo());
}
};
GitVcs.runInBackground(task);
}
// holds settings chosen in GitRejectedPushUpdate dialog to reuse if the next push is rejected again.
static class UpdateSettings {
private final boolean myUpdateAllRoots;
private final UpdateMethod myUpdateMethod;
private UpdateSettings(boolean updateAllRoots, UpdateMethod updateMethod) {
myUpdateAllRoots = updateAllRoots;
myUpdateMethod = updateMethod;
}
public boolean shouldUpdateAllRoots() {
return myUpdateAllRoots;
}
public UpdateMethod getUpdateMethod() {
return myUpdateMethod;
}
public boolean shouldUpdate() {
return getUpdateMethod() != null;
}
@Override
public String toString() {
return String.format("UpdateSettings{myUpdateAllRoots=%s, myUpdateMethod=%s}", myUpdateAllRoots, myUpdateMethod);
}
}
public GitPusher(@NotNull Project project, @NotNull GitPlatformFacade facade, @NotNull ProgressIndicator indicator) {
GitPusher(@NotNull Project project) {
myProject = project;
myPlatformFacade = facade;
myProgressIndicator = indicator;
myRepositoryManager = GitUtil.getRepositoryManager(myProject);
myRepositories = myRepositoryManager.getRepositories();
mySettings = GitVcsSettings.getInstance(myProject);
myPushSettings = GitPushSettings.getInstance(myProject);
myGit = ServiceManager.getService(Git.class);
myRepositoryManager = GitUtil.getRepositoryManager(project);
}
/**
* @param pushSpecs which branches in which repositories should be pushed.
* The most common situation is all repositories in the project with a single currently active branch for
* each of them.
* @throws VcsException if couldn't query 'git log' about commits to be pushed.
* @return
*/
@NotNull
GitCommitsByRepoAndBranch collectCommitsToPush(@NotNull Map<GitRepository, GitPushSpec> pushSpecs) throws VcsException {
Map<GitRepository, List<GitBranchPair>> reposAndBranchesToPush = prepareRepositoriesAndBranchesToPush(pushSpecs);
Map<GitRepository, GitCommitsByBranch> commitsByRepoAndBranch = new HashMap<GitRepository, GitCommitsByBranch>();
for (GitRepository repository : myRepositories) {
List<GitBranchPair> branchPairs = reposAndBranchesToPush.get(repository);
if (branchPairs == null) {
continue;
}
GitCommitsByBranch commitsByBranch = collectsCommitsToPush(repository, branchPairs);
commitsByRepoAndBranch.put(repository, commitsByBranch);
}
return new GitCommitsByRepoAndBranch(commitsByRepoAndBranch);
}
@NotNull
private Map<GitRepository, List<GitBranchPair>> prepareRepositoriesAndBranchesToPush(@NotNull Map<GitRepository, GitPushSpec> pushSpecs) throws VcsException {
Map<GitRepository, List<GitBranchPair>> res = new HashMap<GitRepository, List<GitBranchPair>>();
for (GitRepository repository : myRepositories) {
GitPushSpec pushSpec = pushSpecs.get(repository);
if (pushSpec == null) {
continue;
}
res.put(repository, Collections.singletonList(new GitBranchPair(pushSpec.getSource(), pushSpec.getDest())));
}
return res;
}
@NotNull
private GitCommitsByBranch collectsCommitsToPush(@NotNull GitRepository repository, @NotNull List<GitBranchPair> sourcesDestinations)
throws VcsException {
Map<GitBranch, GitPushBranchInfo> commitsByBranch = new HashMap<GitBranch, GitPushBranchInfo>();
for (GitBranchPair sourceDest : sourcesDestinations) {
GitLocalBranch source = sourceDest.getBranch();
GitRemoteBranch dest = sourceDest.getDest();
assert dest != null : "Destination branch can't be null here for branch " + source;
List<GitCommit> commits;
GitPushBranchInfo.Type type;
if (dest == NO_TARGET_BRANCH) {
commits = collectRecentCommitsOnBranch(repository, source);
type = GitPushBranchInfo.Type.NO_TRACKED_OR_TARGET;
}
else if (GitUtil.repoContainsRemoteBranch(repository, dest)) {
commits = collectCommitsToPush(repository, source.getName(), dest.getName());
type = GitPushBranchInfo.Type.STANDARD;
}
else {
commits = collectRecentCommitsOnBranch(repository, source);
type = GitPushBranchInfo.Type.NEW_BRANCH;
}
commitsByBranch.put(source, new GitPushBranchInfo(source, dest, commits, type));
}
return new GitCommitsByBranch(commitsByBranch);
}
private List<GitCommit> collectRecentCommitsOnBranch(GitRepository repository, GitBranch source) throws VcsException {
return GitHistoryUtils.history(myProject, repository.getRoot(), "--max-count=" + RECENT_COMMITS_NUMBER, source.getName());
}
@NotNull
private List<GitCommit> collectCommitsToPush(@NotNull GitRepository repository, @NotNull String source, @NotNull String destination)
throws VcsException {
return GitHistoryUtils.history(myProject, repository.getRoot(), destination + ".." + source);
}
/**
* Makes push, shows the result in a notification. If push for current branch is rejected, shows a dialog proposing to update.
*/
public void push(@NotNull GitPushInfo pushInfo) {
push(pushInfo, null, null, 0);
}
/**
* Makes push, shows the result in a notification. If push for current branch is rejected, shows a dialog proposing to update.
* If {@code previousResult} and {@code updateSettings} are set, it means that this push is not the first, but is after a successful update.
* In that case, if push is rejected again, the dialog is not shown, and update is performed automatically with the previously chosen
* option.
* Also, at the end results are merged and are shown in a single notification.
*/
private void push(@NotNull GitPushInfo pushInfo, @Nullable GitPushResult previousResult, @Nullable UpdateSettings updateSettings, int attempt) {
GitPushResult result = tryPushAndGetResult(pushInfo);
handleResult(pushInfo, result, previousResult, updateSettings, attempt);
}
@NotNull
private GitPushResult tryPushAndGetResult(@NotNull GitPushInfo pushInfo) {
GitPushResult pushResult = new GitPushResult(myProject);
GitCommitsByRepoAndBranch commits = pushInfo.getCommits();
for (GitRepository repository : commits.getRepositories()) {
if (commits.get(repository).getAllCommits().size() == 0) { // don't push repositories where there is nothing to push. Note that when a branch is created, several recent commits are stored in the pushInfo.
continue;
}
GitPushRepoResult repoResult = pushRepository(pushInfo, commits, repository);
if (repoResult.getType() == GitPushRepoResult.Type.NOT_PUSHING) {
continue;
}
pushResult.append(repository, repoResult);
GitPushRepoResult.Type resultType = repoResult.getType();
if (resultType == GitPushRepoResult.Type.CANCEL || resultType == GitPushRepoResult.Type.NOT_AUTHORIZED) { // don't proceed if user has cancelled or couldn't login
break;
}
}
myRepositoryManager.updateAllRepositories(); // new remote branch may be created
return pushResult;
}
@NotNull
private GitPushRepoResult pushRepository(@NotNull GitPushInfo pushInfo,
@NotNull GitCommitsByRepoAndBranch commits,
@NotNull GitRepository repository) {
GitPushSpec pushSpec = pushInfo.getPushSpecs().get(repository);
GitSimplePushResult simplePushResult = pushAndGetSimpleResult(repository, pushSpec);
String output = simplePushResult.getOutput();
switch (simplePushResult.getType()) {
case SUCCESS:
return successOrErrorRepoResult(commits, repository, output, true);
case ERROR:
return successOrErrorRepoResult(commits, repository, output, false);
case REJECT:
return getResultFromRejectedPush(commits, repository, simplePushResult);
case NOT_AUTHORIZED:
return GitPushRepoResult.notAuthorized(output);
case CANCEL:
return GitPushRepoResult.cancelled(output);
case NOT_PUSHED:
return GitPushRepoResult.notPushed();
default:
return GitPushRepoResult.cancelled(output);
}
}
@NotNull
private static GitPushRepoResult getResultFromRejectedPush(@NotNull GitCommitsByRepoAndBranch commits,
@NotNull GitRepository repository,
@NotNull GitSimplePushResult simplePushResult) {
Collection<String> rejectedBranches = simplePushResult.getRejectedBranches();
Map<GitBranch, GitPushBranchResult> resultMap = new HashMap<GitBranch, GitPushBranchResult>();
GitCommitsByBranch commitsByBranch = commits.get(repository);
boolean pushedBranchWasRejected = false;
for (GitBranch branch : commitsByBranch.getBranches()) {
GitPushBranchResult branchResult;
if (branchInRejected(branch, rejectedBranches)) {
branchResult = GitPushBranchResult.rejected();
pushedBranchWasRejected = true;
}
else {
branchResult = successfulResultForBranch(commitsByBranch, branch);
}
resultMap.put(branch, branchResult);
}
if (pushedBranchWasRejected) {
return GitPushRepoResult.someRejected(resultMap, simplePushResult.getOutput());
} else {
// The rejectedDetector detected rejected push of the branch which had nothing to push (but is behind the upstream). We are not counting it.
return GitPushRepoResult.success(resultMap, simplePushResult.getOutput());
}
}
@NotNull
private GitSimplePushResult pushAndGetSimpleResult(@NotNull GitRepository repository, @NotNull GitPushSpec pushSpec) {
if (pushSpec.getDest() == NO_TARGET_BRANCH) {
return GitSimplePushResult.notPushed();
}
GitRemote remote = pushSpec.getRemote();
Collection<String> pushUrls = remote.getPushUrls();
if (pushUrls.isEmpty()) {
LOG.error("No urls or pushUrls are defined for " + remote);
return GitSimplePushResult.error("There are no URLs defined for remote " + remote.getName());
}
String url = pushUrls.iterator().next();
GitSimplePushResult pushResult;
pushResult = pushNatively(repository, pushSpec, url);
if (pushResult.getType() == GitSimplePushResult.Type.SUCCESS) {
setUpstream(repository, pushSpec.getSource(), pushSpec.getRemote(), pushSpec.getDest());
}
return pushResult;
}
private static void setUpstream(@NotNull GitRepository repository,
@NotNull GitLocalBranch source, @NotNull GitRemote remote, @NotNull GitRemoteBranch dest) {
if (!branchTrackingInfoIsSet(repository, source)) {
Project project = repository.getProject();
VirtualFile root = repository.getRoot();
String branchName = source.getName();
try {
boolean rebase = getMergeOrRebaseConfig(project, root);
GitConfigUtil.setValue(project, root, "branch." + branchName + ".remote", remote.getName());
GitConfigUtil.setValue(project, root, "branch." + branchName + ".merge",
GitBranch.REFS_HEADS_PREFIX + dest.getNameForRemoteOperations());
if (rebase) {
GitConfigUtil.setValue(project, root, "branch." + branchName + ".rebase", "true");
}
}
catch (VcsException e) {
LOG.error(String.format("Couldn't set up tracking for source branch %s, target branch %s, remote %s in root %s",
source, dest, remote, repository), e);
VcsNotifier.getInstance(project).notifyWeakError("Couldn't set up branch tracking");
}
}
}
private static boolean getMergeOrRebaseConfig(Project project, VirtualFile root) throws VcsException {
String autoSetupRebase = GitConfigUtil.getValue(project, root, GitConfigUtil.BRANCH_AUTOSETUP_REBASE);
return autoSetupRebase != null && (autoSetupRebase.equals("remote") || autoSetupRebase.equals("always"));
}
private static boolean branchTrackingInfoIsSet(@NotNull GitRepository repository, @NotNull GitLocalBranch source) {
for (GitBranchTrackInfo trackInfo : repository.getBranchTrackInfos()) {
if (trackInfo.getLocalBranch().equals(source)) {
return true;
}
}
return false;
}
@NotNull
private GitSimplePushResult pushNatively(GitRepository repository, GitPushSpec pushSpec, @NotNull String url) {
GitPushRejectedDetector rejectedDetector = new GitPushRejectedDetector();
GitLineHandlerListener progressListener = GitStandardProgressAnalyzer.createListener(myProgressIndicator);
GitCommandResult res = myGit.push(repository, pushSpec, url, rejectedDetector, progressListener);
if (rejectedDetector.rejected()) {
Collection<String> rejectedBranches = rejectedDetector.getRejectedBranches();
return GitSimplePushResult.reject(rejectedBranches);
}
else if (res.success()) {
return GitSimplePushResult.success();
}
else {
return GitSimplePushResult.error(res.getErrorOutputAsHtmlString());
}
}
@NotNull
private static GitPushRepoResult successOrErrorRepoResult(@NotNull GitCommitsByRepoAndBranch commits, @NotNull GitRepository repository, @NotNull String output, boolean success) {
GitPushRepoResult repoResult;
Map<GitBranch, GitPushBranchResult> resultMap = new HashMap<GitBranch, GitPushBranchResult>();
GitCommitsByBranch commitsByBranch = commits.get(repository);
for (GitBranch branch : commitsByBranch.getBranches()) {
GitPushBranchResult branchResult = success ?
successfulResultForBranch(commitsByBranch, branch) :
GitPushBranchResult.error();
resultMap.put(branch, branchResult);
}
repoResult = success ? GitPushRepoResult.success(resultMap, output) : GitPushRepoResult.error(resultMap, output);
return repoResult;
}
@NotNull
private static GitPushBranchResult successfulResultForBranch(@NotNull GitCommitsByBranch commitsByBranch, @NotNull GitBranch branch) {
GitPushBranchInfo branchInfo = commitsByBranch.get(branch);
if (branchInfo.isNewBranchCreated()) {
return GitPushBranchResult.newBranch(branchInfo.getDestBranch().getName());
}
return GitPushBranchResult.success(branchInfo.getCommits().size());
}
private static boolean branchInRejected(@NotNull GitBranch branch, @NotNull Collection<String> rejectedBranches) {
String branchName = branch.getName();
final String REFS_HEADS = "refs/heads/";
if (branchName.startsWith(REFS_HEADS)) {
branchName = branchName.substring(REFS_HEADS.length());
}
for (String rejectedBranch : rejectedBranches) {
if (rejectedBranch.equals(branchName) || (rejectedBranch.startsWith(REFS_HEADS) && rejectedBranch.substring(REFS_HEADS.length()).equals(branchName))) {
return true;
}
}
return false;
}
// if all repos succeeded, show notification.
// if all repos failed, show notification.
// if some repos failed, show notification with both results.
// if in failed repos, some branches were rejected, it is a warning-type, but not an error.
// if in a failed repo, current branch was rejected, propose to update the branch. Don't do it if not current branch was rejected,
// since it is difficult to update such a branch.
// if in a failed repo, a branch was rejected that had nothing to push, don't notify about the rejection.
// Besides all of the above, don't confuse users with 1 repository with all this "repository/root" stuff;
// don't confuse users which push only a single branch with all this "branch" stuff.
private void handleResult(@NotNull GitPushInfo pushInfo, @NotNull GitPushResult result, @Nullable GitPushResult previousResult, @Nullable UpdateSettings updateSettings,
int pushAttempt) {
result.mergeFrom(previousResult);
if (result.isEmpty()) {
VcsNotifier.getInstance(myProject).notifyInfo("Nothing to push");
}
else if (result.wasErrorCancelOrNotAuthorized()) {
// if there was an error on any repo, we won't propose to update even if current branch of a repo was rejected
result.createPushNotificationAndNotify();
}
else {
// there were no errors, but there might be some rejected branches on some of the repositories
// => for current branch propose to update and re-push it. For others just warn
Map<GitRepository, GitBranch> rejectedPushesForCurrentBranch = result.getRejectedPushesFromCurrentBranchToTrackedBranch(pushInfo);
if (pushAttempt <= MAX_PUSH_ATTEMPTS && !rejectedPushesForCurrentBranch.isEmpty()) {
LOG.info(
String.format("Rejected pushes for current branches: %n%s%nUpdate settings: %s", rejectedPushesForCurrentBranch, updateSettings));
if (updateSettings == null) {
// show dialog only when push is rejected for the first time in a row, otherwise reuse previously chosen update method
// and don't show the dialog again if user has chosen not to ask again
updateSettings = readUpdateSettings();
if (!mySettings.autoUpdateIfPushRejected()) {
updateSettings = showDialogAndGetExitCode(rejectedPushesForCurrentBranch, updateSettings);
saveUpdateSettings(updateSettings);
}
}
if (updateSettings.shouldUpdate()) {
Collection<GitRepository> repositoriesToUpdate = getRootsToUpdate(rejectedPushesForCurrentBranch, updateSettings.shouldUpdateAllRoots());
GitPushResult adjustedPushResult = result.remove(rejectedPushesForCurrentBranch);
adjustedPushResult.markUpdateStartIfNotMarked(repositoriesToUpdate);
boolean updateResult = update(repositoriesToUpdate, updateSettings.getUpdateMethod());
if (updateResult) {
myProgressIndicator.setText(INDICATOR_TEXT);
GitPushInfo newPushInfo = pushInfo.retain(rejectedPushesForCurrentBranch);
push(newPushInfo, adjustedPushResult, updateSettings, pushAttempt + 1);
return; // don't notify - next push will notify all results in compound
}
}
}
result.createPushNotificationAndNotify();
}
}
private void saveUpdateSettings(@NotNull UpdateSettings updateSettings) {
UpdateMethod updateMethod = updateSettings.getUpdateMethod();
if (updateMethod != null) { // null if user has pressed cancel
myPushSettings.setUpdateAllRoots(updateSettings.shouldUpdateAllRoots());
myPushSettings.setUpdateMethod(updateMethod);
}
}
@NotNull
private UpdateSettings readUpdateSettings() {
boolean updateAllRoots = myPushSettings.shouldUpdateAllRoots();
UpdateMethod updateMethod = myPushSettings.getUpdateMethod();
return new UpdateSettings(updateAllRoots, updateMethod);
}
private UpdateSettings showDialogAndGetExitCode(final Map<GitRepository, GitBranch> rejectedPushesForCurrentBranch,
final UpdateSettings initialSettings) {
final AtomicReference<UpdateSettings> updateSettings = new AtomicReference<UpdateSettings>();
UIUtil.invokeAndWaitIfNeeded(new Runnable() {
@Override
public void run() {
final GitRejectedPushUpdateDialog dialog = new GitRejectedPushUpdateDialog(myProject, rejectedPushesForCurrentBranch.keySet(), initialSettings);
dialog.show();
final int exitCode = dialog.getExitCode();
if (exitCode != DialogWrapper.CANCEL_EXIT_CODE) {
mySettings.setAutoUpdateIfPushRejected(dialog.shouldAutoUpdateInFuture());
}
updateSettings.set(new UpdateSettings(dialog.shouldUpdateAll(), getUpdateMethodFromDialogExitCode(exitCode)));
}
});
return updateSettings.get();
}
/**
* @return update method selected in the dialog or {@code null} if user pressed Cancel, i.e. doesn't want to update.
*/
@Nullable
private static UpdateMethod getUpdateMethodFromDialogExitCode(int exitCode) {
switch (exitCode) {
case GitRejectedPushUpdateDialog.MERGE_EXIT_CODE: return UpdateMethod.MERGE;
case GitRejectedPushUpdateDialog.REBASE_EXIT_CODE: return UpdateMethod.REBASE;
}
return null;
}
@NotNull
private Collection<GitRepository> getRootsToUpdate(@NotNull Map<GitRepository, GitBranch> rejectedPushesForCurrentBranch, boolean updateAllRoots) {
return updateAllRoots ? myRepositories : rejectedPushesForCurrentBranch.keySet();
}
private boolean update(@NotNull Collection<GitRepository> rootsToUpdate, @NotNull UpdateMethod updateMethod) {
GitUpdateProcess.UpdateMethod um = updateMethod == UpdateMethod.MERGE ? GitUpdateProcess.UpdateMethod.MERGE : GitUpdateProcess.UpdateMethod.REBASE;
GitUpdateResult updateResult = new GitUpdateProcess(myProject, myPlatformFacade, myProgressIndicator,
new HashSet<GitRepository>(rootsToUpdate), UpdatedFiles.create()).update(um);
for (GitRepository repository : rootsToUpdate) {
repository.getRoot().refresh(true, true);
}
if (updateResult == GitUpdateResult.SUCCESS) {
return true;
}
else if (updateResult == GitUpdateResult.SUCCESS_WITH_RESOLVED_CONFLICTS || updateResult == GitUpdateResult.INCOMPLETE) {
String title = "Push cancelled";
String description;
if (updateResult == GitUpdateResult.INCOMPLETE) {
description = "Push has been cancelled, because not all conflicts were resolved during update.<br/>" +
"Resolve the conflicts and invoke push again.";
}
else {
description = "Push has been cancelled, because there were conflicts during update.<br/>" +
"Check that conflicts were resolved correctly, and invoke push again.";
}
VcsNotifier.getInstance(myProject).notifyMinorWarning(title, description);
return false;
}
else {
return false;
}
@Override
public void push(@NotNull Map<GitRepository, PushSpec<GitPushSource, GitPushTarget>> pushSpecs, @Nullable VcsPushOptionValue vcsPushOptionValue, boolean force) {
GitPushResult result = new GitPushOperation(myProject, pushSpecs, force).execute();
GitPushResultNotification notification = GitPushResultNotification.create(myProject, result, myRepositoryManager.moreThanOneRoot());
notification.notify(myProject);
}
}
@@ -58,7 +58,7 @@ class GitRejectedPushUpdateDialog extends DialogWrapper {
protected GitRejectedPushUpdateDialog(@NotNull Project project,
@NotNull Collection<GitRepository> repositories,
@NotNull GitPusher.UpdateSettings initialSettings) {
@NotNull PushUpdateSettings initialSettings) {
super(project);
myProject = project;
myRepositories = repositories;
@@ -1,92 +0,0 @@
/*
* Copyright 2000-2011 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package git4idea.push;
import org.jetbrains.annotations.NotNull;
import java.util.Collection;
import java.util.Collections;
/**
* @author Kirill Likhodedov
*/
public final class GitSimplePushResult {
private final Type myType;
private final String myErrorOutput;
private final Collection<String> myRejectedBranches;
public enum Type {
NOT_PUSHED,
SUCCESS,
REJECT,
CANCEL,
NOT_AUTHORIZED,
ERROR
}
private GitSimplePushResult(@NotNull Type type, @NotNull String errorOutput, @NotNull Collection<String> rejectedBranches) {
myType = type;
myErrorOutput = errorOutput;
myRejectedBranches = rejectedBranches;
}
@NotNull
public static GitSimplePushResult success() {
return new GitSimplePushResult(Type.SUCCESS, "", Collections.<String>emptyList());
}
@NotNull
public static GitSimplePushResult notPushed() {
return new GitSimplePushResult(Type.NOT_PUSHED, "", Collections.<String>emptyList());
}
@NotNull
public static GitSimplePushResult cancel() {
return new GitSimplePushResult(Type.CANCEL, "Cancelled by user", Collections.<String>emptyList());
}
@NotNull
public static GitSimplePushResult notAuthorized() {
return new GitSimplePushResult(Type.NOT_AUTHORIZED, "Couldn't authorize", Collections.<String>emptyList());
}
@NotNull
public static GitSimplePushResult reject(@NotNull Collection<String> rejectedBranches) {
return new GitSimplePushResult(Type.REJECT, "", rejectedBranches);
}
@NotNull
public static GitSimplePushResult error(@NotNull String errorOutput) {
return new GitSimplePushResult(Type.ERROR, errorOutput, Collections.<String>emptyList());
}
@NotNull
public String getOutput() {
return myErrorOutput;
}
@NotNull
public Collection<String> getRejectedBranches() {
return myRejectedBranches;
}
@NotNull
public Type getType() {
return myType;
}
}
@@ -0,0 +1,57 @@
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package git4idea.push;
import com.intellij.util.containers.ContainerUtil;
import git4idea.repo.GitRepository;
import java.util.Map;
class GroupedPushResult {
final Map<GitRepository, GitPushRepoResult> successful;
final Map<GitRepository, GitPushRepoResult> errors;
final Map<GitRepository, GitPushRepoResult> rejected;
private GroupedPushResult(Map<GitRepository, GitPushRepoResult> successful,
Map<GitRepository, GitPushRepoResult> errors,
Map<GitRepository, GitPushRepoResult> rejected) {
this.successful = successful;
this.errors = errors;
this.rejected = rejected;
}
static GroupedPushResult group(Map<GitRepository, GitPushRepoResult> results) {
Map<GitRepository, GitPushRepoResult> successful = ContainerUtil.newHashMap();
Map<GitRepository, GitPushRepoResult> rejected = ContainerUtil.newHashMap();
Map<GitRepository, GitPushRepoResult> errors = ContainerUtil.newHashMap();
for (Map.Entry<GitRepository, GitPushRepoResult> entry : results.entrySet()) {
GitRepository repository = entry.getKey();
GitPushRepoResult result = entry.getValue();
if (result.getType() == GitPushRepoResult.Type.REJECTED) {
rejected.put(repository, result);
}
else if (result.getType() == GitPushRepoResult.Type.ERROR) {
errors.put(repository, result);
}
else {
successful.put(repository, result);
}
}
return new GroupedPushResult(successful, errors, rejected);
}
}
@@ -0,0 +1,45 @@
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package git4idea.push;
import git4idea.config.UpdateMethod;
import org.jetbrains.annotations.NotNull;
// holds settings chosen in GitRejectedPushUpdate dialog to reuse if the next push is rejected again.
class PushUpdateSettings {
private final boolean myUpdateAllRoots;
@NotNull private final UpdateMethod myUpdateMethod;
PushUpdateSettings(boolean updateAllRoots, @NotNull UpdateMethod updateMethod) {
myUpdateAllRoots = updateAllRoots;
myUpdateMethod = updateMethod;
}
boolean shouldUpdateAllRoots() {
return myUpdateAllRoots;
}
@NotNull
UpdateMethod getUpdateMethod() {
return myUpdateMethod;
}
@Override
public String toString() {
return String.format("UpdateSettings{myUpdateAllRoots=%s, myUpdateMethod=%s}", myUpdateAllRoots, myUpdateMethod);
}
}
@@ -0,0 +1,101 @@
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package git4idea.push;
import org.junit.Test;
import java.util.Arrays;
import static org.junit.Assert.assertEquals;
public class GitPushNativeResultParserTest {
public static final String STANDARD_PREFIX = "Counting objects: 20, done.\n" +
"Delta compression using up to 8 threads.\n" +
"Compressing objects: 100% (14/14), done.\n" +
"Writing objects: 100% (20/20), 1.70 KiB | 0 bytes/s, done.\n" +
"Total 20 (delta 7), reused 0 (delta 0)\n" +
"To /Users/loki/sandbox/git/parent.git";
public static final String TARGET_PREFIX = "To http://example.com/git/parent.git";
public static final String PUSH_DEFAULT_WARNING = "warning: push.default is unset; its implicit value has changed in\n" +
"Git 2.0 from 'matching' to 'simple'. To squelch this message\n" +
"and maintain the traditional behavior, use:\n" +
"\n" +
" git config --global push.default matching\n" +
"\n" +
"To squelch this message and adopt the new behavior now, use:\n" +
"\n" +
" git config --global push.default simple\n" +
"\n" +
"When push.default is set to 'matching', git will push local branches\n" +
"to the remote branches that already exist with the same name.\n" +
"\n" +
"Since Git 2.0, Git defaults to the more conservative 'simple'\n" +
"behavior, which only pushes the current branch to the corresponding\n" +
"remote branch that 'git pull' uses to update the current branch.\n" +
"\n" +
"See 'git help config' and search for 'push.default' for further information.\n" +
"(the 'simple' mode was introduced in Git 1.7.11. Use the similar mode\n" +
"'current' instead of 'simple' if you sometimes use older versions of Git)";
public static final String SUCCESS_SUFFIX = "Done";
public static final String REJECT_SUFFIX = "Done\n" +
"error: failed to push some refs to '/Users/loki/sandbox/git/parent.git'\n" +
"hint: Updates were rejected because the tip of your current branch is behind\n" +
"hint: its remote counterpart. Integrate the remote changes (e.g.\n" +
"hint: 'git pull ...') before pushing again.\n" +
"hint: See the 'Note about fast-forwards' in 'git push --help' for details.";
@Test
public void success() {
String output = " \trefs/heads/master:refs/heads/master\t3e62822..a537351";
GitPushNativeResult result = GitPushNativeResultParser.parse(Arrays.asList(STANDARD_PREFIX, TARGET_PREFIX, output, SUCCESS_SUFFIX));
assertResult(GitPushNativeResult.Type.SUCCESS, "3e62822..a537351", result);
}
@Test
public void rejected() {
String output = "!\trefs/heads/master:refs/heads/master\t[rejected] (non-fast-forward)";
GitPushNativeResult result = GitPushNativeResultParser.parse(Arrays.asList(STANDARD_PREFIX, TARGET_PREFIX, output, REJECT_SUFFIX));
assertResult(GitPushNativeResult.Type.REJECTED, null, result);
}
@Test
public void forcedUpdate() {
String output = "+\trefs/heads/master:refs/heads/master\tb9b3235...23760f8 (forced update)";
GitPushNativeResult result = GitPushNativeResultParser.parse(Arrays.asList(PUSH_DEFAULT_WARNING, TARGET_PREFIX, output));
assertResult(GitPushNativeResult.Type.FORCED_UPDATE, "b9b3235...23760f8", result);
}
@Test
public void upToDate() {
String output = "=\trefs/heads/master:refs/heads/master\t[up to date]";
GitPushNativeResult result = GitPushNativeResultParser.parse(Arrays.asList(TARGET_PREFIX, output));
assertResult(GitPushNativeResult.Type.UP_TO_DATE, null, result);
}
@Test
public void newRef() {
String output = "*\trefs/heads/feature:refs/heads/feature2\t[new branch]";
GitPushNativeResult result = GitPushNativeResultParser.parse(Arrays.asList(STANDARD_PREFIX, TARGET_PREFIX, output));
assertResult(GitPushNativeResult.Type.NEW_REF, null, result);
}
private static void assertResult(GitPushNativeResult.Type expectedType, String expectedRange, GitPushNativeResult actualResult) {
assertEquals(expectedType, actualResult.getType());
assertEquals(expectedRange, actualResult.getRange());
}
}
@@ -0,0 +1,160 @@
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package git4idea.push;
import com.intellij.dvcs.push.PushSpec;
import com.intellij.openapi.util.Trinity;
import com.intellij.openapi.vcs.AbstractVcsHelper;
import com.intellij.testFramework.fixtures.IdeaTestFixtureFactory;
import com.intellij.testFramework.fixtures.TempDirTestFixture;
import git4idea.*;
import git4idea.repo.GitRemote;
import git4idea.repo.GitRepository;
import git4idea.test.GitPlatformTest;
import git4idea.test.GitTestUtil;
import git4idea.test.MockVcsHelper;
import git4idea.update.GitUpdateResult;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import static com.intellij.openapi.vcs.Executor.append;
import static git4idea.test.GitExecutor.*;
abstract class GitPushOperationBaseTest extends GitPlatformTest {
private TempDirTestFixture myOutside;
protected String myExternalPath;
@Override
protected void setUp() throws Exception {
super.setUp();
try {
myOutside = IdeaTestFixtureFactory.getFixtureFactory().createTempDirTestFixture();
myOutside.setUp();
myExternalPath = myOutside.getTempDirPath();
}
catch (Exception e) {
super.tearDown();
throw e;
}
try {
GitTestUtil.overrideService(myProject, AbstractVcsHelper.class, MockVcsHelper.class); // todo temporary: to handle merge dialog
}
catch (Exception e) {
tearDown();
throw e;
}
}
@Override
protected void tearDown() throws Exception {
try {
myOutside.tearDown();
}
finally {
super.tearDown();
}
}
@Override
protected void refresh() {
super.refresh();
myGitRepositoryManager.updateAllRepositories();
}
@NotNull
@Override
protected Collection<String> getDebugLogCategories() {
return Collections.singletonList("#" + GitPushOperation.class.getName());
}
@NotNull
protected Trinity<GitRepository, File, File> setupRepositories(String repoRoot, String parentName, String broName) {
File parentRepo = createParentRepo(parentName);
File broRepo = createBroRepo(broName, parentRepo);
GitRepository repository = GitTestUtil.createRepository(myProject, repoRoot);
cd(repository);
git("remote add origin " + parentRepo.getPath());
git("push --set-upstream origin master:master");
cd(broRepo.getPath());
git("pull");
return Trinity.create(repository, parentRepo, broRepo);
}
@NotNull
private File createParentRepo(@NotNull String parentName) {
cd(myExternalPath);
git("init --bare " + parentName + ".git");
return new File(myExternalPath, parentName + ".git");
}
@NotNull
private File createBroRepo(@NotNull String broName, @NotNull File parentRepo) {
cd(myExternalPath);
git("clone " + parentRepo.getName() + " " + broName);
return new File(myExternalPath, broName);
}
@NotNull
protected static PushSpec<GitPushSource, GitPushTarget> makePushSpec(@NotNull GitRepository repository,
@NotNull String from,
@NotNull String to) {
GitLocalBranch source = repository.getBranches().findLocalBranch(from);
assertNotNull(source);
GitRemoteBranch target = (GitRemoteBranch)repository.getBranches().findBranchByName(to);
boolean newBranch;
if (target == null) {
int firstSlash = to.indexOf('/');
GitRemote remote = GitUtil.findRemoteByName(repository, to.substring(0, firstSlash));
assertNotNull(remote);
target = new GitStandardRemoteBranch(remote, to.substring(firstSlash + 1), GitBranch.DUMMY_HASH);
newBranch = true;
}
else {
newBranch = false;
}
return new PushSpec<GitPushSource, GitPushTarget>(new GitPushSource(source), new GitPushTarget(target, newBranch));
}
@NotNull
protected static String makeCommit(String file) throws IOException {
append(file, "some content");
addCommit("some message");
return last();
}
protected void assertResult(@NotNull GitPushRepoResult.Type type, int pushedCommits, @NotNull String from, @NotNull String to,
@Nullable GitUpdateResult updateResult,
@NotNull GitPushRepoResult actualResult) {
String message = "Result is incorrect: " + actualResult;
assertEquals(message, type, actualResult.getType());
assertEquals(message, pushedCommits, actualResult.getNumberOfPushedCommits());
assertEquals(message, GitBranch.REFS_HEADS_PREFIX + from, actualResult.getSourceBranch());
assertEquals(message, GitBranch.REFS_REMOTES_PREFIX + to, actualResult.getTargetBranch());
assertEquals(message, updateResult, actualResult.getUpdateResult());
}
}
@@ -0,0 +1,123 @@
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package git4idea.push;
import com.intellij.dvcs.push.PushSpec;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.Trinity;
import com.intellij.util.containers.ContainerUtil;
import git4idea.GitLocalBranch;
import git4idea.GitRemoteBranch;
import git4idea.commands.Git;
import git4idea.commands.GitCommandResult;
import git4idea.commands.GitImpl;
import git4idea.commands.GitLineHandlerListener;
import git4idea.repo.GitRepository;
import git4idea.test.GitTestUtil;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import static git4idea.test.GitExecutor.*;
public class GitPushOperationMultiRepoTest extends GitPushOperationBaseTest {
private GitRepository myCommunity;
private GitRepository myRepository;
@Override
protected void setUp() throws Exception {
try {
super.setUp();
}
catch (Exception e) {
super.tearDown();
throw e;
}
try {
Trinity<GitRepository, File, File> mainRepo = setupRepositories(myProjectPath, "parent", "bro");
myRepository = mainRepo.first;
File community = new File(myProjectPath, "community");
assertTrue(community.mkdir());
Trinity<GitRepository, File, File> enclosingRepo = setupRepositories(community.getPath(),
"community_parent", "community_bro");
myCommunity = enclosingRepo.first;
cd(myProjectPath);
refresh();
}
catch (Exception e) {
tearDown();
throw e;
}
}
private static class FailingPushGit extends GitImpl {
private Condition<GitRepository> myPushShouldFail;
@NotNull
@Override
public GitCommandResult push(GitRepository repository,
GitLocalBranch source,
GitRemoteBranch target,
boolean force,
boolean updateTracking,
GitLineHandlerListener... listeners) {
if (myPushShouldFail.value(repository)) {
return new GitCommandResult(false, 128, Arrays.asList("Failed to push to " + target.getName()),
Collections.<String>emptyList(), null);
}
return super.push(repository, source, target, force, updateTracking, listeners);
}
}
public void test_try_push_from_all_roots_even_if_one_fails() throws IOException {
FailingPushGit failingPushGit = GitTestUtil.overrideService(Git.class, FailingPushGit.class);
// fail in the first repo
failingPushGit.myPushShouldFail = new Condition<GitRepository>() {
@Override
public boolean value(GitRepository repository) {
return repository.equals(myRepository);
}
};
cd(myRepository);
makeCommit("file.txt");
cd(myCommunity);
makeCommit("com.txt");
PushSpec<GitPushSource, GitPushTarget> spec1 = makePushSpec(myRepository, "master", "origin/master");
PushSpec<GitPushSource, GitPushTarget> spec2 = makePushSpec(myCommunity, "master", "origin/master");
Map<GitRepository, PushSpec<GitPushSource, GitPushTarget>> map = ContainerUtil.newHashMap();
map.put(myRepository, spec1);
map.put(myCommunity, spec2);
GitPushResult result = new GitPushOperation(myProject, map, false).execute();
GitPushRepoResult result1 = result.getResults().get(myRepository);
GitPushRepoResult result2 = result.getResults().get(myCommunity);
assertResult(GitPushRepoResult.Type.ERROR, -1, "master", "origin/master", null, result1);
assertEquals("Error text is incorrect", "Failed to push to origin/master", result1.getError());
assertResult(GitPushRepoResult.Type.SUCCESS, 1, "master", "origin/master", null, result2);
}
}
@@ -0,0 +1,323 @@
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package git4idea.push;
import com.intellij.dvcs.push.PushSpec;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.Trinity;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.update.FileGroup;
import com.intellij.openapi.vcs.update.UpdatedFiles;
import com.intellij.util.Function;
import com.intellij.util.containers.ContainerUtil;
import git4idea.branch.GitBranchUtil;
import git4idea.config.UpdateMethod;
import git4idea.repo.GitRepository;
import git4idea.test.TestDialogHandler;
import git4idea.update.GitUpdateResult;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import static git4idea.push.GitPushRepoResult.Type.*;
import static git4idea.test.GitExecutor.*;
public class GitPushOperationSingleRepoTest extends GitPushOperationBaseTest {
protected GitRepository myRepository;
protected File myParentRepo;
protected File myBroRepo;
@Override
protected void setUp() throws Exception {
try {
super.setUp();
}
catch (Exception e) {
super.tearDown();
throw e;
}
try {
Trinity<GitRepository, File, File> trinity = setupRepositories(myProjectPath, "parent", "bro");
myParentRepo = trinity.second;
myBroRepo = trinity.third;
myRepository = trinity.first;
cd(myProjectPath);
refresh();
}
catch (Exception e) {
tearDown();
throw e;
}
}
public void test_successful_push() throws IOException {
String hash = makeCommit("file.txt");
GitPushResult result = push("master", "origin/master");
assertResult(SUCCESS, 1, "master", "origin/master", result);
assertPushed(hash, "master");
}
public void test_push_new_branch() {
git("checkout -b feature");
GitPushResult result = push("feature", "origin/feature");
assertResult(NEW_BRANCH, -1, "feature", "origin/feature", result);
assertBranchExists("feature");
}
public void test_push_new_branch_with_commits() {
touch("feature.txt", "content");
addCommit("feature commit");
String hash = last();
git("checkout -b feature");
GitPushResult result = push("feature", "origin/feature");
assertResult(NEW_BRANCH, -1, "feature", "origin/feature", result);
assertBranchExists("feature");
assertPushed(hash, "feature");
}
public void test_upstream_is_set_for_new_branch() {
git("checkout -b feature");
push("feature", "origin/feature");
assertUpstream("feature", "origin", "feature");
}
public void test_upstream_is_not_modified_if_already_set() {
push("master", "origin/feature");
assertUpstream("master", "origin", "master");
}
public void test_rejected_push_to_tracked_branch_proposes_to_update() throws IOException {
pushCommitFromBro();
final Ref<Boolean> dialogShown = Ref.create(false);
myDialogManager.registerDialogHandler(GitRejectedPushUpdateDialog.class, new TestDialogHandler() {
@Override
public int handleDialog(DialogWrapper dialog) {
dialogShown.set(true);
return DialogWrapper.CANCEL_EXIT_CODE;
}
});
GitPushResult result = push("master", "origin/master");
assertTrue("Rejected push dialog wasn't shown", dialogShown.get());
assertResult(REJECTED, -1, "master", "origin/master", result);
}
public void test_rejected_push_to_other_branch_doesnt_propose_to_update() throws IOException {
pushCommitFromBro();
cd(myRepository);
git("checkout -b feature");
final Ref<Boolean> dialogShown = Ref.create(false);
myDialogManager.registerDialogHandler(GitRejectedPushUpdateDialog.class, new TestDialogHandler() {
@Override
public int handleDialog(DialogWrapper dialog) {
dialogShown.set(true);
return DialogWrapper.CANCEL_EXIT_CODE;
}
});
GitPushResult result = push("feature", "origin/master");
assertFalse("Rejected push dialog shouldn't be shown", dialogShown.get());
assertResult(REJECTED, -1, "feature", "origin/master", result);
}
public void test_push_is_rejected_too_many_times() throws IOException {
pushCommitFromBro();
cd(myRepository);
String hash = makeCommit("afile.txt");
agreeToUpdate(GitRejectedPushUpdateDialog.MERGE_EXIT_CODE);
refresh();
PushSpec<GitPushSource, GitPushTarget> pushSpec = makePushSpec(myRepository, "master", "origin/master");
GitPushResult result = new GitPushOperation(myProject, Collections.singletonMap(myRepository, pushSpec), false) {
@NotNull
@Override
protected GitUpdateResult update(@NotNull Collection<GitRepository> rootsToUpdate, @NotNull UpdateMethod updateMethod) {
GitUpdateResult updateResult = super.update(rootsToUpdate, updateMethod);
try {
pushCommitFromBro();
}
catch (IOException e) {
throw new RuntimeException(e);
}
return updateResult;
}
}.execute();
assertResult(REJECTED, -1, "master", "origin/master", GitUpdateResult.SUCCESS, Arrays.asList("bro.txt"), result);
cd(myParentRepo.getPath());
String history = git("log --all --pretty=%H ");
assertFalse("The commit shouldn't be pushed", history.contains(hash));
}
public void test_force_push() throws IOException {
String lostHash = pushCommitFromBro();
cd(myRepository);
String hash = makeCommit("anyfile.txt");
GitPushResult result = push("master", "origin/master", true);
assertResult(FORCED, -1, "master", "origin/master", result);
cd(myParentRepo.getPath());
String history = git("log --all --pretty=%H ");
assertFalse(history.contains(lostHash));
assertEquals(hash, StringUtil.splitByLines(history)[0]);
}
public void test_merge_after_rejected_push() throws IOException {
String broHash = pushCommitFromBro();
cd(myRepository);
String hash = makeCommit("file.txt");
agreeToUpdate(GitRejectedPushUpdateDialog.MERGE_EXIT_CODE);
GitPushResult result = push("master", "origin/master");
cd(myRepository);
String log = git("log -3 --pretty=%H#%s");
String[] commits = StringUtil.splitByLines(log);
String lastCommitMsg = commits[0].split("#")[1];
assertTrue("The last commit doesn't look like a merge commit: " + lastCommitMsg, lastCommitMsg.contains("Merge"));
assertEquals(hash, commits[1].split("#")[0]);
assertEquals(broHash, commits[2].split("#")[0]);
assertResult(SUCCESS, 2, "master", "origin/master", GitUpdateResult.SUCCESS, Arrays.asList("bro.txt"), result);
}
public void test_update_with_conflicts_cancels_push() throws IOException {
cd(myBroRepo.getPath());
append("bro.txt", "bro content");
makeCommit("msg");
git("push origin master:master");
cd(myRepository);
append("bro.txt", "main content");
makeCommit("msg");
agreeToUpdate(GitRejectedPushUpdateDialog.REBASE_EXIT_CODE);
GitPushResult result = push("master", "origin/master");
assertResult(REJECTED, -1, "master", "origin/master", GitUpdateResult.INCOMPLETE, Arrays.asList("bro.txt"), result);
}
@NotNull
private GitPushResult push(@NotNull String from, @NotNull String to) {
return push(from, to, false);
}
@NotNull
private GitPushResult push(@NotNull String from, @NotNull String to, boolean force) {
refresh();
PushSpec<GitPushSource, GitPushTarget> spec = makePushSpec(myRepository, from, to);
return new GitPushOperation(myProject, Collections.singletonMap(myRepository, spec), force).execute();
}
private void agreeToUpdate(final int exitCode) {
myDialogManager.registerDialogHandler(GitRejectedPushUpdateDialog.class, new TestDialogHandler() {
@Override
public int handleDialog(DialogWrapper dialog) {
return exitCode;
}
});
}
private String pushCommitFromBro() throws IOException {
cd(myBroRepo.getPath());
String hash = makeCommit("bro.txt");
git("push");
return hash;
}
private void assertResult(GitPushRepoResult.Type type, int pushedCommits, String from, String to, GitPushResult actualResult) {
assertResult(type, pushedCommits, from, to, null, null, actualResult);
}
private void assertResult(@NotNull GitPushRepoResult.Type type, int pushedCommits, @NotNull String from, @NotNull String to,
@Nullable GitUpdateResult updateResult,
@Nullable List<String> updatedFiles,
@NotNull GitPushResult actualResult) {
assertResult(type, pushedCommits, from, to, updateResult, actualResult.getResults().get(myRepository));
assertSameElements("Updated files set is incorrect",
getUpdatedFiles(actualResult.getUpdatedFiles()), ContainerUtil.notNullize(updatedFiles));
}
@Nullable
private Collection<String> getUpdatedFiles(@NotNull UpdatedFiles updatedFiles) {
Collection<String> result = ContainerUtil.newArrayList();
for (FileGroup group : updatedFiles.getTopLevelGroups()) {
result.addAll(getUpdatedFiles(group));
}
return result;
}
@NotNull
private Collection<String> getUpdatedFiles(@NotNull FileGroup group) {
Function<String, String> getRelative = new Function<String, String>() {
@Override
public String fun(String path) {
return FileUtil.getRelativePath(new File(myProjectPath), new File(path));
}
};
Collection<String> result = ContainerUtil.newArrayList();
result.addAll(ContainerUtil.map(group.getFiles(), getRelative));
for (FileGroup child : group.getChildren()) {
result.addAll(getUpdatedFiles(child));
}
return result;
}
private void assertPushed(String expectedHash, String branch) {
cd(myParentRepo.getPath());
String actualHash = git("log -1 --pretty=%H " + branch);
assertEquals(expectedHash, actualHash);
}
private void assertBranchExists(String branch) {
cd(myParentRepo.getPath());
String out = git("branch");
assertTrue(out.contains(branch));
}
private static void assertUpstream(final String localBranch,
String expectedUpstreamRemote,
String expectedUpstreamBranch) {
String upstreamRemote = GitBranchUtil.stripRefsPrefix(git("config branch." + localBranch + ".remote"));
String upstreamBranch = GitBranchUtil.stripRefsPrefix(git("config branch." + localBranch + ".merge"));
assertEquals(expectedUpstreamRemote, upstreamRemote);
assertEquals(expectedUpstreamBranch, upstreamBranch);
}
}
@@ -0,0 +1,198 @@
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package git4idea.push;
import com.intellij.notification.Notification;
import com.intellij.notification.NotificationType;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.vcs.update.UpdatedFiles;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.containers.ContainerUtil;
import git4idea.GitBranch;
import git4idea.GitLocalBranch;
import git4idea.GitRemoteBranch;
import git4idea.GitStandardRemoteBranch;
import git4idea.repo.GitRemote;
import git4idea.repo.GitRepository;
import git4idea.test.GitPlatformTest;
import git4idea.test.MockGitRepository;
import git4idea.update.GitUpdateResult;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import static git4idea.push.GitPushNativeResult.Type.*;
public class GitPushResultNotificationTest extends GitPlatformTest {
private static Project ourProject;
@Override
public void setUp() throws Exception {
super.setUp();
ourProject = myProject;
}
@Override
public void tearDown() throws Exception {
ourProject = null;
super.tearDown();
}
public void test_single_success() {
GitPushResultNotification notification = notification(singleResult(SUCCESS, "master", "origin/master", 1));
assertNotification(NotificationType.INFORMATION, "Push successful", "Pushed 1 commit to origin/master", notification);
}
public void test_pushed_new_branch() {
GitPushResultNotification notification = notification(singleResult(NEW_REF, "feature", "origin/feature", -1));
assertNotification(NotificationType.INFORMATION, "Push successful", "Pushed feature to new branch origin/feature", notification);
}
public void test_force_pushed() {
GitPushResultNotification notification = notification(singleResult(FORCED_UPDATE, "feature", "origin/feature", -1));
assertNotification(NotificationType.INFORMATION, "Push successful", "Force pushed feature to origin/feature", notification);
}
public void test_success_and_fail() {
GitPushResultNotification notification = notification(new HashMap<GitRepository, GitPushRepoResult>() {{
put(repo("ultimate"), repoResult(SUCCESS, "master", "origin/master", 1));
put(repo("community"), repoResult(ERROR, "master", "origin/master", "Permission denied"));
}});
assertNotification(NotificationType.ERROR, "Push partially failed",
"ultimate: pushed 1 commit to origin/master<br/>" +
"community: failed with error: Permission denied", notification);
}
public void test_success_and_reject() {
GitPushResultNotification notification = notification(new HashMap<GitRepository, GitPushRepoResult>() {{
put(repo("ultimate"), repoResult(SUCCESS, "master", "origin/master", 1));
put(repo("community"), repoResult(REJECTED, "master", "origin/master", -1));
}});
assertNotification(NotificationType.WARNING, "Push partially rejected",
"ultimate: pushed 1 commit to origin/master<br/>" +
"community: push to origin/master was rejected", notification);
}
public void test_success_with_update() {
GitPushResultNotification notification = notification(singleResult(SUCCESS, "master", "origin/master", 2, GitUpdateResult.SUCCESS));
assertNotification(NotificationType.INFORMATION, "Push successful",
"Pushed 2 commits to origin/master<br/>" +
GitPushResultNotification.VIEW_FILES_UPDATED_DURING_THE_PUSH, notification);
}
public void test_success_and_resolved_conflicts() {
GitPushResultNotification notification = notification(new HashMap<GitRepository, GitPushRepoResult>() {{
put(repo("community"), repoResult(REJECTED, "master", "origin/master", -1, GitUpdateResult.SUCCESS_WITH_RESOLVED_CONFLICTS));
put(repo("contrib"), repoResult(REJECTED, "master", "origin/master", -1, GitUpdateResult.SUCCESS_WITH_RESOLVED_CONFLICTS));
put(repo("ultimate"), repoResult(SUCCESS, "master", "origin/master", 1));
}});
assertNotification(NotificationType.WARNING, "Push partially rejected",
"ultimate: pushed 1 commit to origin/master<br/>" +
"community: " + GitPushResultNotification.UPDATE_WITH_RESOLVED_CONFLICTS + "<br/>" +
"contrib: " + GitPushResultNotification.UPDATE_WITH_RESOLVED_CONFLICTS + "<br/>" +
GitPushResultNotification.VIEW_FILES_UPDATED_DURING_THE_PUSH, notification);
}
private static Map<GitRepository, GitPushRepoResult> singleResult(final GitPushNativeResult.Type type,
final String from,
final String to,
final int commits,
@Nullable final GitUpdateResult updateResult) {
return new HashMap<GitRepository, GitPushRepoResult>() {{
put(repo("community"), repoResult(type, from, to, commits, updateResult));
}};
}
private static GitPushRepoResult repoResult(GitPushNativeResult.Type nativeType, String from, String to, int commits) {
return repoResult(nativeType, from, to, commits, null);
}
private static GitPushRepoResult repoResult(GitPushNativeResult.Type nativeType, String from, String to, int commits,
@Nullable GitUpdateResult updateResult) {
GitPushNativeResult nr = new GitPushNativeResult(nativeType, "");
return GitPushRepoResult.addUpdateResult(GitPushRepoResult.convertFromNative(nr, commits, makeLocalBranch(from), makeRemoteBranch(to)),
updateResult);
}
private static Map<GitRepository, GitPushRepoResult> singleResult(final GitPushNativeResult.Type type,
final String from,
final String to, final int commits) {
return singleResult(type, from, to, commits, null);
}
// keep params for unification
@SuppressWarnings("UnusedParameters")
private static GitPushRepoResult repoResult(GitPushNativeResult.Type nativeType, String from, String to, String errorText) {
GitPushNativeResult nr1 = GitPushNativeResult.error(errorText);
return GitPushRepoResult.convertFromNative(nr1, -1, makeLocalBranch(from), makeRemoteBranch(to));
}
private static GitLocalBranch makeLocalBranch(String from) {
return new GitLocalBranch(from, GitBranch.DUMMY_HASH);
}
private static GitRemoteBranch makeRemoteBranch(String to) {
int firstSlash = to.indexOf('/');
GitRemote remote = new GitRemote(to.substring(0, firstSlash), Collections.<String>emptyList(), Collections.<String>emptyList(),
Collections.<String>emptyList(), Collections.<String>emptyList());
return new GitStandardRemoteBranch(remote, to.substring(firstSlash + 1), GitBranch.DUMMY_HASH);
}
private GitPushResultNotification notification(Map<GitRepository, GitPushRepoResult> map) {
boolean wasUpdatePerformed = ContainerUtil.exists(map.values(), new Condition<GitPushRepoResult>() {
@Override
public boolean value(GitPushRepoResult aNew) {
return aNew.getUpdateResult() != null;
}
});
UpdatedFiles updatedFiles = UpdatedFiles.create();
if (wasUpdatePerformed) {
updatedFiles.getTopLevelGroups().get(0).add("file.txt", "Git", null);
}
return GitPushResultNotification.create(myProject, new GitPushResult(map, updatedFiles, null, null), map.size() > 1);
}
private static MockGitRepository repo(final String name) {
VirtualFile root = ApplicationManager.getApplication().runWriteAction(new Computable<VirtualFile>() {
@Override
public VirtualFile compute() {
try {
return ourProject.getBaseDir().createChildData(null, name);
}
catch (IOException e) {
throw new RuntimeException(e);
}
}
});
return new MockGitRepository(ourProject, root);
}
private static void assertNotification(NotificationType type, String title, String content, Notification actual) {
assertEquals(type, actual.getType());
assertEquals(title, actual.getTitle());
assertEquals(content, actual.getContent());
}
}