From 98d5287aad0876cdc2b96cd914818ccf6908a465 Mon Sep 17 00:00:00 2001 From: Kirill Likhodedov Date: Wed, 17 Sep 2014 16:23:59 +0400 Subject: [PATCH] [git] Rewrite Git push procedure & use new VcsPushDialog --- .../com/intellij/openapi/vcs/VcsNotifier.java | 8 +- .../com/intellij/openapi/vcs/Executor.java | 4 + plugins/git4idea/src/META-INF/plugin.xml | 1 + .../src/git4idea/actions/GitPushAction.java | 70 ++- .../GitDeleteRemoteBranchOperation.java | 9 - .../checkin/GitCheckinEnvironment.java | 10 +- .../git4idea/src/git4idea/commands/Git.java | 18 +- .../src/git4idea/commands/GitImpl.java | 46 +- .../src/git4idea/push/GitCommitsByBranch.java | 89 --- .../push/GitCommitsByRepoAndBranch.java | 94 --- .../git4idea/push/GitManualPushToBranch.java | 245 -------- .../push/GitOutgoingCommitsProvider.java | 63 ++ .../src/git4idea/push/GitPushBranchInfo.java | 84 --- .../git4idea/push/GitPushBranchResult.java | 86 --- .../src/git4idea/push/GitPushDialog.java | 337 ----------- .../src/git4idea/push/GitPushInfo.java | 58 -- .../src/git4idea/push/GitPushLog.java | 418 -------------- .../git4idea/push/GitPushNativeResult.java | 77 +++ .../push/GitPushNativeResultParser.java | 132 +++++ .../src/git4idea/push/GitPushOperation.java | 370 ++++++++++++ .../push/GitPushRejectedDetector.java | 74 --- .../src/git4idea/push/GitPushRepoResult.java | 212 ++++--- .../src/git4idea/push/GitPushResult.java | 369 +----------- .../push/GitPushResultNotification.java | 219 +++++++ .../{GitPushSpec.java => GitPushSource.java} | 41 +- .../src/git4idea/push/GitPushSupport.java | 134 +++++ .../src/git4idea/push/GitPushTarget.java | 99 ++++ .../src/git4idea/push/GitPushTargetPanel.java | 205 +++++++ .../git4idea/src/git4idea/push/GitPusher.java | 542 +----------------- .../push/GitRejectedPushUpdateDialog.java | 2 +- .../git4idea/push/GitSimplePushResult.java | 92 --- .../src/git4idea/push/GroupedPushResult.java | 57 ++ .../src/git4idea/push/PushUpdateSettings.java | 45 ++ .../push/GitPushNativeResultParserTest.java | 101 ++++ .../push/GitPushOperationBaseTest.java | 160 ++++++ .../push/GitPushOperationMultiRepoTest.java | 123 ++++ .../push/GitPushOperationSingleRepoTest.java | 323 +++++++++++ .../push/GitPushResultNotificationTest.java | 198 +++++++ 38 files changed, 2564 insertions(+), 2651 deletions(-) delete mode 100644 plugins/git4idea/src/git4idea/push/GitCommitsByBranch.java delete mode 100644 plugins/git4idea/src/git4idea/push/GitCommitsByRepoAndBranch.java delete mode 100644 plugins/git4idea/src/git4idea/push/GitManualPushToBranch.java create mode 100644 plugins/git4idea/src/git4idea/push/GitOutgoingCommitsProvider.java delete mode 100644 plugins/git4idea/src/git4idea/push/GitPushBranchInfo.java delete mode 100644 plugins/git4idea/src/git4idea/push/GitPushBranchResult.java delete mode 100644 plugins/git4idea/src/git4idea/push/GitPushDialog.java delete mode 100644 plugins/git4idea/src/git4idea/push/GitPushInfo.java delete mode 100644 plugins/git4idea/src/git4idea/push/GitPushLog.java create mode 100644 plugins/git4idea/src/git4idea/push/GitPushNativeResult.java create mode 100644 plugins/git4idea/src/git4idea/push/GitPushNativeResultParser.java create mode 100644 plugins/git4idea/src/git4idea/push/GitPushOperation.java delete mode 100644 plugins/git4idea/src/git4idea/push/GitPushRejectedDetector.java create mode 100644 plugins/git4idea/src/git4idea/push/GitPushResultNotification.java rename plugins/git4idea/src/git4idea/push/{GitPushSpec.java => GitPushSource.java} (52%) create mode 100644 plugins/git4idea/src/git4idea/push/GitPushSupport.java create mode 100644 plugins/git4idea/src/git4idea/push/GitPushTarget.java create mode 100644 plugins/git4idea/src/git4idea/push/GitPushTargetPanel.java delete mode 100644 plugins/git4idea/src/git4idea/push/GitSimplePushResult.java create mode 100644 plugins/git4idea/src/git4idea/push/GroupedPushResult.java create mode 100644 plugins/git4idea/src/git4idea/push/PushUpdateSettings.java create mode 100644 plugins/git4idea/tests/git4idea/push/GitPushNativeResultParserTest.java create mode 100644 plugins/git4idea/tests/git4idea/push/GitPushOperationBaseTest.java create mode 100644 plugins/git4idea/tests/git4idea/push/GitPushOperationMultiRepoTest.java create mode 100644 plugins/git4idea/tests/git4idea/push/GitPushOperationSingleRepoTest.java create mode 100644 plugins/git4idea/tests/git4idea/push/GitPushResultNotificationTest.java diff --git a/platform/vcs-impl/src/com/intellij/openapi/vcs/VcsNotifier.java b/platform/vcs-impl/src/com/intellij/openapi/vcs/VcsNotifier.java index 985f383ecbc6..ff2b490bf26c 100644 --- a/platform/vcs-impl/src/com/intellij/openapi/vcs/VcsNotifier.java +++ b/platform/vcs-impl/src/com/intellij/openapi/vcs/VcsNotifier.java @@ -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; diff --git a/platform/vcs-impl/testSrc/com/intellij/openapi/vcs/Executor.java b/platform/vcs-impl/testSrc/com/intellij/openapi/vcs/Executor.java index 388b9e05fe56..91ef3dd1716d 100644 --- a/platform/vcs-impl/testSrc/com/intellij/openapi/vcs/Executor.java +++ b/platform/vcs-impl/testSrc/com/intellij/openapi/vcs/Executor.java @@ -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); diff --git a/plugins/git4idea/src/META-INF/plugin.xml b/plugins/git4idea/src/META-INF/plugin.xml index d26f434a9972..f0d8fa5ff5e1 100644 --- a/plugins/git4idea/src/META-INF/plugin.xml +++ b/plugins/git4idea/src/META-INF/plugin.xml @@ -141,6 +141,7 @@ + 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 gitRoots, - @NotNull final VirtualFile defaultRoot, - final Set affectedRoots, - final List exceptions) throws VcsException { - GitPusher.showPushDialogAndPerformPush(project, ServiceManager.getService(project, GitPlatformFacade.class)); + @NotNull + private static Collection collectRepositories(@NotNull Project project, + @Nullable VirtualFile[] files) { + GitRepositoryManager manager = GitUtil.getRepositoryManager(project); + Collection 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()); } } diff --git a/plugins/git4idea/src/git4idea/branch/GitDeleteRemoteBranchOperation.java b/plugins/git4idea/src/git4idea/branch/GitDeleteRemoteBranchOperation.java index 46630d0e1125..8aa308623dbd 100644 --- a/plugins/git4idea/src/git4idea/branch/GitDeleteRemoteBranchOperation.java +++ b/plugins/git4idea/src/git4idea/branch/GitDeleteRemoteBranchOperation.java @@ -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.emptyList() : Collections.singletonList(result.getOutput()), - success ? Collections.singletonList(result.getOutput()) : Collections.emptyList(), null); - } - @Nullable private static GitRemote getRemoteByName(@NotNull GitRepository repository, @NotNull String remoteName) { for (GitRemote remote : repository.getRemotes()) { diff --git a/plugins/git4idea/src/git4idea/checkin/GitCheckinEnvironment.java b/plugins/git4idea/src/git4idea/checkin/GitCheckinEnvironment.java index 448dd25ef7ab..705e40384c9a 100644 --- a/plugins/git4idea/src/git4idea/checkin/GitCheckinEnvironment.java +++ b/plugins/git4idea/src/git4idea/checkin/GitCheckinEnvironment.java @@ -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 repositories = GitUtil.getRepositoriesFromRoots(manager, sortedChanges.keySet()); + final List preselectedRepositories = ContainerUtil.newArrayList(repositories); UIUtil.invokeLaterIfNeeded(new Runnable() { public void run() { - GitPusher.showPushDialogAndPerformPush(myProject, ServiceManager.getService(myProject, GitPlatformFacade.class)); + new VcsPushDialog(myProject, preselectedRepositories).show(); } }); } diff --git a/plugins/git4idea/src/git4idea/commands/Git.java b/plugins/git4idea/src/git4idea/commands/Git.java index 830f73b4508f..aa0018194396 100644 --- a/plugins/git4idea/src/git4idea/commands/Git.java +++ b/plugins/git4idea/src/git4idea/commands/Git.java @@ -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); diff --git a/plugins/git4idea/src/git4idea/commands/GitImpl.java b/plugins/git4idea/src/git4idea/commands/GitImpl.java index 09e254f5392b..94464ae0368b 100644 --- a/plugins/git4idea/src/git4idea/commands/GitImpl.java +++ b/plugins/git4idea/src/git4idea/commands/GitImpl.java @@ -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() { @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 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 diff --git a/plugins/git4idea/src/git4idea/push/GitCommitsByBranch.java b/plugins/git4idea/src/git4idea/push/GitCommitsByBranch.java deleted file mode 100644 index dd19d44aa8f6..000000000000 --- a/plugins/git4idea/src/git4idea/push/GitCommitsByBranch.java +++ /dev/null @@ -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 myCommitsByBranch; - - GitCommitsByBranch(@NotNull Map commitsByBranch) { - myCommitsByBranch = new HashMap(commitsByBranch); - } - - GitCommitsByBranch(GitCommitsByBranch commitsByBranch) { - this(new HashMap(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 getBranches() { - return new HashSet(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 res = new HashMap(); - if (myCommitsByBranch.containsKey(branch)) { - res.put(branch, myCommitsByBranch.get(branch)); - } - return new GitCommitsByBranch(res); - } - - @NotNull - public Collection getAllCommits() { - Collection commits = new ArrayList(); - for (GitPushBranchInfo branchInfo : myCommitsByBranch.values()) { - commits.addAll(branchInfo.getCommits()); - } - return commits; - } -} diff --git a/plugins/git4idea/src/git4idea/push/GitCommitsByRepoAndBranch.java b/plugins/git4idea/src/git4idea/push/GitCommitsByRepoAndBranch.java deleted file mode 100644 index 03f225d120e2..000000000000 --- a/plugins/git4idea/src/git4idea/push/GitCommitsByRepoAndBranch.java +++ /dev/null @@ -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 myCommitsByRepository; - - GitCommitsByRepoAndBranch(@NotNull Map commitsByRepository) { - myCommitsByRepository = commitsByRepository; - } - - @NotNull - static GitCommitsByRepoAndBranch empty() { - return new GitCommitsByRepoAndBranch(new HashMap()); - } - - @NotNull - Collection getRepositories() { - return new HashSet(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 repositories) { - Map commits = new HashMap(); - 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 repositoriesBranches) { - Map commits = new HashMap(); - 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 getAllCommits() { - Collection commits = new ArrayList(); - for (GitCommitsByBranch commitsByBranch : myCommitsByRepository.values()) { - commits.addAll(commitsByBranch.getAllCommits()); - } - return commits; - } - -} - diff --git a/plugins/git4idea/src/git4idea/push/GitManualPushToBranch.java b/plugins/git4idea/src/git4idea/push/GitManualPushToBranch.java deleted file mode 100644 index f21ce637c702..000000000000 --- a/plugins/git4idea/src/git4idea/push/GitManualPushToBranch.java +++ /dev/null @@ -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 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 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 getRemotesWithCommonNames(@NotNull Collection repositories) { - if (repositories.isEmpty()) { - return Collections.emptyList(); - } - Iterator iterator = repositories.iterator(); - List commonRemotes = new ArrayList(iterator.next().getRemotes()); - while (iterator.hasNext()) { - GitRepository repository = iterator.next(); - Collection remoteNames = getRemoteNames(repository); - for (Iterator commonIter = commonRemotes.iterator(); commonIter.hasNext(); ) { - GitRemote remote = commonIter.next(); - if (!remoteNames.contains(remote.getName())) { - commonIter.remove(); - } - } - } - return commonRemotes; - } - - @NotNull - private static Collection getRemoteNames(@NotNull GitRepository repository) { - Collection names = new ArrayList(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 myRemotes; - private JComboBox myRemoteCombobox; - - private RemoteSelector(@NotNull Collection 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); - } - } - -} diff --git a/plugins/git4idea/src/git4idea/push/GitOutgoingCommitsProvider.java b/plugins/git4idea/src/git4idea/push/GitOutgoingCommitsProvider.java new file mode 100644 index 000000000000..9b2559eb8b43 --- /dev/null +++ b/plugins/git4idea/src/git4idea/push/GitOutgoingCommitsProvider.java @@ -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 { + + @NotNull private final Project myProject; + + public GitOutgoingCommitsProvider(@NotNull Project project) { + myProject = project; + } + + @NotNull + @Override + public OutgoingResult getOutgoingCommits(@NotNull GitRepository repository, @NotNull PushSpec pushSpec, + boolean initial) { + String source = pushSpec.getSource().getBranch().getFullName(); + GitPushTarget target = pushSpec.getTarget(); + assert target != null; + String destination = target.getBranch().getFullName(); + try { + List 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.emptyList()); + } + catch (VcsException e) { + return new OutgoingResult(Collections.emptyList(), + Collections.singletonList(new VcsError(GitUtil.cleanupErrorPrefixes(e.getMessage())))); + } + } +} diff --git a/plugins/git4idea/src/git4idea/push/GitPushBranchInfo.java b/plugins/git4idea/src/git4idea/push/GitPushBranchInfo.java deleted file mode 100644 index 9eeeb2724103..000000000000 --- a/plugins/git4idea/src/git4idea/push/GitPushBranchInfo.java +++ /dev/null @@ -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 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 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 getCommits() { - return new ArrayList(myCommits); - } - - @NotNull - public GitLocalBranch getSourceBranch() { - return mySourceBranch; - } - - boolean isEmpty() { - return myCommits.isEmpty(); - } - -} diff --git a/plugins/git4idea/src/git4idea/push/GitPushBranchResult.java b/plugins/git4idea/src/git4idea/push/GitPushBranchResult.java deleted file mode 100644 index 4bf8891c9ab9..000000000000 --- a/plugins/git4idea/src/git4idea/push/GitPushBranchResult.java +++ /dev/null @@ -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 : ""; - } - -} diff --git a/plugins/git4idea/src/git4idea/push/GitPushDialog.java b/plugins/git4idea/src/git4idea/push/GitPushDialog.java deleted file mode 100644 index f12b7b407c89..000000000000 --- a/plugins/git4idea/src/git4idea/push/GitPushDialog.java +++ /dev/null @@ -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 myPushSpecs; - private final Collection myRepositories; - private final JBLoadingPanel myLoadingPanel; - private final Object COMMITS_LOADING_LOCK = new Object(); - private final GitManualPushToBranch myRefspecPanel; - private final AtomicReference myDestBranchInfoOnRefresh = new AtomicReference(); - - 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 getRepositoriesWithRemotes() { - List repositories = new ArrayList(); - 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 error = new AtomicReference(); - synchronized (COMMITS_LOADING_LOCK) { - error.set(collectInfoToPush()); - } - - final Couple 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 getRemoteAndTrackedBranchForCurrentBranch() { - if (myGitCommitsToPush != null) { - Collection 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 pushSpecsForCurrentOrEnteredBranches() throws VcsException { - Map defaultSpecs = new HashMap(); - 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 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 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 { - @Override public void consume(Boolean checked) { - if (checked) { - setOKActionEnabled(true); - } else { - Collection 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(); - } - } - -} diff --git a/plugins/git4idea/src/git4idea/push/GitPushInfo.java b/plugins/git4idea/src/git4idea/push/GitPushInfo.java deleted file mode 100644 index b73bd299085c..000000000000 --- a/plugins/git4idea/src/git4idea/push/GitPushInfo.java +++ /dev/null @@ -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 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 pushSpecs) { - myCommits = commits; - myPushSpecs = pushSpecs; - } - - @NotNull - public Map getPushSpecs() { - return myPushSpecs; - } - - @NotNull - public GitCommitsByRepoAndBranch getCommits() { - return myCommits; - } - - @NotNull - public GitPushInfo retain(Map repoBranchMap) { - return new GitPushInfo(myCommits.retainAll(repoBranchMap), new HashMap(myPushSpecs)); - } -} diff --git a/plugins/git4idea/src/git4idea/push/GitPushLog.java b/plugins/git4idea/src/git4idea/push/GitPushLog.java deleted file mode 100644 index e3659554bdf2..000000000000 --- a/plugins/git4idea/src/git4idea/push/GitPushLog.java +++ /dev/null @@ -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 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 repositories, @NotNull final Consumer 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(((GitCommit)nodeInfo).getChanges())); - return; - } - } - setDefaultEmptyText(); - myChangesBrowser.setChangesToDisplay(Collections.emptyList()); - } - }); - ToolTipManager.sharedInstance().registerComponent(myTree); - - myChangesBrowser = new ChangesBrowser(project, null, Collections.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 sortRepositories(@NotNull final GitCommitsByRepoAndBranch commits) { - List repos = new ArrayList(commits.getRepositories()); - Collections.sort(repos, new Comparator() { - @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 sortBranches(@NotNull Collection branches) { - List sortedBranches = new ArrayList(branches); - Collections.sort(sortedBranches, new Comparator() { - @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 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 selectedRepositories = new ArrayList(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 { - } -} diff --git a/plugins/git4idea/src/git4idea/push/GitPushNativeResult.java b/plugins/git4idea/src/git4idea/push/GitPushNativeResult.java new file mode 100644 index 000000000000..206d5311961b --- /dev/null +++ b/plugins/git4idea/src/git4idea/push/GitPushNativeResult.java @@ -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); + } +} diff --git a/plugins/git4idea/src/git4idea/push/GitPushNativeResultParser.java b/plugins/git4idea/src/git4idea/push/GitPushNativeResultParser.java new file mode 100644 index 000000000000..91eff9a72014 --- /dev/null +++ b/plugins/git4idea/src/git4idea/push/GitPushNativeResultParser.java @@ -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: + *
+ * 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:
+ *
+ *             <flag> \t <from>:<to> \t <summary> (<reason>)
+ *
+ * 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 .. in most cases, and
+ *     ... 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// 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// 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.
+ * 
+ */ +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 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; + } +} diff --git a/plugins/git4idea/src/git4idea/push/GitPushOperation.java b/plugins/git4idea/src/git4idea/push/GitPushOperation.java new file mode 100644 index 000000000000..18edf5ce9eb6 --- /dev/null +++ b/plugins/git4idea/src/git4idea/push/GitPushOperation.java @@ -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: + *
    + *
  • Calls push for the given repositories with given parameters;
  • + *
  • Collects results;
  • + *
  • If push is rejected, proposes to update via merge or rebase;
  • + *
  • Shows a notification about push result
  • + *
+ */ +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> 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> 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 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 preUpdatePositions = updateRootInfoAndRememberPositions(); + + final Map results = ContainerUtil.newHashMap(); + Map updatedRoots = ContainerUtil.newHashMap(); + + try { + Collection remainingRoots = myPushSpecs.keySet(); + for (int pushAttempt = 0; + pushAttempt < MAX_PUSH_ATTEMPTS && !remainingRoots.isEmpty(); + pushAttempt++, remainingRoots = getRejectedAndNotPushed(results)) { + Map 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 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 rejected) { + return ContainerUtil.exists(rejected.entrySet(), new Condition>() { + @Override + public boolean value(Map.Entry 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 getRejectedAndNotPushed(@NotNull final Map results) { + return ContainerUtil.filter(results.keySet(), new Condition() { + @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 updateRootInfoAndRememberPositions() { + return ContainerUtil.map2Map(myPushSpecs.keySet(), + new Function>() { + @Override + public Pair fun(GitRepository repository) { + repository.update(); + return Pair.create(repository, repository.getCurrentRevision()); + } + }); + } + + private GitPushResult prepareCombinedResult(final Map allRoots, + final Map updatedRoots, + final Map preUpdatePositions, + Label beforeUpdateLabel, + Label afterUpdateLabel) { + Map results = ContainerUtil.newHashMap(); + UpdatedFiles updatedFiles = UpdatedFiles.create(); + for (Map.Entry 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 push(@NotNull Collection repositories) { + Map results = ContainerUtil.newLinkedHashMap(); + for (GitRepository repository : repositories) { + PushSpec 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 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 exceptions = new ArrayList(); + collector.collect(updatedFiles, exceptions); + for (VcsException exception : exceptions) { + LOG.info(exception); + } + } + + @NotNull + private GitPushNativeResult doPush(@NotNull GitRepository repository, @NotNull PushSpec 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() { + @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 repositories, + @NotNull final PushUpdateSettings initialSettings) { + final Ref 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 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(rootsToUpdate), UpdatedFiles.create()).update(um); + for (GitRepository repository : rootsToUpdate) { + repository.getRoot().refresh(true, true); + repository.update(); + } + return updateResult; + } + +} diff --git a/plugins/git4idea/src/git4idea/push/GitPushRejectedDetector.java b/plugins/git4idea/src/git4idea/push/GitPushRejectedDetector.java deleted file mode 100644 index ca96475cfeec..000000000000 --- a/plugins/git4idea/src/git4idea/push/GitPushRejectedDetector.java +++ /dev/null @@ -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 myRejectedRefs = new ArrayList(); - - @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 getRejectedBranches() { - Collection branches = new ArrayList(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; - } - } -} diff --git a/plugins/git4idea/src/git4idea/push/GitPushRepoResult.java b/plugins/git4idea/src/git4idea/push/GitPushRepoResult.java index 361fdade3398..360d3dbe20c2 100644 --- a/plugins/git4idea/src/git4idea/push/GitPushRepoResult.java +++ b/plugins/git4idea/src/git4idea/push/GitPushRepoResult.java @@ -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. + *

+ * 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 myBranchResults; + static Comparator TYPE_COMPARATOR = new Comparator() { + @Override + public int compare(Type o1, Type o2) { + return o1.ordinal() - o2.ordinal(); + } + }; - GitPushRepoResult(@NotNull Type type, @NotNull Map 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 resultsByBranch, @NotNull String output) { - return new GitPushRepoResult(Type.SUCCESS, resultsByBranch, output); - } - - @NotNull - static GitPushRepoResult error(@NotNull Map resultsByBranch, @NotNull String output) { - return new GitPushRepoResult(Type.ERROR, resultsByBranch, output); - } - - @NotNull - static GitPushRepoResult someRejected(@NotNull Map 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.emptyMap(), output); - } - - @NotNull - public static GitPushRepoResult notAuthorized(@NotNull String output) { - return new GitPushRepoResult(Type.NOT_AUTHORIZED, Collections.emptyMap(), output); - } - - @NotNull - static GitPushRepoResult notPushed() { - return new GitPushRepoResult(Type.NOT_PUSHING, Collections.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 getBranchResults() { - return myBranchResults; - } - - @NotNull - GitPushRepoResult remove(@NotNull GitBranch branch) { - Map resultsByBranch = new HashMap(); - for (Map.Entry 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 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 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("
"); - } + 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); } - } diff --git a/plugins/git4idea/src/git4idea/push/GitPushResult.java b/plugins/git4idea/src/git4idea/push/GitPushResult.java index 1d105eaea0f1..e4fc19a2bc33 100644 --- a/plugins/git4idea/src/git4idea/push/GitPushResult.java +++ b/plugins/git4idea/src/git4idea/push/GitPushResult.java @@ -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 myResults; + @NotNull private final UpdatedFiles myUpdatedFiles; + @Nullable private final Label myBeforeUpdateLabel; + @Nullable private final Label myAfterUpdateLabel; - private final Project myProject; - private final Map myResults = new HashMap(); - private final Map myUpdateStarts = new HashMap(); - private Label myBeforeUpdateLabel; - - GitPushResult(@NotNull Project project) { - this(project, null, new HashMap()); - } - - private GitPushResult(@NotNull Project project, @Nullable Label beforeUpdateLabel, @NotNull Map updateStarts) { - myProject = project; - myBeforeUpdateLabel = beforeUpdateLabel; - for (Map.Entry 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 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 results, + @NotNull UpdatedFiles files, + @Nullable Label beforeUpdateLabel, + @Nullable Label afterUpdateLabel) { + myResults = results; + myUpdatedFiles = files; + myAfterUpdateLabel = beforeUpdateLabel; + myBeforeUpdateLabel = afterUpdateLabel; } @NotNull - private GroupedResult group() { - final Map successfulResults = new HashMap(); - final Map errorResults = new HashMap(); - final Map rejectedResults = new HashMap(); - - for (Map.Entry 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 getResults() { + return myResults; } @NotNull - GitPushResult remove(@NotNull Map repoBranchPairs) { - GitPushResult result = new GitPushResult(myProject, myBeforeUpdateLabel, myUpdateStarts); - for (Map.Entry 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 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 entry : previousResult.myUpdateStarts.entrySet()) { - myUpdateStarts.put(entry.getKey(), entry.getValue()); - } - myBeforeUpdateLabel = previousResult.myBeforeUpdateLabel; + @Nullable + public Label getBeforeUpdateLabel() { + return myBeforeUpdateLabel; } - @NotNull - Map getRejectedPushesFromCurrentBranchToTrackedBranch(GitPushInfo pushInfo) { - final Map rejectedPushesForCurrentBranch = new HashMap(); - for (Map.Entry 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("View files updated during the push"); - } - - 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 updatedRepository : myUpdateStarts.entrySet()) { - GitRepository repository = updatedRepository.getKey(); - final MergeChangeCollector collector = new MergeChangeCollector(myProject, repository.getRoot(), updatedRepository.getValue()); - final ArrayList exceptions = new ArrayList(); - collector.collect(updatedFiles, exceptions); - for (VcsException exception : exceptions) { - LOG.info(exception); - } - } - return updatedFiles; - } - - private static int calcPushedCommitTotalNumber(@NotNull Map 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 groupResult, @NotNull GroupedResult.Type resultType) { - StringBuilder sb = new StringBuilder(); - for (Map.Entry entry : groupResult.entrySet()) { - GitRepository repository = entry.getKey(); - GitPushRepoResult result = entry.getValue(); - - if (!GitUtil.justOneGitRepository(myProject)) { - sb.append("" + repository.getPresentableUrl() + ":
"); - } - if (resultType == GroupedResult.Type.SUCCESS || resultType == GroupedResult.Type.REJECT) { - sb.append(result.getPerBranchesNonErrorReport()); - } else { - sb.append(result.getOutput()); - } - sb.append("
"); - } - return sb.toString(); - } - - - private static class GroupedResult { - enum Type { - SUCCESS, - REJECT, - ERROR - } - - private final Map mySuccessfulResults; - private final Map myErrorResults; - private final Map myRejectedResults; - - GroupedResult(@NotNull Map successfulResults, - @NotNull Map errorResults, - @NotNull Map 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; } } diff --git a/plugins/git4idea/src/git4idea/push/GitPushResultNotification.java b/plugins/git4idea/src/git4idea/push/GitPushResultNotification.java new file mode 100644 index 000000000000..2637041926b4 --- /dev/null +++ b/plugins/git4idea/src/git4idea/push/GitPushResultNotification.java @@ -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 = "View files updated during the push"; + + public static final String UPDATE_WITH_RESOLVED_CONFLICTS = "push has been cancelled, because there were conflicts during update.
" + + "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.
" + + "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 += "
" + 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 results, final boolean multiRepoProject) { + List> entries = ContainerUtil.sorted(results.entrySet(), + new Comparator>() { + @Override + public int compare(Map.Entry o1, Map.Entry 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, String>() { + @Override + public String fun(Map.Entry 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; + } + }, "
"); + } + + 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()); + } + } + } + } + + +} diff --git a/plugins/git4idea/src/git4idea/push/GitPushSpec.java b/plugins/git4idea/src/git4idea/push/GitPushSource.java similarity index 52% rename from plugins/git4idea/src/git4idea/push/GitPushSpec.java rename to plugins/git4idea/src/git4idea/push/GitPushSource.java index 11beefbd3043..146c854564a8 100644 --- a/plugins/git4idea/src/git4idea/push/GitPushSpec.java +++ b/plugins/git4idea/src/git4idea/push/GitPushSource.java @@ -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; } } diff --git a/plugins/git4idea/src/git4idea/push/GitPushSupport.java b/plugins/git4idea/src/git4idea/push/GitPushSupport.java new file mode 100644 index 000000000000..8c948af7635f --- /dev/null +++ b/plugins/git4idea/src/git4idea/push/GitPushSupport.java @@ -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 { + + private static final Logger LOG = Logger.getInstance(GitPushSupport.class); + + @NotNull private final GitRepositoryManager myRepositoryManager; + @NotNull private final GitVcs myVcs; + @NotNull private final Pusher myPusher; + @NotNull private final OutgoingCommitsProvider 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 getPusher() { + return myPusher; + } + + @NotNull + @Override + public OutgoingCommitsProvider 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 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 getRepositoryManager() { + return myRepositoryManager; + } + + @NotNull + @Override + public PushTargetPanel createTargetPanel(@NotNull GitRepository repository, @Nullable GitPushTarget defaultTarget) { + return new GitPushTargetPanel(repository, defaultTarget); + } + +} diff --git a/plugins/git4idea/src/git4idea/push/GitPushTarget.java b/plugins/git4idea/src/git4idea/push/GitPushTarget.java new file mode 100644 index 000000000000..084eabbad826 --- /dev/null +++ b/plugins/git4idea/src/git4idea/push/GitPushTarget.java @@ -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 remotes, @NotNull final String candidate) { + return ContainerUtil.find(remotes, new Condition() { + @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() { + @Override + public boolean value(GitRemoteBranch remoteBranch) { + return remoteBranch.getRemote().equals(remote) && + remoteBranch.getNameForRemoteOperations().equals(GitBranchUtil.stripRefsPrefix(nameAtRemote)); + } + }); + } + +} diff --git a/plugins/git4idea/src/git4idea/push/GitPushTargetPanel.java b/plugins/git4idea/src/git4idea/push/GitPushTargetPanel.java new file mode 100644 index 000000000000..aa8ce0b54287 --- /dev/null +++ b/plugins/git4idea/src/git4idea/push/GitPushTargetPanel.java @@ -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 { + + private static final Logger LOG = Logger.getInstance(GitPushTargetPanel.class); + + private static final Comparator 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 remotes = ContainerUtil.map(myRepository.getRemotes(), new Function() { + @Override + public String fun(GitRemote remote) { + return remote.getName(); + } + }); + + ListPopup popup = JBPopupFactory.getInstance().createListPopup(new BaseListPopupStep(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 getTargetNames(@NotNull GitRepository repository) { + List remoteBranches = ContainerUtil.sorted(repository.getBranches().getRemoteBranches(), REMOTE_BRANCH_COMPARATOR); + return ContainerUtil.map(remoteBranches, new Function() { + @Override + public String fun(GitRemoteBranch branch) { + return branch.getNameForRemoteOperations(); + } + }); + } + + private static class MyRemoteBranchComparator implements Comparator { + @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()); + } + } +} diff --git a/plugins/git4idea/src/git4idea/push/GitPusher.java b/plugins/git4idea/src/git4idea/push/GitPusher.java index 1dfcdfaf327d..06e613715dec 100644 --- a/plugins/git4idea/src/git4idea/push/GitPusher.java +++ b/plugins/git4idea/src/git4idea/push/GitPusher.java @@ -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 { @NotNull private final Project myProject; @NotNull private final GitRepositoryManager myRepositoryManager; - @NotNull private final ProgressIndicator myProgressIndicator; - @NotNull private final Collection 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 pushSpecs) throws VcsException { - Map> reposAndBranchesToPush = prepareRepositoriesAndBranchesToPush(pushSpecs); - - Map commitsByRepoAndBranch = new HashMap(); - for (GitRepository repository : myRepositories) { - List branchPairs = reposAndBranchesToPush.get(repository); - if (branchPairs == null) { - continue; - } - GitCommitsByBranch commitsByBranch = collectsCommitsToPush(repository, branchPairs); - commitsByRepoAndBranch.put(repository, commitsByBranch); - } - return new GitCommitsByRepoAndBranch(commitsByRepoAndBranch); - } - - @NotNull - private Map> prepareRepositoriesAndBranchesToPush(@NotNull Map pushSpecs) throws VcsException { - Map> res = new HashMap>(); - 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 sourcesDestinations) - throws VcsException { - Map commitsByBranch = new HashMap(); - - 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 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 collectRecentCommitsOnBranch(GitRepository repository, GitBranch source) throws VcsException { - return GitHistoryUtils.history(myProject, repository.getRoot(), "--max-count=" + RECENT_COMMITS_NUMBER, source.getName()); - } - - @NotNull - private List 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 rejectedBranches = simplePushResult.getRejectedBranches(); - - Map resultMap = new HashMap(); - 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 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 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 resultMap = new HashMap(); - 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 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 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 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 rejectedPushesForCurrentBranch, - final UpdateSettings initialSettings) { - final AtomicReference updateSettings = new AtomicReference(); - 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 getRootsToUpdate(@NotNull Map rejectedPushesForCurrentBranch, boolean updateAllRoots) { - return updateAllRoots ? myRepositories : rejectedPushesForCurrentBranch.keySet(); - } - - private boolean update(@NotNull Collection 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(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.
" + - "Resolve the conflicts and invoke push again."; - } - else { - description = "Push has been cancelled, because there were conflicts during update.
" + - "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> 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); } } diff --git a/plugins/git4idea/src/git4idea/push/GitRejectedPushUpdateDialog.java b/plugins/git4idea/src/git4idea/push/GitRejectedPushUpdateDialog.java index 2a00d629b6a1..3ee56d2fb5ef 100644 --- a/plugins/git4idea/src/git4idea/push/GitRejectedPushUpdateDialog.java +++ b/plugins/git4idea/src/git4idea/push/GitRejectedPushUpdateDialog.java @@ -58,7 +58,7 @@ class GitRejectedPushUpdateDialog extends DialogWrapper { protected GitRejectedPushUpdateDialog(@NotNull Project project, @NotNull Collection repositories, - @NotNull GitPusher.UpdateSettings initialSettings) { + @NotNull PushUpdateSettings initialSettings) { super(project); myProject = project; myRepositories = repositories; diff --git a/plugins/git4idea/src/git4idea/push/GitSimplePushResult.java b/plugins/git4idea/src/git4idea/push/GitSimplePushResult.java deleted file mode 100644 index 2cd7baeec717..000000000000 --- a/plugins/git4idea/src/git4idea/push/GitSimplePushResult.java +++ /dev/null @@ -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 myRejectedBranches; - - public enum Type { - NOT_PUSHED, - SUCCESS, - REJECT, - CANCEL, - NOT_AUTHORIZED, - ERROR - } - - private GitSimplePushResult(@NotNull Type type, @NotNull String errorOutput, @NotNull Collection rejectedBranches) { - myType = type; - myErrorOutput = errorOutput; - myRejectedBranches = rejectedBranches; - } - - @NotNull - public static GitSimplePushResult success() { - return new GitSimplePushResult(Type.SUCCESS, "", Collections.emptyList()); - } - - @NotNull - public static GitSimplePushResult notPushed() { - return new GitSimplePushResult(Type.NOT_PUSHED, "", Collections.emptyList()); - } - - @NotNull - public static GitSimplePushResult cancel() { - return new GitSimplePushResult(Type.CANCEL, "Cancelled by user", Collections.emptyList()); - } - - @NotNull - public static GitSimplePushResult notAuthorized() { - return new GitSimplePushResult(Type.NOT_AUTHORIZED, "Couldn't authorize", Collections.emptyList()); - } - - @NotNull - public static GitSimplePushResult reject(@NotNull Collection rejectedBranches) { - return new GitSimplePushResult(Type.REJECT, "", rejectedBranches); - } - - @NotNull - public static GitSimplePushResult error(@NotNull String errorOutput) { - return new GitSimplePushResult(Type.ERROR, errorOutput, Collections.emptyList()); - } - - @NotNull - public String getOutput() { - return myErrorOutput; - } - - @NotNull - public Collection getRejectedBranches() { - return myRejectedBranches; - } - - @NotNull - public Type getType() { - return myType; - } - -} diff --git a/plugins/git4idea/src/git4idea/push/GroupedPushResult.java b/plugins/git4idea/src/git4idea/push/GroupedPushResult.java new file mode 100644 index 000000000000..4ea030bfaf8d --- /dev/null +++ b/plugins/git4idea/src/git4idea/push/GroupedPushResult.java @@ -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 successful; + final Map errors; + final Map rejected; + + private GroupedPushResult(Map successful, + Map errors, + Map rejected) { + this.successful = successful; + this.errors = errors; + this.rejected = rejected; + } + + static GroupedPushResult group(Map results) { + Map successful = ContainerUtil.newHashMap(); + Map rejected = ContainerUtil.newHashMap(); + Map errors = ContainerUtil.newHashMap(); + for (Map.Entry 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); + } +} diff --git a/plugins/git4idea/src/git4idea/push/PushUpdateSettings.java b/plugins/git4idea/src/git4idea/push/PushUpdateSettings.java new file mode 100644 index 000000000000..d4eafa913d20 --- /dev/null +++ b/plugins/git4idea/src/git4idea/push/PushUpdateSettings.java @@ -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); + } +} diff --git a/plugins/git4idea/tests/git4idea/push/GitPushNativeResultParserTest.java b/plugins/git4idea/tests/git4idea/push/GitPushNativeResultParserTest.java new file mode 100644 index 000000000000..ae527322bb37 --- /dev/null +++ b/plugins/git4idea/tests/git4idea/push/GitPushNativeResultParserTest.java @@ -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()); + } + +} \ No newline at end of file diff --git a/plugins/git4idea/tests/git4idea/push/GitPushOperationBaseTest.java b/plugins/git4idea/tests/git4idea/push/GitPushOperationBaseTest.java new file mode 100644 index 000000000000..f08ee259f0da --- /dev/null +++ b/plugins/git4idea/tests/git4idea/push/GitPushOperationBaseTest.java @@ -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 getDebugLogCategories() { + return Collections.singletonList("#" + GitPushOperation.class.getName()); + } + + @NotNull + protected Trinity 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 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(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()); + } + +} diff --git a/plugins/git4idea/tests/git4idea/push/GitPushOperationMultiRepoTest.java b/plugins/git4idea/tests/git4idea/push/GitPushOperationMultiRepoTest.java new file mode 100644 index 000000000000..cb5cd440e255 --- /dev/null +++ b/plugins/git4idea/tests/git4idea/push/GitPushOperationMultiRepoTest.java @@ -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 mainRepo = setupRepositories(myProjectPath, "parent", "bro"); + myRepository = mainRepo.first; + + File community = new File(myProjectPath, "community"); + assertTrue(community.mkdir()); + Trinity 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 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.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() { + @Override + public boolean value(GitRepository repository) { + return repository.equals(myRepository); + } + }; + + cd(myRepository); + makeCommit("file.txt"); + cd(myCommunity); + makeCommit("com.txt"); + + PushSpec spec1 = makePushSpec(myRepository, "master", "origin/master"); + PushSpec spec2 = makePushSpec(myCommunity, "master", "origin/master"); + Map> 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); + } + +} diff --git a/plugins/git4idea/tests/git4idea/push/GitPushOperationSingleRepoTest.java b/plugins/git4idea/tests/git4idea/push/GitPushOperationSingleRepoTest.java new file mode 100644 index 000000000000..8974ccc9f260 --- /dev/null +++ b/plugins/git4idea/tests/git4idea/push/GitPushOperationSingleRepoTest.java @@ -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 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 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 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 pushSpec = makePushSpec(myRepository, "master", "origin/master"); + + GitPushResult result = new GitPushOperation(myProject, Collections.singletonMap(myRepository, pushSpec), false) { + @NotNull + @Override + protected GitUpdateResult update(@NotNull Collection 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 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 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 getUpdatedFiles(@NotNull UpdatedFiles updatedFiles) { + Collection result = ContainerUtil.newArrayList(); + for (FileGroup group : updatedFiles.getTopLevelGroups()) { + result.addAll(getUpdatedFiles(group)); + } + return result; + } + + @NotNull + private Collection getUpdatedFiles(@NotNull FileGroup group) { + Function getRelative = new Function() { + @Override + public String fun(String path) { + return FileUtil.getRelativePath(new File(myProjectPath), new File(path)); + } + }; + Collection 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); + } + +} \ No newline at end of file diff --git a/plugins/git4idea/tests/git4idea/push/GitPushResultNotificationTest.java b/plugins/git4idea/tests/git4idea/push/GitPushResultNotificationTest.java new file mode 100644 index 000000000000..2926c94c63a4 --- /dev/null +++ b/plugins/git4idea/tests/git4idea/push/GitPushResultNotificationTest.java @@ -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() {{ + 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
" + + "community: failed with error: Permission denied", notification); + } + + public void test_success_and_reject() { + GitPushResultNotification notification = notification(new HashMap() {{ + 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
" + + "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
" + + GitPushResultNotification.VIEW_FILES_UPDATED_DURING_THE_PUSH, notification); + + } + + public void test_success_and_resolved_conflicts() { + GitPushResultNotification notification = notification(new HashMap() {{ + 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
" + + "community: " + GitPushResultNotification.UPDATE_WITH_RESOLVED_CONFLICTS + "
" + + "contrib: " + GitPushResultNotification.UPDATE_WITH_RESOLVED_CONFLICTS + "
" + + GitPushResultNotification.VIEW_FILES_UPDATED_DURING_THE_PUSH, notification); + + } + + private static Map singleResult(final GitPushNativeResult.Type type, + final String from, + final String to, + final int commits, + @Nullable final GitUpdateResult updateResult) { + return new HashMap() {{ + 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 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.emptyList(), Collections.emptyList(), + Collections.emptyList(), Collections.emptyList()); + return new GitStandardRemoteBranch(remote, to.substring(firstSlash + 1), GitBranch.DUMMY_HASH); + } + + private GitPushResultNotification notification(Map map) { + boolean wasUpdatePerformed = ContainerUtil.exists(map.values(), new Condition() { + @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() { + @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()); + } + +} \ No newline at end of file