| /* |
| * 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.update; |
| |
| import com.intellij.dvcs.DvcsUtil; |
| import com.intellij.openapi.components.ServiceManager; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.progress.ProgressIndicator; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.util.Key; |
| import com.intellij.openapi.util.text.StringUtil; |
| import com.intellij.openapi.vcs.VcsNotifier; |
| import com.intellij.openapi.vfs.VirtualFile; |
| import com.intellij.util.ArrayUtil; |
| import git4idea.GitLocalBranch; |
| import git4idea.GitRemoteBranch; |
| import git4idea.GitUtil; |
| import git4idea.GitVcs; |
| import git4idea.branch.GitBranchUtil; |
| import git4idea.commands.Git; |
| import git4idea.commands.GitCommandResult; |
| import git4idea.commands.GitLineHandlerAdapter; |
| import git4idea.commands.GitLineHandlerListener; |
| import git4idea.repo.GitBranchTrackInfo; |
| import git4idea.repo.GitRemote; |
| import git4idea.repo.GitRepository; |
| import git4idea.repo.GitRepositoryManager; |
| import git4idea.util.GitUIUtil; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import java.util.*; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| import static git4idea.GitBranch.REFS_HEADS_PREFIX; |
| import static git4idea.GitBranch.REFS_REMOTES_PREFIX; |
| |
| public class GitFetcher { |
| |
| private static final Logger LOG = Logger.getInstance(GitFetcher.class); |
| |
| private final Project myProject; |
| private final GitRepositoryManager myRepositoryManager; |
| private final ProgressIndicator myProgressIndicator; |
| private final boolean myFetchAll; |
| private final GitVcs myVcs; |
| |
| private final Collection<Exception> myErrors = new ArrayList<Exception>(); |
| |
| /** |
| * @param fetchAll Pass {@code true} to fetch all remotes and all branches (like {@code git fetch} without parameters does). |
| * Pass {@code false} to fetch only the tracked branch of the current branch. |
| */ |
| public GitFetcher(@NotNull Project project, @NotNull ProgressIndicator progressIndicator, boolean fetchAll) { |
| myProject = project; |
| myProgressIndicator = progressIndicator; |
| myFetchAll = fetchAll; |
| myRepositoryManager = GitUtil.getRepositoryManager(myProject); |
| myVcs = GitVcs.getInstance(project); |
| } |
| |
| /** |
| * Invokes 'git fetch'. |
| * @return true if fetch was successful, false in the case of error. |
| */ |
| public GitFetchResult fetch(@NotNull GitRepository repository) { |
| // TODO need to have a fair compound result here |
| GitFetchResult fetchResult = GitFetchResult.success(); |
| if (myFetchAll) { |
| fetchResult = fetchAll(repository, fetchResult); |
| } |
| else { |
| return fetchCurrentRemote(repository); |
| } |
| |
| repository.update(); |
| return fetchResult; |
| } |
| |
| @NotNull |
| public GitFetchResult fetch(@NotNull VirtualFile root, @NotNull String remoteName, @Nullable String branch) { |
| GitRepository repository = myRepositoryManager.getRepositoryForRoot(root); |
| if (repository == null) { |
| return logError("Repository can't be null for " + root, myRepositoryManager.toString()); |
| } |
| GitRemote remote = GitUtil.findRemoteByName(repository, remoteName); |
| if (remote == null) { |
| return logError("Couldn't find remote with the name " + remoteName, null); |
| } |
| String url = remote.getFirstUrl(); |
| if (url == null) { |
| return logError("URL is null for remote " + remote.getName(), null); |
| } |
| return fetchRemote(repository, remote, url, branch); |
| } |
| |
| private static GitFetchResult logError(@NotNull String message, @Nullable String additionalInfo) { |
| String addInfo = additionalInfo != null ? "\n" + additionalInfo : ""; |
| LOG.error(message + addInfo); |
| return GitFetchResult.error(message); |
| } |
| |
| @NotNull |
| private GitFetchResult fetchCurrentRemote(@NotNull GitRepository repository) { |
| FetchParams fetchParams = getFetchParams(repository); |
| if (fetchParams.isError()) { |
| return fetchParams.getError(); |
| } |
| |
| GitRemote remote = fetchParams.getRemote(); |
| String url = fetchParams.getUrl(); |
| return fetchRemote(repository, remote, url, null); |
| } |
| |
| @NotNull |
| private GitFetchResult fetchRemote(@NotNull GitRepository repository, |
| @NotNull GitRemote remote, |
| @NotNull String url, |
| @Nullable String branch) { |
| return fetchNatively(repository, remote, url, branch); |
| } |
| |
| // leaving this unused method, because the wanted behavior can change again |
| @SuppressWarnings("UnusedDeclaration") |
| @NotNull |
| private GitFetchResult fetchCurrentBranch(@NotNull GitRepository repository) { |
| FetchParams fetchParams = getFetchParams(repository); |
| if (fetchParams.isError()) { |
| return fetchParams.getError(); |
| } |
| |
| GitRemote remote = fetchParams.getRemote(); |
| String remoteBranch = fetchParams.getRemoteBranch().getNameForRemoteOperations(); |
| String url = fetchParams.getUrl(); |
| return fetchNatively(repository, remote, url, remoteBranch); |
| } |
| |
| @NotNull |
| private static FetchParams getFetchParams(@NotNull GitRepository repository) { |
| GitLocalBranch currentBranch = repository.getCurrentBranch(); |
| if (currentBranch == null) { |
| // fetching current branch is called from Update Project and Push, where branch tracking is pre-checked |
| String message = "Current branch can't be null here. \nRepository: " + repository; |
| LOG.error(message); |
| return new FetchParams(GitFetchResult.error(new Exception(message))); |
| } |
| GitBranchTrackInfo trackInfo = GitBranchUtil.getTrackInfoForBranch(repository, currentBranch); |
| if (trackInfo == null) { |
| String message = "Tracked info is null for branch " + currentBranch + "\n Repository: " + repository; |
| LOG.error(message); |
| return new FetchParams(GitFetchResult.error(new Exception(message))); |
| } |
| |
| GitRemote remote = trackInfo.getRemote(); |
| String url = remote.getFirstUrl(); |
| if (url == null) { |
| String message = "URL is null for remote " + remote.getName(); |
| LOG.error(message); |
| return new FetchParams(GitFetchResult.error(new Exception(message))); |
| } |
| |
| return new FetchParams(remote, trackInfo.getRemoteBranch(), url); |
| } |
| |
| @NotNull |
| private GitFetchResult fetchAll(@NotNull GitRepository repository, @NotNull GitFetchResult fetchResult) { |
| for (GitRemote remote : repository.getRemotes()) { |
| String url = remote.getFirstUrl(); |
| if (url == null) { |
| LOG.error("URL is null for remote " + remote.getName()); |
| continue; |
| } |
| GitFetchResult res = fetchNatively(repository, remote, url, null); |
| res.addPruneInfo(fetchResult.getPrunedRefs()); |
| fetchResult = res; |
| if (!fetchResult.isSuccess()) { |
| break; |
| } |
| } |
| return fetchResult; |
| } |
| |
| @NotNull |
| private static GitFetchResult fetchNatively(@NotNull GitRepository repository, @NotNull GitRemote remote, @NotNull String url, |
| @Nullable String branch) { |
| Git git = ServiceManager.getService(Git.class); |
| String[] additionalParams = branch != null ? |
| new String[]{ getFetchSpecForBranch(branch, remote.getName()) } : |
| ArrayUtil.EMPTY_STRING_ARRAY; |
| |
| GitFetchPruneDetector pruneDetector = new GitFetchPruneDetector(); |
| GitCommandResult result = |
| git.fetch(repository, url, remote.getName(), Collections.<GitLineHandlerListener>singletonList(pruneDetector), additionalParams); |
| |
| GitFetchResult fetchResult; |
| if (result.success()) { |
| fetchResult = GitFetchResult.success(); |
| } |
| else if (result.cancelled()) { |
| fetchResult = GitFetchResult.cancel(); |
| } |
| else { |
| fetchResult = GitFetchResult.error(result.getErrorOutputAsJoinedString()); |
| } |
| fetchResult.addPruneInfo(pruneDetector.getPrunedRefs()); |
| return fetchResult; |
| } |
| |
| private static String getRidOfPrefixIfExists(String branch) { |
| if (branch.startsWith(REFS_HEADS_PREFIX)) { |
| return branch.substring(REFS_HEADS_PREFIX.length()); |
| } |
| return branch; |
| } |
| |
| @NotNull |
| public static String getFetchSpecForBranch(@NotNull String branch, @NotNull String remoteName) { |
| branch = getRidOfPrefixIfExists(branch); |
| return REFS_HEADS_PREFIX + branch + ":" + REFS_REMOTES_PREFIX + remoteName + "/" + branch; |
| } |
| |
| @NotNull |
| public Collection<Exception> getErrors() { |
| return myErrors; |
| } |
| |
| public static void displayFetchResult(@NotNull Project project, |
| @NotNull GitFetchResult result, |
| @Nullable String errorNotificationTitle, @NotNull Collection<? extends Exception> errors) { |
| if (result.isSuccess()) { |
| VcsNotifier.getInstance(project).notifySuccess("Fetched successfully" + result.getAdditionalInfo()); |
| } else if (result.isCancelled()) { |
| VcsNotifier.getInstance(project).notifyMinorWarning("", "Fetch cancelled by user" + result.getAdditionalInfo()); |
| } else if (result.isNotAuthorized()) { |
| String title; |
| String description; |
| if (errorNotificationTitle != null) { |
| title = errorNotificationTitle; |
| description = "Fetch failed: couldn't authorize"; |
| } else { |
| title = "Fetch failed"; |
| description = "Couldn't authorize"; |
| } |
| description += result.getAdditionalInfo(); |
| GitUIUtil.notifyMessage(project, title, description, true, null); |
| } else { |
| GitVcs instance = GitVcs.getInstance(project); |
| if (instance != null && instance.getExecutableValidator().isExecutableValid()) { |
| GitUIUtil.notifyMessage(project, "Fetch failed", result.getAdditionalInfo(), true, errors); |
| } |
| } |
| } |
| |
| /** |
| * Fetches all specified roots. |
| * Once a root has failed, stops and displays the notification. |
| * If needed, displays the successful notification at the end. |
| * @param roots roots to fetch. |
| * @param errorNotificationTitle if specified, this notification title will be used instead of the standard "Fetch failed". |
| * 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. |
| */ |
| public boolean fetchRootsAndNotify(@NotNull Collection<GitRepository> roots, |
| @Nullable String errorNotificationTitle, boolean notifySuccess) { |
| Map<VirtualFile, String> additionalInfo = new HashMap<VirtualFile, String>(); |
| for (GitRepository repository : roots) { |
| LOG.info("fetching " + repository); |
| GitFetchResult result = fetch(repository); |
| String ai = result.getAdditionalInfo(); |
| if (!StringUtil.isEmptyOrSpaces(ai)) { |
| additionalInfo.put(repository.getRoot(), ai); |
| } |
| if (!result.isSuccess()) { |
| Collection<Exception> errors = new ArrayList<Exception>(getErrors()); |
| errors.addAll(result.getErrors()); |
| displayFetchResult(myProject, result, errorNotificationTitle, errors); |
| return false; |
| } |
| } |
| if (notifySuccess) { |
| VcsNotifier.getInstance(myProject).notifySuccess("Fetched successfully"); |
| } |
| |
| String addInfo = makeAdditionalInfoByRoot(additionalInfo); |
| if (!StringUtil.isEmptyOrSpaces(addInfo)) { |
| VcsNotifier.getInstance(myProject).notifyMinorInfo("Fetch details", addInfo); |
| } |
| |
| return true; |
| } |
| |
| @NotNull |
| private String makeAdditionalInfoByRoot(@NotNull Map<VirtualFile, String> additionalInfo) { |
| if (additionalInfo.isEmpty()) { |
| return ""; |
| } |
| StringBuilder info = new StringBuilder(); |
| if (myRepositoryManager.moreThanOneRoot()) { |
| for (Map.Entry<VirtualFile, String> entry : additionalInfo.entrySet()) { |
| info.append(entry.getValue()).append(" in ").append(DvcsUtil.getShortRepositoryName(myProject, entry.getKey())).append("<br/>"); |
| } |
| } |
| else { |
| info.append(additionalInfo.values().iterator().next()); |
| } |
| return info.toString(); |
| } |
| |
| private static class GitFetchPruneDetector extends GitLineHandlerAdapter { |
| |
| private static final Pattern PRUNE_PATTERN = Pattern.compile("\\s*x\\s*\\[deleted\\].*->\\s*(\\S*)"); |
| |
| @NotNull private final Collection<String> myPrunedRefs = new ArrayList<String>(); |
| |
| @Override |
| public void onLineAvailable(String line, Key outputType) { |
| // x [deleted] (none) -> origin/frmari |
| Matcher matcher = PRUNE_PATTERN.matcher(line); |
| if (matcher.matches()) { |
| myPrunedRefs.add(matcher.group(1)); |
| } |
| } |
| |
| @NotNull |
| public Collection<String> getPrunedRefs() { |
| return myPrunedRefs; |
| } |
| } |
| |
| private static class FetchParams { |
| private GitRemote myRemote; |
| private GitRemoteBranch myRemoteBranch; |
| private GitFetchResult myError; |
| private String myUrl; |
| |
| FetchParams(GitFetchResult error) { |
| myError = error; |
| } |
| |
| FetchParams(GitRemote remote, GitRemoteBranch remoteBranch, String url) { |
| myRemote = remote; |
| myRemoteBranch = remoteBranch; |
| myUrl = url; |
| } |
| |
| boolean isError() { |
| return myError != null; |
| } |
| |
| public GitFetchResult getError() { |
| return myError; |
| } |
| |
| public GitRemote getRemote() { |
| return myRemote; |
| } |
| |
| public GitRemoteBranch getRemoteBranch() { |
| return myRemoteBranch; |
| } |
| |
| public String getUrl() { |
| return myUrl; |
| } |
| } |
| } |