/*
* Copyright 2000-2015 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 com.intellij.vcs.log.data;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.Consumer;
import com.intellij.util.Function;
import com.intellij.util.NotNullFunction;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.ui.UIUtil;
import com.intellij.vcs.log.*;
import com.intellij.vcs.log.data.index.VcsLogIndex;
import com.intellij.vcs.log.graph.GraphCommit;
import com.intellij.vcs.log.graph.GraphCommitImpl;
import com.intellij.vcs.log.graph.PermanentGraph;
import com.intellij.vcs.log.impl.RequirementsImpl;
import com.intellij.vcs.log.util.StopWatch;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.stream.Collectors;
public class VcsLogRefresherImpl implements VcsLogRefresher {
private static final Logger LOG = Logger.getInstance(VcsLogRefresherImpl.class);
@NotNull private final Project myProject;
@NotNull private final VcsLogStorage myHashMap;
@NotNull private final Map<VirtualFile, VcsLogProvider> myProviders;
@NotNull private final VcsUserRegistryImpl myUserRegistry;
@NotNull private final VcsLogIndex myIndex;
@NotNull private final TopCommitsCache myTopCommitsDetailsCache;
@NotNull private final Consumer<Exception> myExceptionHandler;
@NotNull private final VcsLogProgress myProgress;
private final int myRecentCommitCount;
@NotNull private final SingleTaskController<RefreshRequest, DataPack> mySingleTaskController;
@NotNull private volatile DataPack myDataPack = DataPack.EMPTY;
public VcsLogRefresherImpl(@NotNull Project project,
@NotNull VcsLogStorage hashMap,
@NotNull Map<VirtualFile, VcsLogProvider> providers,
@NotNull VcsUserRegistryImpl userRegistry,
@NotNull VcsLogIndex index,
@NotNull VcsLogProgress progress,
@NotNull TopCommitsCache topCommitsDetailsCache,
@NotNull Consumer<DataPack> dataPackUpdateHandler,
@NotNull Consumer<Exception> exceptionHandler,
int recentCommitsCount) {
myProject = project;
myHashMap = hashMap;
myProviders = providers;
myUserRegistry = userRegistry;
myIndex = index;
myTopCommitsDetailsCache = topCommitsDetailsCache;
myExceptionHandler = exceptionHandler;
myRecentCommitCount = recentCommitsCount;
myProgress = progress;
mySingleTaskController = new SingleTaskController<RefreshRequest, DataPack>(dataPack -> {
myDataPack = dataPack;
dataPackUpdateHandler.consume(dataPack);
}) {
@Override
protected void startNewBackgroundTask() {
VcsLogRefresherImpl.this.startNewBackgroundTask(new MyRefreshTask(myDataPack));
}
};
}
protected void startNewBackgroundTask(@NotNull final Task.Backgroundable refreshTask) {
UIUtil.invokeLaterIfNeeded(() -> {
LOG.debug("Starting a background task...");
ProgressManager.getInstance().runProcessWithProgressAsynchronously(refreshTask, myProgress.createProgressIndicator());
});
}
@NotNull
public DataPack getCurrentDataPack() {
return myDataPack;
}
@NotNull
@Override
public DataPack readFirstBlock() {
try {
LogInfo data = loadRecentData(new CommitCountRequirements(myRecentCommitCount).asMap(myProviders.keySet()));
Collection<List<GraphCommit<Integer>>> commits = data.getCommits();
Map<VirtualFile, CompressedRefs> refs = data.getRefs();
List<GraphCommit<Integer>> compoundList = multiRepoJoin(commits);
compoundList = compoundList.subList(0, Math.min(myRecentCommitCount, compoundList.size()));
myDataPack = DataPack.build(compoundList, refs, myProviders, myHashMap, false);
mySingleTaskController.request(RefreshRequest.RELOAD_ALL); // build/rebuild the full log in background
return myDataPack;
}
catch (VcsException e) {
myExceptionHandler.consume(e);
return DataPack.EMPTY;
}
}
@NotNull
private LogInfo loadRecentData(@NotNull final Map<VirtualFile, VcsLogProvider.Requirements> requirements) throws VcsException {
final StopWatch sw = StopWatch.start("loading commits");
final LogInfo logInfo = new LogInfo(myHashMap);
new ProviderIterator() {
@Override
public void each(@NotNull VirtualFile root, @NotNull VcsLogProvider provider) throws VcsException {
VcsLogProvider.DetailedLogData data = provider.readFirstBlock(root, requirements.get(root));
logInfo.put(root, compactCommits(data.getCommits(), root));
logInfo.put(root, data.getRefs());
storeUsersAndDetails(data.getCommits());
sw.rootCompleted(root);
}
}.iterate(getProvidersForRoots(requirements.keySet()));
myUserRegistry.flush();
myIndex.scheduleIndex(false);
sw.report();
return logInfo;
}
@NotNull
private Map<VirtualFile, VcsLogProvider> getProvidersForRoots(@NotNull Set<VirtualFile> roots) {
return ContainerUtil.map2Map(roots,
root -> Pair.create(root, myProviders.get(root)));
}
@Override
public void refresh(@NotNull Collection<VirtualFile> rootsToRefresh) {
if (!rootsToRefresh.isEmpty()) {
mySingleTaskController.request(new RefreshRequest(rootsToRefresh));
}
}
@NotNull
private static <T extends GraphCommit<Integer>> List<T> multiRepoJoin(@NotNull Collection<List<T>> commits) {
StopWatch sw = StopWatch.start("multi-repo join");
List<T> joined = new VcsLogMultiRepoJoiner<Integer, T>().join(commits);
sw.report();
return joined;
}
@NotNull
private List<GraphCommit<Integer>> compactCommits(@NotNull List<? extends TimedVcsCommit> commits, @NotNull final VirtualFile root) {
StopWatch sw = StopWatch.start("compacting commits");
List<GraphCommit<Integer>> map = ContainerUtil.map(commits, new Function<TimedVcsCommit, GraphCommit<Integer>>() {
@NotNull
@Override
public GraphCommit<Integer> fun(@NotNull TimedVcsCommit commit) {
return compactCommit(commit, root);
}
});
myHashMap.flush();
sw.report();
return map;
}
@NotNull
private GraphCommitImpl<Integer> compactCommit(@NotNull TimedVcsCommit commit, @NotNull final VirtualFile root) {
List<Integer> parents = ContainerUtil.map(commit.getParents(),
(NotNullFunction<Hash, Integer>)hash -> myHashMap.getCommitIndex(hash, root));
int index = myHashMap.getCommitIndex(commit.getId(), root);
myIndex.markForIndexing(index, root);
return new GraphCommitImpl<>(index, parents, commit.getTimestamp());
}
private void storeUsersAndDetails(@NotNull List<? extends VcsCommitMetadata> metadatas) {
for (VcsCommitMetadata detail : metadatas) {
myUserRegistry.addUser(detail.getAuthor());
myUserRegistry.addUser(detail.getCommitter());
}
myTopCommitsDetailsCache.storeDetails(metadatas);
}
@NotNull
public VcsLogProgress getProgress() {
return myProgress;
}
private class MyRefreshTask extends Task.Backgroundable {
@NotNull private DataPack myCurrentDataPack;
@NotNull private final LogInfo myLoadedInfo = new LogInfo(myHashMap);
MyRefreshTask(@NotNull DataPack currentDataPack) {
super(VcsLogRefresherImpl.this.myProject, "Refreshing History...", false);
myCurrentDataPack = currentDataPack;
}
@Override
public void run(@NotNull ProgressIndicator indicator) {
LOG.debug("Refresh task started");
indicator.setIndeterminate(true);
DataPack dataPack = myCurrentDataPack;
while (true) {
List<RefreshRequest> requests = mySingleTaskController.popRequests();
Collection<VirtualFile> rootsToRefresh = getRootsToRefresh(requests);
LOG.debug("Requests: " + requests + ". roots to refresh: " + rootsToRefresh);
if (rootsToRefresh.isEmpty()) {
mySingleTaskController.taskCompleted(dataPack);
break;
}
dataPack = doRefresh(rootsToRefresh);
}
}
@NotNull
private Collection<VirtualFile> getRootsToRefresh(@NotNull List<RefreshRequest> requests) {
Collection<VirtualFile> rootsToRefresh = ContainerUtil.newArrayList();
for (RefreshRequest request : requests) {
if (request == RefreshRequest.RELOAD_ALL) {
myCurrentDataPack = DataPack.EMPTY;
return myProviders.keySet();
}
rootsToRefresh.addAll(request.rootsToRefresh);
}
return rootsToRefresh;
}
@NotNull
private DataPack doRefresh(@NotNull Collection<VirtualFile> roots) {
StopWatch sw = StopWatch.start("refresh");
PermanentGraph<Integer> permanentGraph = myCurrentDataPack.isFull() ? myCurrentDataPack.getPermanentGraph() : null;
Map<VirtualFile, CompressedRefs> currentRefs = myCurrentDataPack.getRefsModel().getAllRefsByRoot();
try {
if (permanentGraph != null) {
int commitCount = myRecentCommitCount;
for (int attempt = 0; attempt <= 1; attempt++) {
loadLogAndRefs(roots, currentRefs, commitCount);
List<? extends GraphCommit<Integer>> compoundLog = multiRepoJoin(myLoadedInfo.getCommits());
Map<VirtualFile, CompressedRefs> allNewRefs = getAllNewRefs(myLoadedInfo, currentRefs);
List<GraphCommit<Integer>> joinedFullLog = join(compoundLog, permanentGraph.getAllCommits(), currentRefs, allNewRefs);
if (joinedFullLog == null) {
commitCount *= 5;
}
else {
return DataPack.build(joinedFullLog, allNewRefs, myProviders, myHashMap, true);
}
}
// couldn't join => need to reload everything; if 5000 commits is still not enough, it's worth reporting:
LOG.info("Couldn't join " + commitCount / 5 + " recent commits to the log (" +
permanentGraph.getAllCommits().size() + " commits)");
}
return loadFullLog();
}
catch (Exception e) {
myExceptionHandler.consume(e);
return DataPack.EMPTY;
}
finally {
sw.report();
}
}
@NotNull
private Map<VirtualFile, CompressedRefs> getAllNewRefs(@NotNull LogInfo newInfo,
@NotNull Map<VirtualFile, CompressedRefs> previousRefs) {
Map<VirtualFile, CompressedRefs> result = ContainerUtil.newHashMap();
for (VirtualFile root : previousRefs.keySet()) {
CompressedRefs newInfoRefs = newInfo.getRefs().get(root);
result.put(root, newInfoRefs != null ? newInfoRefs : previousRefs.get(root));
}
return result;
}
private void loadLogAndRefs(@NotNull Collection<VirtualFile> roots,
@NotNull Map<VirtualFile, CompressedRefs> prevRefs,
int commitCount) throws VcsException {
LogInfo logInfo = loadRecentData(prepareRequirements(roots, commitCount, prevRefs));
for (VirtualFile root : roots) {
myLoadedInfo.put(root, logInfo.getCommits(root));
myLoadedInfo.put(root, logInfo.getRefs().get(root));
}
}
@NotNull
private Map<VirtualFile, VcsLogProvider.Requirements> prepareRequirements(@NotNull Collection<VirtualFile> roots,
int commitCount,
@NotNull Map<VirtualFile, CompressedRefs> prevRefs) {
Map<VirtualFile, VcsLogProvider.Requirements> requirements = ContainerUtil.newHashMap();
for (VirtualFile root : roots) {
requirements.put(root, new RequirementsImpl(commitCount, true, prevRefs.get(root).getRefs()));
}
return requirements;
}
@Nullable
private List<GraphCommit<Integer>> join(@NotNull List<? extends GraphCommit<Integer>> recentCommits,
@NotNull List<GraphCommit<Integer>> fullLog,
@NotNull Map<VirtualFile, CompressedRefs> previousRefs,
@NotNull Map<VirtualFile, CompressedRefs> newRefs) {
StopWatch sw = StopWatch.start("joining new commits");
Collection<Integer> prevRefIndices =
previousRefs.values().stream().flatMap(refs -> refs.getCommits().stream()).collect(Collectors.toSet());
Collection<Integer> newRefIndices = newRefs.values().stream().flatMap(refs -> refs.getCommits().stream()).collect(Collectors.toSet());
try {
List<GraphCommit<Integer>> commits = new VcsLogJoiner<Integer, GraphCommit<Integer>>().addCommits(fullLog, prevRefIndices,
recentCommits,
newRefIndices).first;
sw.report();
return commits;
}
catch (VcsLogRefreshNotEnoughDataException e) {
// valid case: e.g. another developer merged a long-developed branch, or we just didn't pull for a long time
LOG.info(e);
}
catch (IllegalStateException e) {
// it happens from time to time, but we don't know why, and can hardly debug it.
LOG.info(e);
}
return null;
}
@NotNull
private DataPack loadFullLog() throws VcsException {
StopWatch sw = StopWatch.start("full log reload");
LogInfo logInfo = readFullLogFromVcs();
List<? extends GraphCommit<Integer>> graphCommits = multiRepoJoin(logInfo.getCommits());
DataPack dataPack = DataPack.build(graphCommits, logInfo.getRefs(), myProviders, myHashMap, true);
sw.report();
return dataPack;
}
@NotNull
private LogInfo readFullLogFromVcs() throws VcsException {
final StopWatch sw = StopWatch.start("read full log from VCS");
final LogInfo logInfo = new LogInfo(myHashMap);
new ProviderIterator() {
@Override
void each(@NotNull final VirtualFile root, @NotNull VcsLogProvider provider) throws VcsException {
final List<GraphCommit<Integer>> graphCommits = ContainerUtil.newArrayList();
VcsLogProvider.LogData data = provider.readAllHashes(root, commit -> graphCommits.add(compactCommit(commit, root)));
logInfo.put(root, graphCommits);
logInfo.put(root, data.getRefs());
myUserRegistry.addUsers(data.getUsers());
sw.rootCompleted(root);
}
}.iterate(myProviders);
myUserRegistry.flush();
myIndex.scheduleIndex(true);
sw.report();
return logInfo;
}
}
private static class RefreshRequest {
private static final RefreshRequest RELOAD_ALL = new RefreshRequest(Collections.<VirtualFile>emptyList()) {
@Override
public String toString() {
return "RELOAD_ALL";
}
};
private final Collection<VirtualFile> rootsToRefresh;
RefreshRequest(@NotNull Collection<VirtualFile> rootsToRefresh) {
this.rootsToRefresh = rootsToRefresh;
}
@Override
public String toString() {
return "{" + rootsToRefresh + "}";
}
}
private static abstract class ProviderIterator {
abstract void each(@NotNull VirtualFile root, @NotNull VcsLogProvider provider) throws VcsException;
final void iterate(@NotNull Map<VirtualFile, VcsLogProvider> providers) throws VcsException {
for (Map.Entry<VirtualFile, VcsLogProvider> entry : providers.entrySet()) {
each(entry.getKey(), entry.getValue());
}
}
}
private static class CommitCountRequirements implements VcsLogProvider.Requirements {
private final int myCommitCount;
public CommitCountRequirements(int commitCount) {
myCommitCount = commitCount;
}
@Override
public int getCommitCount() {
return myCommitCount;
}
@NotNull
Map<VirtualFile, VcsLogProvider.Requirements> asMap(@NotNull Collection<VirtualFile> roots) {
return ContainerUtil
.map2Map(roots, root -> Pair.<VirtualFile, VcsLogProvider.Requirements>create(root, this));
}
}
@SuppressWarnings("StringConcatenationInsideStringBufferAppend")
private static class LogInfo {
private final VcsLogStorage myHashMap;
private final Map<VirtualFile, CompressedRefs> myRefs = ContainerUtil.newHashMap();
private final Map<VirtualFile, List<GraphCommit<Integer>>> myCommits = ContainerUtil.newHashMap();
public LogInfo(VcsLogStorage hashMap) {
myHashMap = hashMap;
}
void put(@NotNull VirtualFile root, @NotNull List<GraphCommit<Integer>> commits) {
myCommits.put(root, commits);
}
void put(@NotNull VirtualFile root, @NotNull Set<VcsRef> refs) {
myRefs.put(root, new CompressedRefs(refs, myHashMap));
}
void put(@NotNull VirtualFile root, @NotNull CompressedRefs refs) {
myRefs.put(root, refs);
}
@NotNull
Collection<List<GraphCommit<Integer>>> getCommits() {
return myCommits.values();
}
List<GraphCommit<Integer>> getCommits(@NotNull VirtualFile root) {
return myCommits.get(root);
}
@NotNull
Map<VirtualFile, CompressedRefs> getRefs() {
return myRefs;
}
}
}