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) {