From fd8772d48f48788bb7edc7c453f54ccfb834fdaa Mon Sep 17 00:00:00 2001 From: Kirill Likhodedov Date: Fri, 22 Nov 2013 18:13:00 +0400 Subject: [PATCH] [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. --- .../log/data/ContainingBranchesGetter.java | 148 ++++++++++++++++++ .../com/intellij/vcs/log/data/DataPack.java | 2 +- .../com/intellij/vcs/log/data/RefsModel.java | 32 ++-- .../vcs/log/data/VcsLogDataHolder.java | 14 +- .../vcs/log/ui/frame/DetailsPanel.java | 31 +++- .../intellij/vcs/log/ui/frame/MainFrame.java | 6 + 6 files changed, 214 insertions(+), 19 deletions(-) create mode 100644 platform/vcs-log/impl/src/com/intellij/vcs/log/data/ContainingBranchesGetter.java diff --git a/platform/vcs-log/impl/src/com/intellij/vcs/log/data/ContainingBranchesGetter.java b/platform/vcs-log/impl/src/com/intellij/vcs/log/data/ContainingBranchesGetter.java new file mode 100644 index 000000000000..ddcea8986060 --- /dev/null +++ b/platform/vcs-log/impl/src/com/intellij/vcs/log/data/ContainingBranchesGetter.java @@ -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 myTaskExecutor; + @NotNull private final Collection myLoadingFinishedListeners = new ArrayList(); + @NotNull private final SLRUMap> myCache = new SLRUMap>(1000, 1000); + + ContainingBranchesGetter(@NotNull Disposable parentDisposable) { + myTaskExecutor = new SequentialLimitedLifoExecutor(parentDisposable, 10, new ThrowableConsumer() { + @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 requestContainingBranches(@NotNull DataPack dataPack, @NotNull Node node) { + List refs = myCache.get(node); + if (refs == null) { + myTaskExecutor.queue(new Task(dataPack, node)); + } + return refs; + } + + @NotNull + private List getContainingBranches(@NotNull DataPack dataPack, @NotNull Node node) { + RefsModel refsModel = dataPack.getRefsModel(); + Set containingBranches = ContainerUtil.newHashSet(); + + Set visitedNodes = ContainerUtil.newHashSet(); + Set nodesToCheck = ContainerUtil.newHashSet(); + nodesToCheck.add(node); + while (!nodesToCheck.isEmpty()) { + Iterator 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 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 getBranchesPointingToThisNode(@NotNull RefsModel refsModel, @NotNull Node node) { + return ContainerUtil.filter(refsModel.refsToCommit(node.getCommitIndex()), new Condition() { + @Override + public boolean value(VcsRef ref) { + return ref.getType().isBranch(); + } + }); + } + + private static List sortByName(Collection branches) { + List branchesList = new ArrayList(branches); + ContainerUtil.sort(branchesList, new Comparator() { + @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; + } + } + +} diff --git a/platform/vcs-log/impl/src/com/intellij/vcs/log/data/DataPack.java b/platform/vcs-log/impl/src/com/intellij/vcs/log/data/DataPack.java index baef6132d5d5..b0b402ca71da 100644 --- a/platform/vcs-log/impl/src/com/intellij/vcs/log/data/DataPack.java +++ b/platform/vcs-log/impl/src/com/intellij/vcs/log/data/DataPack.java @@ -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() { @NotNull @Override diff --git a/platform/vcs-log/impl/src/com/intellij/vcs/log/data/RefsModel.java b/platform/vcs-log/impl/src/com/intellij/vcs/log/data/RefsModel.java index 4078bbfe7965..8f79a75076a4 100644 --- a/platform/vcs-log/impl/src/com/intellij/vcs/log/data/RefsModel.java +++ b/platform/vcs-log/impl/src/com/intellij/vcs/log/data/RefsModel.java @@ -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 myBranches; @NotNull private final MultiMap myRefsToHashes; - private final Function myHashGetter; + @NotNull private final MultiMap myRefsToIndices; + @NotNull private final NotNullFunction myIndexGetter; - public RefsModel(@NotNull Collection allRefs, Function hashGetter) { - myHashGetter = hashGetter; + public RefsModel(@NotNull Collection allRefs, @NotNull NotNullFunction indexGetter) { + myIndexGetter = indexGetter; myBranches = ContainerUtil.filter(allRefs, new Condition() { @Override public boolean value(VcsRef ref) { @@ -31,6 +32,16 @@ public class RefsModel { }); myRefsToHashes = prepareRefsMap(allRefs); + myRefsToIndices = prepareRefsToIndicesMap(allRefs); + } + + @NotNull + private MultiMap prepareRefsToIndicesMap(@NotNull Collection refs) { + MultiMap 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 refsToCommit(int index) { + return myRefsToIndices.containsKey(index) ? myRefsToIndices.get(index) : Collections.emptyList(); + } + @NotNull public Collection getBranches() { return myBranches; @@ -69,8 +85,4 @@ public class RefsModel { return new ArrayList(myRefsToHashes.values()); } - public boolean isBranchRef(int hash) { - return isBranchRef(myHashGetter.fun(hash)); - } - } diff --git a/platform/vcs-log/impl/src/com/intellij/vcs/log/data/VcsLogDataHolder.java b/platform/vcs-log/impl/src/com/intellij/vcs/log/data/VcsLogDataHolder.java index 4af25dff7093..de7b48f18ef1 100644 --- a/platform/vcs-log/impl/src/com/intellij/vcs/log/data/VcsLogDataHolder.java +++ b/platform/vcs-log/impl/src/com/intellij/vcs/log/data/VcsLogDataHolder.java @@ -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 myHashGetter; private final NotNullFunction myIndexGetter; + private final ContainingBranchesGetter myContainingBranchesGetter; public VcsLogDataHolder(@NotNull Project project, @NotNull Map 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, Integer> joinResult = myLogJoiner.addCommits(myLogData.getLog(root), myLogData.getRefs(root), + Collection oldRefs = myLogData.getRefs(root); + Pair, Integer> joinResult = myLogJoiner.addCommits(myLogData.getLog(root), oldRefs, info.firstBlockCommits, info.newRefs); + if (!Comparing.haveEqualElements(oldRefs, info.newRefs)) { + myContainingBranchesGetter.clearCache(); + } List 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 firstBlockCommits; Collection newRefs; diff --git a/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/frame/DetailsPanel.java b/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/frame/DetailsPanel.java index f93b0295fdba..47052e936b62 100644 --- a/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/frame/DetailsPanel.java +++ b/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/frame/DetailsPanel.java @@ -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 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.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 branches) { + if (commit == null || branches == null) { setText(""); } else { - String body = getHashText(commit) + "
" + getAuthorText(commit) + "

" + getMessageText(commit) + "

"; + String body = getHashText(commit) + "
" + getAuthorText(commit) + "

" + getMessageText(commit) + "

" + + "

" + getContainedBranchesText(branches) + "

"; setText("" + UIUtil.getCssFontDeclaration(UIUtil.getLabelFont()) + "" + body + ""); setCaretPosition(0); } } + private static String getContainedBranchesText(List branches) { + return "Contained in branches: " + StringUtil.join(branches, new Function() { + @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()) : ""; diff --git a/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/frame/MainFrame.java b/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/frame/MainFrame.java index 008cde930488..987b797803d1 100644 --- a/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/frame/MainFrame.java +++ b/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/frame/MainFrame.java @@ -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) {