blob: 72aef054a78511c83fc78993a48982aeb3864067 [file] [log] [blame]
/*
* Copyright 2000-2013 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.log;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.VcsKey;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.*;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.OpenTHashSet;
import com.intellij.vcs.log.*;
import com.intellij.vcs.log.data.VcsLogSorter;
import com.intellij.vcs.log.impl.LogDataImpl;
import com.intellij.vcs.log.impl.HashImpl;
import git4idea.*;
import git4idea.branch.GitBranchUtil;
import git4idea.config.GitVersionSpecialty;
import git4idea.history.GitHistoryUtils;
import git4idea.repo.GitRepository;
import git4idea.repo.GitRepositoryChangeListener;
import git4idea.repo.GitRepositoryManager;
import gnu.trove.TObjectHashingStrategy;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
public class GitLogProvider implements VcsLogProvider {
private static final Logger LOG = Logger.getInstance(GitLogProvider.class);
public static final Function<VcsRef, String> GET_TAG_NAME = new Function<VcsRef, String>() {
@Override
public String fun(VcsRef ref) {
return ref.getType() == GitRefManager.TAG ? ref.getName() : null;
}
};
private static final TObjectHashingStrategy<VcsRef> REF_ONLY_NAME_STRATEGY = new TObjectHashingStrategy<VcsRef>() {
@Override
public int computeHashCode(@NotNull VcsRef ref) {
return ref.getName().hashCode();
}
@Override
public boolean equals(@NotNull VcsRef ref1, @NotNull VcsRef ref2) {
return ref1.getName().equals(ref2.getName());
}
};
@NotNull private final Project myProject;
@NotNull private final GitVcs myVcs;
@NotNull private final GitRepositoryManager myRepositoryManager;
@NotNull private final GitUserRegistry myUserRegistry;
@NotNull private final VcsLogRefManager myRefSorter;
@NotNull private final VcsLogObjectsFactory myVcsObjectsFactory;
public GitLogProvider(@NotNull Project project, @NotNull GitRepositoryManager repositoryManager, @NotNull VcsLogObjectsFactory factory,
@NotNull GitUserRegistry userRegistry) {
myProject = project;
myRepositoryManager = repositoryManager;
myUserRegistry = userRegistry;
myRefSorter = new GitRefManager(myRepositoryManager);
myVcsObjectsFactory = factory;
myVcs = ObjectUtils.assertNotNull(GitVcs.getInstance(project));
}
@NotNull
@Override
public DetailedLogData readFirstBlock(@NotNull VirtualFile root, @NotNull Requirements requirements) throws VcsException {
if (!isRepositoryReady(root)) {
return LogDataImpl.empty();
}
GitRepository repository = ObjectUtils.assertNotNull(myRepositoryManager.getRepositoryForRoot(root));
// need to query more to sort them manually; this doesn't affect performance: it is equal for -1000 and -2000
int commitCount = requirements.getCommitCount() * 2;
String[] params = new String[]{"HEAD", "--branches", "--remotes", "--max-count=" + commitCount};
// NB: not specifying --tags, because it introduces great slowdown if there are many tags,
// but makes sense only if there are heads without branch or HEAD labels (rare case). Such cases are partially handled below.
boolean refresh = requirements instanceof VcsLogProviderRequirementsEx && ((VcsLogProviderRequirementsEx)requirements).isRefresh();
DetailedLogData data = GitHistoryUtils.loadMetadata(myProject, root, refresh, params);
Set<VcsRef> safeRefs = data.getRefs();
Set<VcsRef> allRefs = new OpenTHashSet<VcsRef>(safeRefs, REF_ONLY_NAME_STRATEGY);
addNewElements(allRefs, readBranches(repository));
Collection<VcsCommitMetadata> allDetails;
if (!refresh) {
allDetails = data.getCommits();
}
else {
// on refresh: get new tags, which point to commits not from the first block; then get history, walking down just from these tags
// on init: just ignore such tagged-only branches. The price for speed-up.
VcsLogProviderRequirementsEx rex = (VcsLogProviderRequirementsEx)requirements;
Set<String> currentTags = readCurrentTagNames(root);
addOldStillExistingTags(allRefs, currentTags, rex.getPreviousRefs());
allDetails = ContainerUtil.newHashSet(data.getCommits());
Set<String> previousTags = new HashSet<String>(ContainerUtil.mapNotNull(rex.getPreviousRefs(), GET_TAG_NAME));
Set<String> safeTags = new HashSet<String>(ContainerUtil.mapNotNull(safeRefs, GET_TAG_NAME));
Set<String> newUnmatchedTags = remove(currentTags, previousTags, safeTags);
if (!newUnmatchedTags.isEmpty()) {
DetailedLogData commitsFromTags = loadSomeCommitsOnTaggedBranches(root, commitCount, newUnmatchedTags);
addNewElements(allDetails, commitsFromTags.getCommits());
addNewElements(allRefs, commitsFromTags.getRefs());
}
}
List<VcsCommitMetadata> sortedCommits = VcsLogSorter.sortByDateTopoOrder(allDetails);
sortedCommits = sortedCommits.subList(0, Math.min(sortedCommits.size(), requirements.getCommitCount()));
return new LogDataImpl(allRefs, sortedCommits);
}
private static void addOldStillExistingTags(@NotNull Set<VcsRef> allRefs,
@NotNull Set<String> currentTags,
@NotNull Set<VcsRef> previousRefs) {
for (VcsRef ref : previousRefs) {
if (!allRefs.contains(ref) && currentTags.contains(ref.getName())) {
allRefs.add(ref);
}
}
}
@NotNull
private Set<String> readCurrentTagNames(@NotNull VirtualFile root) throws VcsException {
Set<String> tags = ContainerUtil.newHashSet();
GitTag.listAsStrings(myProject, root, tags, null);
return tags;
}
@NotNull
private static <T> Set<T> remove(@NotNull Set<T> original, @NotNull Set<T>... toRemove) {
Set<T> result = ContainerUtil.newHashSet(original);
for (Set<T> set : toRemove) {
result.removeAll(set);
}
return result;
}
private static <T> void addNewElements(@NotNull Collection<T> original, @NotNull Collection<T> toAdd) {
for (T item : toAdd) {
if (!original.contains(item)) {
original.add(item);
}
}
}
@NotNull
private DetailedLogData loadSomeCommitsOnTaggedBranches(@NotNull VirtualFile root, int commitCount,
@NotNull Collection<String> unmatchedTags) throws VcsException {
List<String> params = new ArrayList<String>();
params.add("--max-count=" + commitCount);
params.addAll(unmatchedTags);
return GitHistoryUtils.loadMetadata(myProject, root, true, ArrayUtil.toStringArray(params));
}
@Override
@NotNull
public LogData readAllHashes(@NotNull VirtualFile root, @NotNull final Consumer<TimedVcsCommit> commitConsumer) throws VcsException {
if (!isRepositoryReady(root)) {
return LogDataImpl.empty();
}
List<String> parameters = new ArrayList<String>(GitHistoryUtils.LOG_ALL);
parameters.add("--sparse");
final GitBekParentFixer parentFixer = GitBekParentFixer.prepare(root, this);
Set<VcsUser> userRegistry = ContainerUtil.newHashSet();
Set<VcsRef> refs = ContainerUtil.newHashSet();
GitHistoryUtils.readCommits(myProject, root, parameters, new CollectConsumer<VcsUser>(userRegistry),
new CollectConsumer<VcsRef>(refs), new Consumer<TimedVcsCommit>() {
@Override
public void consume(TimedVcsCommit commit) {
commitConsumer.consume(parentFixer.fixCommit(commit));
}
});
return new LogDataImpl(refs, userRegistry);
}
@NotNull
@Override
public List<? extends VcsShortCommitDetails> readShortDetails(@NotNull VirtualFile root,
@NotNull List<String> hashes) throws VcsException {
return GitHistoryUtils.readMiniDetails(myProject, root, hashes);
}
@NotNull
@Override
public List<? extends VcsFullCommitDetails> readFullDetails(@NotNull VirtualFile root, @NotNull List<String> hashes) throws VcsException {
String noWalk = GitVersionSpecialty.NO_WALK_UNSORTED.existsIn(myVcs.getVersion()) ? "--no-walk=unsorted" : "--no-walk";
List<String> params = new ArrayList<String>();
params.add(noWalk);
params.addAll(hashes);
return GitHistoryUtils.history(myProject, root, ArrayUtil.toStringArray(params));
}
@NotNull
private Set<VcsRef> readBranches(@NotNull GitRepository repository) {
VirtualFile root = repository.getRoot();
Collection<GitLocalBranch> localBranches = repository.getBranches().getLocalBranches();
Collection<GitRemoteBranch> remoteBranches = repository.getBranches().getRemoteBranches();
Set<VcsRef> refs = new HashSet<VcsRef>(localBranches.size() + remoteBranches.size());
for (GitLocalBranch localBranch : localBranches) {
refs.add(
myVcsObjectsFactory.createRef(HashImpl.build(localBranch.getHash()), localBranch.getName(), GitRefManager.LOCAL_BRANCH, root));
}
for (GitRemoteBranch remoteBranch : remoteBranches) {
refs.add(myVcsObjectsFactory.createRef(HashImpl.build(remoteBranch.getHash()), remoteBranch.getNameForLocalOperations(),
GitRefManager.REMOTE_BRANCH, root));
}
String currentRevision = repository.getCurrentRevision();
if (currentRevision != null) { // null => fresh repository
refs.add(myVcsObjectsFactory.createRef(HashImpl.build(currentRevision), "HEAD", GitRefManager.HEAD, root));
}
return refs;
}
@NotNull
@Override
public VcsKey getSupportedVcs() {
return GitVcs.getKey();
}
@NotNull
@Override
public VcsLogRefManager getReferenceManager() {
return myRefSorter;
}
@Override
public void subscribeToRootRefreshEvents(@NotNull final Collection<VirtualFile> roots, @NotNull final VcsLogRefresher refresher) {
myProject.getMessageBus().connect(myProject).subscribe(GitRepository.GIT_REPO_CHANGE, new GitRepositoryChangeListener() {
@Override
public void repositoryChanged(@NotNull GitRepository repository) {
VirtualFile root = repository.getRoot();
if (roots.contains(root)) {
refresher.refresh(root);
}
}
});
}
@NotNull
@Override
public List<TimedVcsCommit> getCommitsMatchingFilter(@NotNull final VirtualFile root,
@NotNull VcsLogFilterCollection filterCollection,
int maxCount) throws VcsException {
if (!isRepositoryReady(root)) {
return Collections.emptyList();
}
List<String> filterParameters = ContainerUtil.newArrayList();
if (filterCollection.getBranchFilter() != null) {
GitRepository repository = getRepository(root);
assert repository != null : "repository is null for root " + root + " but was previously reported as 'ready'";
boolean atLeastOneBranchExists = false;
for (String branchName : filterCollection.getBranchFilter().getBranchNames()) {
if (branchName.equals("HEAD") || repository.getBranches().findBranchByName(branchName) != null) {
filterParameters.add(branchName);
atLeastOneBranchExists = true;
}
}
if (!atLeastOneBranchExists) { // no such branches in this repository => filter matches nothing
return Collections.emptyList();
}
}
else {
filterParameters.addAll(GitHistoryUtils.LOG_ALL);
}
if (filterCollection.getUserFilter() != null) {
String authorFilter = StringUtil.join(filterCollection.getUserFilter().getUserNames(root), "|");
filterParameters.add(prepareParameter("author", StringUtil.escapeChar(StringUtil.escapeBackSlashes(authorFilter), '|')));
}
if (filterCollection.getDateFilter() != null) {
// assuming there is only one date filter, until filter expressions are defined
VcsLogDateFilter filter = filterCollection.getDateFilter();
if (filter.getAfter() != null) {
filterParameters.add(prepareParameter("after", filter.getAfter().toString()));
}
if (filter.getBefore() != null) {
filterParameters.add(prepareParameter("before", filter.getBefore().toString()));
}
}
if (filterCollection.getTextFilter() != null) {
String textFilter = filterCollection.getTextFilter().getText();
filterParameters.add(prepareParameter("grep", textFilter));
}
filterParameters.add("--regexp-ignore-case"); // affects case sensitivity of any filter (except file filter)
if (maxCount > 0) {
filterParameters.add(prepareParameter("max-count", String.valueOf(maxCount)));
}
filterParameters.add("--date-order");
// note: structure filter must be the last parameter, because it uses "--" which separates parameters from paths
if (filterCollection.getStructureFilter() != null) {
filterParameters.add("--simplify-merges");
filterParameters.add("--");
for (VirtualFile file : filterCollection.getStructureFilter().getFiles(root)) {
filterParameters.add(file.getPath());
}
}
List<TimedVcsCommit> commits = ContainerUtil.newArrayList();
GitHistoryUtils.readCommits(myProject, root, filterParameters, Consumer.EMPTY_CONSUMER, Consumer.EMPTY_CONSUMER,
new CollectConsumer<TimedVcsCommit>(commits));
return commits;
}
@Nullable
@Override
public VcsUser getCurrentUser(@NotNull VirtualFile root) throws VcsException {
return myUserRegistry.getOrReadUser(root);
}
@NotNull
@Override
public Collection<String> getContainingBranches(@NotNull VirtualFile root, @NotNull Hash commitHash) throws VcsException {
return GitBranchUtil.getBranches(myProject, root, true, true, commitHash.asString());
}
private static String prepareParameter(String paramName, String value) {
return "--" + paramName + "=" + value; // no value quoting needed, because the parameter itself will be quoted by GeneralCommandLine
}
@Nullable
private GitRepository getRepository(@NotNull VirtualFile root) {
myRepositoryManager.waitUntilInitialized();
return myRepositoryManager.getRepositoryForRoot(root);
}
private boolean isRepositoryReady(@NotNull VirtualFile root) {
GitRepository repository = getRepository(root);
if (repository == null) {
LOG.error("Repository not found for root " + root);
return false;
}
else if (repository.isFresh()) {
LOG.info("Fresh repository: " + root);
return false;
}
return true;
}
}