[log] IDEA-116470 Show "Contained in branches:" information in details

* Improve RefsModel to be able to find references by indices,
  not hashes.
* Introduce ContainingBranchesGetter:
  - holds a background queue to calculate the information asynchronously
  - calculates by walking up the graph starting from the given nodes.
  - saves the calculated value in the cache.
  - if there is already information about some node in the cache,
    reuse it and don't walk over this graph branch.
  - clear the cache from VcsLogDataHolder on smart refresh,
    if references have changed; and at any case during initial refresh.
This commit is contained in:
Kirill Likhodedov
2013-11-22 18:22:12 +04:00
parent fe846c1d21
commit fd8772d48f
6 changed files with 214 additions and 19 deletions
@@ -0,0 +1,148 @@
/*
* 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 com.intellij.vcs.log.data;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.util.Condition;
import com.intellij.util.ThrowableConsumer;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.SLRUMap;
import com.intellij.vcs.log.VcsRef;
import com.intellij.vcs.log.graph.elements.Edge;
import com.intellij.vcs.log.graph.elements.Node;
import com.intellij.vcs.log.util.SequentialLimitedLifoExecutor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
/**
* Provides capabilities to asynchronously calculate "contained in branches" information.
*/
public class ContainingBranchesGetter {
@NotNull private final SequentialLimitedLifoExecutor<Task> myTaskExecutor;
@NotNull private final Collection<Runnable> myLoadingFinishedListeners = new ArrayList<Runnable>();
@NotNull private final SLRUMap<Node, List<VcsRef>> myCache = new SLRUMap<Node, List<VcsRef>>(1000, 1000);
ContainingBranchesGetter(@NotNull Disposable parentDisposable) {
myTaskExecutor = new SequentialLimitedLifoExecutor<Task>(parentDisposable, 10, new ThrowableConsumer<Task, Throwable>() {
@Override
public void consume(Task task) throws Throwable {
myCache.put(task.node, getContainingBranches(task.pack, task.node));
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
for (Runnable listener : myLoadingFinishedListeners) {
listener.run();
}
}
});
}
});
}
void clearCache() {
myCache.clear();
}
/**
* This task will be executed each time the calculating process completes.
*/
public void addTaskCompletedListener(@NotNull Runnable runnable) {
myLoadingFinishedListeners.add(runnable);
}
/**
* Returns the alphabetically sorted list of branches containing the specified node, if this information is ready;
* if it is not available, starts calculating in the background and returns null.
*/
@Nullable
public List<VcsRef> requestContainingBranches(@NotNull DataPack dataPack, @NotNull Node node) {
List<VcsRef> refs = myCache.get(node);
if (refs == null) {
myTaskExecutor.queue(new Task(dataPack, node));
}
return refs;
}
@NotNull
private List<VcsRef> getContainingBranches(@NotNull DataPack dataPack, @NotNull Node node) {
RefsModel refsModel = dataPack.getRefsModel();
Set<VcsRef> containingBranches = ContainerUtil.newHashSet();
Set<Node> visitedNodes = ContainerUtil.newHashSet();
Set<Node> nodesToCheck = ContainerUtil.newHashSet();
nodesToCheck.add(node);
while (!nodesToCheck.isEmpty()) {
Iterator<Node> nodeIterator = nodesToCheck.iterator();
Node nextNode = nodeIterator.next();
nodeIterator.remove();
if (!visitedNodes.add(nextNode)) {
continue;
}
for (Edge edge : nextNode.getUpEdges()) {
Node upNode = edge.getUpNode();
// optimization: the node is contained in all branches which contain its child => no need to walk over this graph branch
List<VcsRef> upRefs = myCache.get(upNode);
if (upRefs != null) {
containingBranches.addAll(upRefs);
}
else {
nodesToCheck.add(upNode);
}
}
containingBranches.addAll(getBranchesPointingToThisNode(refsModel, nextNode));
}
return sortByName(containingBranches);
}
@NotNull
private static Collection<VcsRef> getBranchesPointingToThisNode(@NotNull RefsModel refsModel, @NotNull Node node) {
return ContainerUtil.filter(refsModel.refsToCommit(node.getCommitIndex()), new Condition<VcsRef>() {
@Override
public boolean value(VcsRef ref) {
return ref.getType().isBranch();
}
});
}
private static List<VcsRef> sortByName(Collection<VcsRef> branches) {
List<VcsRef> branchesList = new ArrayList<VcsRef>(branches);
ContainerUtil.sort(branchesList, new Comparator<VcsRef>() {
@Override
public int compare(VcsRef o1, VcsRef o2) {
return o1.getName().compareTo(o2.getName());
}
});
return branchesList;
}
private static class Task {
private final DataPack pack;
private final Node node;
public Task(DataPack pack, Node node) {
this.pack = pack;
this.node = node;
}
}
}
@@ -52,7 +52,7 @@ public class DataPack {
}
});
final RefsModel refsModel = new RefsModel(allRefs, hashGetter);
final RefsModel refsModel = new RefsModel(allRefs, indexGetter);
graphModel.getFragmentManager().setUnconcealedNodeFunction(new Function<Node, Boolean>() {
@NotNull
@Override
@@ -1,7 +1,7 @@
package com.intellij.vcs.log.data;
import com.intellij.openapi.util.Condition;
import com.intellij.util.Function;
import com.intellij.util.NotNullFunction;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.MultiMap;
import com.intellij.vcs.log.Hash;
@@ -19,10 +19,11 @@ public class RefsModel {
@NotNull private final Collection<VcsRef> myBranches;
@NotNull private final MultiMap<Hash, VcsRef> myRefsToHashes;
private final Function<Integer, Hash> myHashGetter;
@NotNull private final MultiMap<Integer, VcsRef> myRefsToIndices;
@NotNull private final NotNullFunction<Hash, Integer> myIndexGetter;
public RefsModel(@NotNull Collection<VcsRef> allRefs, Function<Integer, Hash> hashGetter) {
myHashGetter = hashGetter;
public RefsModel(@NotNull Collection<VcsRef> allRefs, @NotNull NotNullFunction<Hash, Integer> indexGetter) {
myIndexGetter = indexGetter;
myBranches = ContainerUtil.filter(allRefs, new Condition<VcsRef>() {
@Override
public boolean value(VcsRef ref) {
@@ -31,6 +32,16 @@ public class RefsModel {
});
myRefsToHashes = prepareRefsMap(allRefs);
myRefsToIndices = prepareRefsToIndicesMap(allRefs);
}
@NotNull
private MultiMap<Integer, VcsRef> prepareRefsToIndicesMap(@NotNull Collection<VcsRef> refs) {
MultiMap<Integer, VcsRef> map = MultiMap.create();
for (VcsRef ref : refs) {
map.putValue(myIndexGetter.fun(ref.getCommitHash()), ref);
}
return map;
}
@NotNull
@@ -42,8 +53,8 @@ public class RefsModel {
return map;
}
public boolean isBranchRef(@NotNull Hash commitHash) {
for (VcsRef ref : refsToCommit(commitHash)) {
public boolean isBranchRef(int hash) {
for (VcsRef ref : refsToCommit(hash)) {
if (ref.getType().isBranch()) {
return true;
}
@@ -59,6 +70,11 @@ public class RefsModel {
return Collections.emptyList();
}
@NotNull
public Collection<VcsRef> refsToCommit(int index) {
return myRefsToIndices.containsKey(index) ? myRefsToIndices.get(index) : Collections.<VcsRef>emptyList();
}
@NotNull
public Collection<VcsRef> getBranches() {
return myBranches;
@@ -69,8 +85,4 @@ public class RefsModel {
return new ArrayList<VcsRef>(myRefsToHashes.values());
}
public boolean isBranchRef(int hash) {
return isBranchRef(myHashGetter.fun(hash));
}
}
@@ -23,6 +23,7 @@ import com.intellij.openapi.progress.BackgroundTaskQueue;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.vcs.VcsException;
@@ -145,6 +146,7 @@ public class VcsLogDataHolder implements Disposable {
private final VcsLogHashMap myHashMap;
private final NotNullFunction<Integer, Hash> myHashGetter;
private final NotNullFunction<Hash, Integer> myIndexGetter;
private final ContainingBranchesGetter myContainingBranchesGetter;
public VcsLogDataHolder(@NotNull Project project,
@NotNull Map<VirtualFile, VcsLogProvider> logProviders, @NotNull VcsLogSettings settings) {
@@ -179,6 +181,7 @@ public class VcsLogDataHolder implements Disposable {
return putHash(hash);
}
};
myContainingBranchesGetter = new ContainingBranchesGetter(this);
}
@NotNull
@@ -373,8 +376,12 @@ public class VcsLogDataHolder implements Disposable {
VirtualFile root = entry.getKey();
RecentCommitsInfo info = entry.getValue();
Pair<List<TimedVcsCommit>, Integer> joinResult = myLogJoiner.addCommits(myLogData.getLog(root), myLogData.getRefs(root),
Collection<VcsRef> oldRefs = myLogData.getRefs(root);
Pair<List<TimedVcsCommit>, Integer> joinResult = myLogJoiner.addCommits(myLogData.getLog(root), oldRefs,
info.firstBlockCommits, info.newRefs);
if (!Comparing.haveEqualElements(oldRefs, info.newRefs)) {
myContainingBranchesGetter.clearCache();
}
List<TimedVcsCommit> refreshedLog = joinResult.getFirst();
int newCommitsCount = joinResult.getSecond();
// the value can significantly increase if user keeps IDEA open for a long time, and frequently receives many new commits,
@@ -437,6 +444,7 @@ public class VcsLogDataHolder implements Disposable {
myLogData = new LogData(logsToBuild, refsByRoot, compoundLog, dataPack, false);
}
myContainingBranchesGetter.clearCache();
handleOnSuccessInEdt(onSuccess, dataPack);
}
@@ -550,6 +558,10 @@ public class VcsLogDataHolder implements Disposable {
return mySettings;
}
public ContainingBranchesGetter getContainingBranchesGetter() {
return myContainingBranchesGetter;
}
private static class RecentCommitsInfo {
List<TimedVcsCommit> firstBlockCommits;
Collection<VcsRef> newRefs;
@@ -2,16 +2,19 @@ package com.intellij.vcs.log.ui.frame;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.changes.issueLinks.IssueLinkHtmlRenderer;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.ui.BrowserHyperlinkListener;
import com.intellij.ui.ScrollPaneFactory;
import com.intellij.ui.components.JBLoadingPanel;
import com.intellij.util.Function;
import com.intellij.util.text.DateFormatUtil;
import com.intellij.util.ui.UIUtil;
import com.intellij.vcs.log.Hash;
import com.intellij.vcs.log.VcsFullCommitDetails;
import com.intellij.vcs.log.VcsRef;
import com.intellij.vcs.log.data.DataPack;
import com.intellij.vcs.log.data.LoadingDetails;
import com.intellij.vcs.log.data.VcsLogDataHolder;
import com.intellij.vcs.log.graph.render.PrintParameters;
@@ -82,21 +85,24 @@ class DetailsPanel extends JPanel implements ListSelectionListener {
}
else {
((CardLayout)getLayout()).show(this, STANDARD_LAYER);
Hash hash = ((AbstractVcsLogTableModel)myGraphTable.getModel()).getHashAtRow(rows[0]);
int row = rows[0];
Hash hash = ((AbstractVcsLogTableModel)myGraphTable.getModel()).getHashAtRow(row);
if (hash == null) {
showMessage("No commits selected");
return;
}
VcsFullCommitDetails commitData = myLogDataHolder.getCommitDetailsGetter().getCommitData(hash);
if (commitData instanceof LoadingDetails) {
DataPack dataPack = myLogDataHolder.getDataPack();
List<VcsRef> branches = myLogDataHolder.getContainingBranchesGetter().requestContainingBranches(dataPack, dataPack.getNode(row));
if (commitData instanceof LoadingDetails || branches == null) {
myLoadingPanel.startLoading();
myDataPanel.setData(null);
myDataPanel.setData(null, null);
myRefsPanel.setRefs(Collections.<VcsRef>emptyList());
}
else {
myLoadingPanel.stopLoading();
myDataPanel.setData(commitData);
myDataPanel.setData(commitData, branches);
myRefsPanel.setRefs(sortRefs(hash, commitData.getRoot()));
}
}
@@ -123,19 +129,30 @@ class DetailsPanel extends JPanel implements ListSelectionListener {
setEditable(false);
myProject = project;
addHyperlinkListener(new BrowserHyperlinkListener());
setPreferredSize(new Dimension(150, 100));
}
void setData(@Nullable VcsFullCommitDetails commit) {
if (commit == null) {
void setData(@Nullable VcsFullCommitDetails commit, @Nullable List<VcsRef> branches) {
if (commit == null || branches == null) {
setText("");
}
else {
String body = getHashText(commit) + "<br/>" + getAuthorText(commit) + "<p>" + getMessageText(commit) + "</p>";
String body = getHashText(commit) + "<br/>" + getAuthorText(commit) + "<p>" + getMessageText(commit) + "</p>" +
"<p>" + getContainedBranchesText(branches) + "</p>";
setText("<html><head>" + UIUtil.getCssFontDeclaration(UIUtil.getLabelFont()) + "</head><body>" + body + "</body></html>");
setCaretPosition(0);
}
}
private static String getContainedBranchesText(List<VcsRef> branches) {
return "<i>Contained in branches:</i> " + StringUtil.join(branches, new Function<VcsRef, String>() {
@Override
public String fun(VcsRef ref) {
return ref.getName();
}
}, ", ");
}
private String getMessageText(VcsFullCommitDetails commit) {
String subject = commit.getSubject();
String description = subject.length() < commit.getFullMessage().length() ? commit.getFullMessage().substring(subject.length()) : "";
@@ -123,6 +123,12 @@ public class MainFrame extends JPanel implements TypeSafeDataProvider {
myDetailsPanel.valueChanged(null);
}
});
myLogDataHolder.getContainingBranchesGetter().addTaskCompletedListener(new Runnable() {
@Override
public void run() {
myDetailsPanel.valueChanged(null);
}
});
}
public void setupDetailsSplitter(boolean state) {