blob: f9c65632d9becdd617a03d73aaf3dee64fd087fc [file] [log] [blame]
/*
* 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;
}
}
}