Rewrite Git fetch procedure for better API and cleaner code

GitFetcher appears to be not convenient to use, and has some serious flaws,
but it is already used by several external plugins, so it is hard
to rewrite it keeping the compatibility.

Thus introducing a new API to fetch - the GitFetchSupport;
GitFetcher is kept intact and deprecated.
This commit is contained in:
Kirill Likhodedov
2018-09-26 14:37:59 +03:00
parent c9f65743de
commit 377a6762f9
10 changed files with 296 additions and 23 deletions
+1 -1
View File
@@ -211,10 +211,10 @@
<projectService serviceInterface="git4idea.branch.GitBrancher"
serviceImplementation="git4idea.branch.GitBrancherImpl"/>
<projectService serviceImplementation="git4idea.GitUserRegistry" />
<projectService serviceImplementation="git4idea.branch.DeepComparatorHolder"/>
<projectService serviceInterface="git4idea.config.GitExecutableProblemsNotifier"
serviceImplementation="git4idea.config.GitExecutableProblemsNotifier"/>
<projectService serviceInterface="git4idea.fetch.GitFetchSupport" serviceImplementation="git4idea.fetch.GitFetchSupportImpl" />
<applicationService serviceInterface="git4idea.config.GitVcsApplicationSettings"
serviceImplementation="git4idea.config.GitVcsApplicationSettings"/>
@@ -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();
}
});
}
@@ -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);
}
@@ -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<GitRepository> 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<GitRemote> 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);
}
}
@@ -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<GitFetchSupportImpl>()
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<GitRepository>): GitFetchResult {
val results = mutableMapOf<GitRepository, RepoResult>()
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<GitRemote>): GitFetchResult {
return resultOf(mapOf(Pair(repository, doFetch(repository, remotes))))
}
private fun doFetch(repository: GitRepository, remotes: List<GitRemote>): RepoResult {
val results = mutableMapOf<GitRemote, SingleRemoteResult>()
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<GitRepository, RepoResult>) = FetchResultImpl(project, results)
private class RepoResult(val results: Map<GitRemote, SingleRemoteResult>) {
/*
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<String>) {
fun success() = error == null
}
private class FetchResultImpl(val project: Project,
val results: Map<GitRepository, RepoResult>) : 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()) "<b>Fetch Successful</b>" else "<b>$failureTitle</b>$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()) "<br/>$text" else ""
}
}
@@ -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;
@@ -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<GitRepository> roots,
@Nullable String errorNotificationTitle, boolean notifySuccess) {
MultiRootMessage additionalInfo = new MultiRootMessage(myProject, GitUtil.getRootsFromRepositories(roots), true);
@@ -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<GitRepository> repositories) {
return new GitFetcher(myProject, myProgressIndicator, false).fetchRootsAndNotify(repositories, "Update failed", false);
return fetchSupport(myProject).fetch(repositories).showNotificationIfFailed("Update failed");
}
/**
@@ -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
@@ -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) {