diff --git a/plugins/git4idea/src/META-INF/plugin.xml b/plugins/git4idea/src/META-INF/plugin.xml
index 63481329cdda..cf7858712934 100644
--- a/plugins/git4idea/src/META-INF/plugin.xml
+++ b/plugins/git4idea/src/META-INF/plugin.xml
@@ -211,10 +211,10 @@
-
+
diff --git a/plugins/git4idea/src/git4idea/actions/GitFetch.java b/plugins/git4idea/src/git4idea/actions/GitFetch.java
index 1ddf44335499..bf71fede4b5e 100644
--- a/plugins/git4idea/src/git4idea/actions/GitFetch.java
+++ b/plugins/git4idea/src/git4idea/actions/GitFetch.java
@@ -9,11 +9,13 @@ import git4idea.GitUtil;
import git4idea.GitVcs;
import git4idea.i18n.GitBundle;
import git4idea.repo.GitRepositoryManager;
-import git4idea.update.GitFetcher;
import org.jetbrains.annotations.NotNull;
import java.util.List;
+import static git4idea.GitUtil.getRepositoriesFromRoots;
+import static git4idea.fetch.GitFetchSupport.fetchSupport;
+
public class GitFetch extends GitRepositoryAction {
@Override
@NotNull
@@ -29,8 +31,7 @@ public class GitFetch extends GitRepositoryAction {
@Override
public void run(@NotNull ProgressIndicator indicator) {
GitRepositoryManager repositoryManager = GitUtil.getRepositoryManager(project);
- new GitFetcher(project, indicator, true).fetchRootsAndNotify(GitUtil.getRepositoriesFromRoots(repositoryManager, gitRoots),
- null, true);
+ fetchSupport(project).fetch(getRepositoriesFromRoots(repositoryManager, gitRoots)).showNotification();
}
});
}
diff --git a/plugins/git4idea/src/git4idea/fetch/GitFetchResult.java b/plugins/git4idea/src/git4idea/fetch/GitFetchResult.java
new file mode 100644
index 000000000000..be950cc65148
--- /dev/null
+++ b/plugins/git4idea/src/git4idea/fetch/GitFetchResult.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2000-2017 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.fetch;
+
+import org.jetbrains.annotations.NotNull;
+
+public interface GitFetchResult {
+ void showNotification();
+
+ boolean showNotificationIfFailed();
+
+ boolean showNotificationIfFailed(@NotNull String title);
+}
diff --git a/plugins/git4idea/src/git4idea/fetch/GitFetchSupport.java b/plugins/git4idea/src/git4idea/fetch/GitFetchSupport.java
new file mode 100644
index 000000000000..df6af2ec258d
--- /dev/null
+++ b/plugins/git4idea/src/git4idea/fetch/GitFetchSupport.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2000-2017 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.fetch;
+
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.project.Project;
+import git4idea.repo.GitRemote;
+import git4idea.repo.GitRepository;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * High-level API to execute the {@code git fetch} command.
+ */
+public interface GitFetchSupport {
+
+ /**
+ * For each given repository, fetches the "default" remote.
+ * The latter is identified by {@link #getDefaultRemoteToFetch}.
+ */
+ @NotNull
+ GitFetchResult fetch(@NotNull Collection repositories);
+
+ /**
+ * Fetches the given remote.
+ */
+ @NotNull
+ GitFetchResult fetch(@NotNull GitRepository repository, @NotNull GitRemote remote);
+
+ /**
+ * Fetches all specified remotes in the repository.
+ */
+ @NotNull
+ GitFetchResult fetch(@NotNull GitRepository repository, @NotNull List remotes);
+
+ /**
+ * Returns the default remote to fetch from, or null if there are no remotes in the repository,
+ * or if it is impossible to guess which remote is default.
+ */
+ @Nullable
+ GitRemote getDefaultRemoteToFetch(@NotNull GitRepository repository);
+
+ @NotNull
+ static GitFetchSupport fetchSupport(@NotNull Project project) {
+ return ServiceManager.getService(project, GitFetchSupport.class);
+ }
+}
diff --git a/plugins/git4idea/src/git4idea/fetch/GitFetchSupportImpl.kt b/plugins/git4idea/src/git4idea/fetch/GitFetchSupportImpl.kt
new file mode 100644
index 000000000000..6d52814a4669
--- /dev/null
+++ b/plugins/git4idea/src/git4idea/fetch/GitFetchSupportImpl.kt
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2000-2016 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.fetch
+
+import com.intellij.dvcs.MultiMessage
+import com.intellij.dvcs.MultiRootMessage
+import com.intellij.notification.NotificationType
+import com.intellij.openapi.diagnostic.logger
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.vcs.VcsNotifier
+import com.intellij.openapi.vcs.VcsNotifier.STANDARD_NOTIFICATION
+import git4idea.GitUtil.findRemoteByName
+import git4idea.GitUtil.mention
+import git4idea.commands.Git
+import git4idea.repo.GitRemote
+import git4idea.repo.GitRemote.ORIGIN
+import git4idea.repo.GitRepository
+import java.util.regex.Pattern
+
+private val LOG = logger()
+private val PRUNE_PATTERN = Pattern.compile("\\s*x\\s*\\[deleted\\].*->\\s*(\\S*)") // x [deleted] (none) -> origin/branch
+
+internal class GitFetchSupportImpl(val project: Project) : GitFetchSupport {
+
+ override fun getDefaultRemoteToFetch(repository: GitRepository): GitRemote? {
+ val remotes = repository.remotes
+ return when {
+ remotes.isEmpty() -> null
+ remotes.size == 1 -> remotes.first()
+ else -> {
+ // this emulates behavior of the native `git fetch`:
+ // if current branch doesn't give a hint, then return "origin"; if there is no "origin", don't guess and fail
+ repository.currentBranch?.findTrackedBranch(repository)?.remote ?: findRemoteByName(repository, ORIGIN)
+ }
+ }
+ }
+
+ override fun fetch(repositories: Collection): GitFetchResult {
+ val results = mutableMapOf()
+ for (repository in repositories) {
+ val remote = getDefaultRemoteToFetch(repository)
+ if (remote != null) results[repository] = doFetch(repository, listOf(remote))
+ else LOG.info("No remote to fetch found in $repository")
+ }
+ return resultOf(results)
+ }
+
+ override fun fetch(repository: GitRepository, remote: GitRemote): GitFetchResult {
+ return fetch(repository, listOf(remote))
+ }
+
+ override fun fetch(repository: GitRepository, remotes: List): GitFetchResult {
+ return resultOf(mapOf(Pair(repository, doFetch(repository, remotes))))
+ }
+
+ private fun doFetch(repository: GitRepository, remotes: List): RepoResult {
+ val results = mutableMapOf()
+ for (remote in remotes) {
+ results[remote] = doFetch(repository, remote)
+ }
+ return RepoResult(results)
+ }
+
+ private fun doFetch(repository: GitRepository, remote: GitRemote): SingleRemoteResult {
+ val result = Git.getInstance().fetch(repository, remote, emptyList())
+ val pruned = result.output.mapNotNull { getPrunedRef(it) }
+ val error = if (result.success()) null else result.errorOutputAsJoinedString
+ return SingleRemoteResult(error, pruned)
+ }
+
+ private fun getPrunedRef(line: String): String? {
+ val matcher = PRUNE_PATTERN.matcher(line)
+ return if (matcher.matches()) matcher.group(1) else null
+ }
+
+ private fun resultOf(results: Map) = FetchResultImpl(project, results)
+
+ private class RepoResult(val results: Map) {
+ /*
+ For simplicity, remote and repository results are merged separately.
+ It means that they are not merged, if two repositories have two remotes,
+ and then fetch succeeds for the first remote in both repos, and fails for the second remote in both repos.
+ Such cases are rare, and can be handled when actual problem is reported.
+ */
+
+ fun totallySuccessful() = results.values.all { it.success() }
+
+ fun error(): String? {
+ val errorMessage = multiRemoteMessage()
+ for ((remote, result) in results) {
+ if (result.error != null) errorMessage.append(remote, result.error)
+ }
+ return errorMessage.asString()
+ }
+
+ fun prunedRefs(): String {
+ val prunedRefs = multiRemoteMessage()
+ for ((remote, result) in results) {
+ if (result.prunedRefs.isNotEmpty()) prunedRefs.append(remote, result.prunedRefs.joinToString("\n"))
+ }
+ return prunedRefs.asString()
+ }
+
+ private fun multiRemoteMessage() = MultiMessage(results.keys, GitRemote::getName, GitRemote::getName)
+ }
+
+ private class SingleRemoteResult(val error: String?, val prunedRefs: List) {
+ fun success() = error == null
+ }
+
+ private class FetchResultImpl(val project: Project,
+ val results: Map) : GitFetchResult {
+
+ override fun showNotification() {
+ doShowNotification()
+ }
+
+ override fun showNotificationIfFailed(): Boolean {
+ return showNotificationIfFailed("Fetch Failed")
+ }
+
+ override fun showNotificationIfFailed(title: String): Boolean {
+ val failure = results.values.any { !it.totallySuccessful() }
+ if (failure) doShowNotification(title)
+ return !failure
+ }
+
+ private fun doShowNotification(failureTitle: String = "Fetch Failed") {
+ val roots = results.keys.map { it.root }
+ val errorMessage = MultiRootMessage(project, roots, true)
+ val prunedRefs = MultiRootMessage(project, roots, true)
+
+ val failed = results.filterValues { !it.totallySuccessful() }
+
+ for ((repo, result) in failed) {
+ if (result.error() != null) errorMessage.append(repo.root, result.error()!!)
+ }
+ for ((repo, result) in results) {
+ prunedRefs.append(repo.root, result.prunedRefs())
+ }
+
+ val type = if (failed.isEmpty()) NotificationType.INFORMATION else NotificationType.ERROR
+ val mentionFailedRepos = if (failed.size == roots.size) "" else mention(failed.keys)
+ val title = if (failed.isEmpty()) "Fetch Successful" else "$failureTitle$mentionFailedRepos"
+ val message = title + prefixWithBr(errorMessage.asString()) + prefixWithBr(prunedRefs.asString())
+ val notification = STANDARD_NOTIFICATION.createNotification("", message, type, null)
+ VcsNotifier.getInstance(project).notify(notification)
+ }
+
+ private fun prefixWithBr(text: String): String = if (text.isNotEmpty()) "
$text" else ""
+ }
+}
diff --git a/plugins/git4idea/src/git4idea/update/GitFetchResult.java b/plugins/git4idea/src/git4idea/update/GitFetchResult.java
index 9b7551b55078..0147915809bb 100644
--- a/plugins/git4idea/src/git4idea/update/GitFetchResult.java
+++ b/plugins/git4idea/src/git4idea/update/GitFetchResult.java
@@ -25,8 +25,9 @@ import static com.intellij.openapi.util.text.StringUtil.join;
import static com.intellij.openapi.util.text.StringUtil.pluralize;
/**
- * @author Kirill Likhodedov
+ * @deprecated Use {@link git4idea.fetch.GitFetchSupport}
*/
+@Deprecated
public final class GitFetchResult {
private final Type myType;
diff --git a/plugins/git4idea/src/git4idea/update/GitFetcher.java b/plugins/git4idea/src/git4idea/update/GitFetcher.java
index 6483d50f7d66..cf616b23f86e 100644
--- a/plugins/git4idea/src/git4idea/update/GitFetcher.java
+++ b/plugins/git4idea/src/git4idea/update/GitFetcher.java
@@ -33,6 +33,7 @@ import git4idea.branch.GitBranchUtil;
import git4idea.commands.Git;
import git4idea.commands.GitCommandResult;
import git4idea.commands.GitLineHandlerListener;
+import git4idea.fetch.GitFetchSupport;
import git4idea.repo.GitBranchTrackInfo;
import git4idea.repo.GitRemote;
import git4idea.repo.GitRepository;
@@ -51,6 +52,10 @@ import static git4idea.GitBranch.REFS_HEADS_PREFIX;
import static git4idea.GitBranch.REFS_REMOTES_PREFIX;
import static git4idea.commands.GitAuthenticationListener.GIT_AUTHENTICATION_SUCCESS;
+/**
+ * @deprecated Use {@link GitFetchSupport}
+ */
+@Deprecated
public class GitFetcher {
private static final Logger LOG = Logger.getInstance(GitFetcher.class);
@@ -78,7 +83,9 @@ public class GitFetcher {
/**
* Invokes 'git fetch'.
* @return true if fetch was successful, false in the case of error.
+ * @deprecated Use {@link GitFetchSupport}
*/
+ @Deprecated
public GitFetchResult fetch(@NotNull GitRepository repository) {
// TODO need to have a fair compound result here
GitFetchResult fetchResult = myFetchAll ? fetchAll(repository) : fetchCurrentRemote(repository);
@@ -87,6 +94,10 @@ public class GitFetcher {
return fetchResult;
}
+ /**
+ * @deprecated Use {@link GitFetchSupport}
+ */
+ @Deprecated
@NotNull
public GitFetchResult fetch(@NotNull VirtualFile root, @NotNull String remoteName, @Nullable String branch) {
GitRepository repository = myRepositoryManager.getRepositoryForRoot(root);
@@ -211,7 +222,7 @@ public class GitFetcher {
}
@NotNull
- public static String getFetchSpecForBranch(@NotNull String branch, @NotNull String remoteName) {
+ private static String getFetchSpecForBranch(@NotNull String branch, @NotNull String remoteName) {
branch = getRidOfPrefixIfExists(branch);
return REFS_HEADS_PREFIX + branch + ":" + REFS_REMOTES_PREFIX + remoteName + "/" + branch;
}
@@ -254,7 +265,9 @@ public class GitFetcher {
* Use this when fetch is a part of a compound process.
* @param notifySuccess if set to {@code true} successful notification will be displayed.
* @return true if all fetches were successful, false if at least one fetch failed.
+ * @deprecated Use {@link GitFetchSupport}
*/
+ @Deprecated
public boolean fetchRootsAndNotify(@NotNull Collection roots,
@Nullable String errorNotificationTitle, boolean notifySuccess) {
MultiRootMessage additionalInfo = new MultiRootMessage(myProject, GitUtil.getRootsFromRepositories(roots), true);
diff --git a/plugins/git4idea/src/git4idea/update/GitUpdateProcess.java b/plugins/git4idea/src/git4idea/update/GitUpdateProcess.java
index 805e3a06dab2..f078f61ab778 100644
--- a/plugins/git4idea/src/git4idea/update/GitUpdateProcess.java
+++ b/plugins/git4idea/src/git4idea/update/GitUpdateProcess.java
@@ -44,6 +44,7 @@ import java.util.*;
import static com.intellij.dvcs.DvcsUtil.getShortRepositoryName;
import static git4idea.GitUtil.getRootsFromRepositories;
import static git4idea.GitUtil.mention;
+import static git4idea.fetch.GitFetchSupport.fetchSupport;
import static git4idea.util.GitUIUtil.*;
/**
@@ -313,7 +314,7 @@ public class GitUpdateProcess {
// fetch all roots. If an error happens, return false and notify about errors.
private boolean fetchAndNotify(@NotNull Collection repositories) {
- return new GitFetcher(myProject, myProgressIndicator, false).fetchRootsAndNotify(repositories, "Update failed", false);
+ return fetchSupport(myProject).fetch(repositories).showNotificationIfFailed("Update failed");
}
/**
diff --git a/plugins/github/src/org/jetbrains/plugins/github/GithubCreatePullRequestWorker.java b/plugins/github/src/org/jetbrains/plugins/github/GithubCreatePullRequestWorker.java
index c8d82ba3e0b8..95aa56a7af8a 100644
--- a/plugins/github/src/org/jetbrains/plugins/github/GithubCreatePullRequestWorker.java
+++ b/plugins/github/src/org/jetbrains/plugins/github/GithubCreatePullRequestWorker.java
@@ -20,7 +20,10 @@ import com.intellij.dvcs.util.CommitCompareInfo;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.diagnostic.Logger;
-import com.intellij.openapi.progress.*;
+import com.intellij.openapi.progress.ProcessCanceledException;
+import com.intellij.openapi.progress.ProgressIndicator;
+import com.intellij.openapi.progress.ProgressManager;
+import com.intellij.openapi.progress.Task;
import com.intellij.openapi.progress.util.BackgroundTaskUtil;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Couple;
@@ -33,6 +36,7 @@ import com.intellij.vcs.log.VcsCommitMetadata;
import git4idea.DialogManager;
import git4idea.GitCommit;
import git4idea.GitLocalBranch;
+import git4idea.GitUtil;
import git4idea.changes.GitChangeUtils;
import git4idea.commands.Git;
import git4idea.commands.GitCommandResult;
@@ -40,8 +44,6 @@ import git4idea.history.GitHistoryUtils;
import git4idea.repo.GitRemote;
import git4idea.repo.GitRepository;
import git4idea.ui.branch.GitCompareBranchesHelper;
-import git4idea.update.GitFetchResult;
-import git4idea.update.GitFetcher;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.github.api.GithubApiRequestExecutor;
@@ -67,6 +69,8 @@ import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
+import static git4idea.fetch.GitFetchSupport.fetchSupport;
+
public class GithubCreatePullRequestWorker {
private static final Logger LOG = GithubUtil.LOG;
private static final String CANNOT_CREATE_PULL_REQUEST = "Can't Create Pull Request";
@@ -331,13 +335,13 @@ public class GithubCreatePullRequestWorker {
}
private void doFetchRemote(@NotNull ForkInfo fork) {
- if (fork.getRemoteName() == null) return;
-
- GitFetchResult result =
- new GitFetcher(myProject, new EmptyProgressIndicator(), false).fetch(myGitRepository.getRoot(), fork.getRemoteName(), null);
- if (!result.isSuccess()) {
- GitFetcher.displayFetchResult(myProject, result, null, result.getErrors());
+ String remoteName = fork.getRemoteName();
+ if (remoteName == null) return;
+ GitRemote remote = GitUtil.findRemoteByName(myGitRepository, remoteName);
+ if (remote == null) {
+ LOG.warn("Couldn't find remote " + remoteName + " in " + myGitRepository);
}
+ fetchSupport(myProject).fetch(myGitRepository, remote).showNotificationIfFailed();
}
@NotNull
diff --git a/plugins/github/src/org/jetbrains/plugins/github/GithubRebaseAction.java b/plugins/github/src/org/jetbrains/plugins/github/GithubRebaseAction.java
index 8bfa40dbb2bf..3a2dafc2f24e 100644
--- a/plugins/github/src/org/jetbrains/plugins/github/GithubRebaseAction.java
+++ b/plugins/github/src/org/jetbrains/plugins/github/GithubRebaseAction.java
@@ -20,8 +20,6 @@ import git4idea.rebase.GitRebaser;
import git4idea.repo.GitRemote;
import git4idea.repo.GitRepository;
import git4idea.repo.GitRepositoryManager;
-import git4idea.update.GitFetchResult;
-import git4idea.update.GitFetcher;
import git4idea.update.GitUpdateResult;
import git4idea.util.GitPreservingProcess;
import org.jetbrains.annotations.NotNull;
@@ -39,6 +37,7 @@ import java.util.Collections;
import java.util.List;
import static git4idea.commands.GitLocalChangesWouldBeOverwrittenDetector.Operation.CHECKOUT;
+import static git4idea.fetch.GitFetchSupport.fetchSupport;
public class GithubRebaseAction extends AbstractGithubUrlGroupingAction {
private static final Logger LOG = GithubUtil.LOG;
@@ -188,13 +187,13 @@ public class GithubRebaseAction extends AbstractGithubUrlGroupingAction {
}
}
- private boolean fetchParent(@NotNull ProgressIndicator indicator) {
- GitFetchResult result = new GitFetcher(myProject, indicator, false).fetch(myRepository.getRoot(), "upstream", null);
- if (!result.isSuccess()) {
- GitFetcher.displayFetchResult(myProject, result, null, result.getErrors());
+ private boolean fetchParent(@NotNull final ProgressIndicator indicator) {
+ GitRemote remote = GitUtil.findRemoteByName(myRepository, "upstream");
+ if (remote == null) {
+ LOG.warn("Couldn't find remote " + " remoteName " + " in " + myRepository);
return false;
}
- return true;
+ return fetchSupport(myProject).fetch(myRepository, remote).showNotificationIfFailed();
}
private void rebaseCurrentBranch(@NotNull ProgressIndicator indicator) {