diff --git a/platform/vcs-log/api/resources/messages/VcsLogBundle.properties b/platform/vcs-log/api/resources/messages/VcsLogBundle.properties index de4ca684471b..6bc76477b6a9 100644 --- a/platform/vcs-log/api/resources/messages/VcsLogBundle.properties +++ b/platform/vcs-log/api/resources/messages/VcsLogBundle.properties @@ -305,6 +305,8 @@ vcs.log.column.hash=Hash # graph options graph.options.linear=Linearize Merges graph.options.linear.description=In case of merge show incoming commits on top of main branch commits as if they were rebased +graph.options.first.parent=First Parent +graph.options.first.parent.description=Follow only the first parent commit upon seeing a merge commit graph.sort.standard=Topologically graph.sort.standard.description=In case of merge show incoming commits first, directly below merge commit graph.sort.off=By Commit Date diff --git a/platform/vcs-log/graph-api/src/com/intellij/vcs/log/graph/PermanentGraph.kt b/platform/vcs-log/graph-api/src/com/intellij/vcs/log/graph/PermanentGraph.kt index 293b99d327fe..55f94ea2f753 100644 --- a/platform/vcs-log/graph-api/src/com/intellij/vcs/log/graph/PermanentGraph.kt +++ b/platform/vcs-log/graph-api/src/com/intellij/vcs/log/graph/PermanentGraph.kt @@ -67,6 +67,11 @@ interface PermanentGraph { */ data object LinearBek : Options() + /** + * Follow only the first parent commit upon seeing a merge commit. + */ + data object FirstParent: Options() + companion object { @JvmField val Default = Base(SortType.Normal) diff --git a/platform/vcs-log/graph/src/com/intellij/vcs/log/graph/impl/facade/FilteredController.kt b/platform/vcs-log/graph/src/com/intellij/vcs/log/graph/impl/facade/FilteredController.kt index f28a762653ac..2e092855830b 100644 --- a/platform/vcs-log/graph/src/com/intellij/vcs/log/graph/impl/facade/FilteredController.kt +++ b/platform/vcs-log/graph/src/com/intellij/vcs/log/graph/impl/facade/FilteredController.kt @@ -6,22 +6,13 @@ import com.intellij.vcs.log.graph.api.permanent.PermanentGraphInfo import com.intellij.vcs.log.graph.collapsing.CollapsedGraph import com.intellij.vcs.log.graph.collapsing.DottedFilterEdgesGenerator import com.intellij.vcs.log.graph.utils.LinearGraphUtils -import com.intellij.vcs.log.graph.utils.UnsignedBitSet import com.intellij.vcs.log.graph.utils.getReachableMatchingNodes -class FilteredController(delegateLinearGraphController: LinearGraphController, - permanentGraphInfo: PermanentGraphInfo<*>, - matchedIds: Set, - visibleHeadsIds: Set? = null) : +class FilteredController(delegateLinearGraphController: LinearGraphController, permanentGraphInfo: PermanentGraphInfo<*>, + buildCollapsedGraph: () -> CollapsedGraph) : CascadeController(delegateLinearGraphController, permanentGraphInfo) { - val collapsedGraph: CollapsedGraph = buildGraph(permanentGraphInfo.linearGraph.getReachableMatchingNodes(visibleHeadsIds, matchedIds)) - - private fun buildGraph(visibility: UnsignedBitSet): CollapsedGraph { - return CollapsedGraph.newInstance(delegateController.compiledGraph, visibility).also { - DottedFilterEdgesGenerator.update(it, 0, it.delegatedGraph.nodesCount() - 1) - } - } + val collapsedGraph: CollapsedGraph = buildCollapsedGraph() override fun performLinearGraphAction(action: LinearGraphController.LinearGraphAction): LinearGraphController.LinearGraphAnswer { // filter prohibits any actions on delegate graph for now @@ -41,4 +32,18 @@ class FilteredController(delegateLinearGraphController: LinearGraphController, override fun performAction(action: LinearGraphController.LinearGraphAction) = null override fun getCompiledGraph() = collapsedGraph.compiledGraph -} + + companion object { + fun create(delegateController: LinearGraphController, + permanentGraphInfo: PermanentGraphInfo<*>, + matchedIds: Set, + visibleHeadsIds: Set? = null): FilteredController { + val visibility = delegateController.compiledGraph.getReachableMatchingNodes(visibleHeadsIds, matchedIds) + return FilteredController(delegateController, permanentGraphInfo) { + CollapsedGraph.newInstance(delegateController.compiledGraph, visibility).also { + DottedFilterEdgesGenerator.update(it, 0, it.delegatedGraph.nodesCount() - 1) + } + } + } + } +} \ No newline at end of file diff --git a/platform/vcs-log/graph/src/com/intellij/vcs/log/graph/impl/facade/FirstParentController.kt b/platform/vcs-log/graph/src/com/intellij/vcs/log/graph/impl/facade/FirstParentController.kt new file mode 100644 index 000000000000..166c4054e6f2 --- /dev/null +++ b/platform/vcs-log/graph/src/com/intellij/vcs/log/graph/impl/facade/FirstParentController.kt @@ -0,0 +1,59 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.vcs.log.graph.impl.facade + +import com.intellij.vcs.log.graph.api.EdgeFilter +import com.intellij.vcs.log.graph.api.LinearGraph +import com.intellij.vcs.log.graph.api.permanent.PermanentGraphInfo +import com.intellij.vcs.log.graph.collapsing.CollapsedGraph +import com.intellij.vcs.log.graph.collapsing.DottedFilterEdgesGenerator +import com.intellij.vcs.log.graph.collapsing.EdgeStorageWrapper +import com.intellij.vcs.log.graph.impl.permanent.GraphLayoutBuilder.getHeads +import com.intellij.vcs.log.graph.utils.Dfs +import com.intellij.vcs.log.graph.utils.UnsignedBitSet +import com.intellij.vcs.log.graph.utils.impl.BitSetFlags +import org.jetbrains.annotations.VisibleForTesting + +object FirstParentController { + fun create(delegateController: LinearGraphController, permanentGraphInfo: PermanentGraphInfo<*>, + matchedIds: Set?, visibleHeadsIds: Set? = null): FilteredController { + val linearGraph = permanentGraphInfo.linearGraph + val startNodes = visibleHeadsIds ?: (permanentGraphInfo.branchNodeIds + linearGraph.getHeads()) + return FilteredController(delegateController, permanentGraphInfo) { + buildCollapsedGraph(linearGraph, startNodes, matchedIds) + } + } + + @VisibleForTesting + fun buildCollapsedGraph(linearGraph: LinearGraph, startNodes: Set, matchedNodes: Set?): CollapsedGraph { + val (visibleNodes, hiddenEdges) = linearGraph.getVisibleNodesAndHiddenEdges(startNodes, matchedNodes) + + val collapsedGraph = CollapsedGraph.newInstance(LinearGraphWrapper(linearGraph, hiddenEdges = hiddenEdges), visibleNodes) + if (matchedNodes != null) { + DottedFilterEdgesGenerator.update(collapsedGraph, 0, collapsedGraph.delegatedGraph.nodesCount() - 1) + } + return collapsedGraph + } + + private fun LinearGraph.getVisibleNodesAndHiddenEdges(startNodes: Set, matchedNodes: Set?): Pair { + val visibleNodes = UnsignedBitSet() + val hiddenEdges = EdgeStorageWrapper.createSimpleEdgeStorage() + + val visited = BitSetFlags(nodesCount()) + for (start in startNodes) { + if (start < 0) continue + + var node = start + while (node != Dfs.NextNode.NODE_NOT_FOUND) { + if (visited[node]) break + visited[node] = true + + if (matchedNodes == null || matchedNodes.contains(node)) visibleNodes[node] = true + + val downEdges = getAdjacentEdges(node, EdgeFilter.NORMAL_DOWN) + downEdges.drop(1).forEach { hiddenEdges.createEdge(it) } + node = downEdges.firstOrNull()?.downNodeIndex ?: Dfs.NextNode.NODE_NOT_FOUND + } + } + return Pair(visibleNodes, hiddenEdges) + } +} \ No newline at end of file diff --git a/platform/vcs-log/graph/src/com/intellij/vcs/log/graph/impl/facade/PermanentGraphImpl.kt b/platform/vcs-log/graph/src/com/intellij/vcs/log/graph/impl/facade/PermanentGraphImpl.kt index 8146c68e91e6..ddd74172fa9f 100644 --- a/platform/vcs-log/graph/src/com/intellij/vcs/log/graph/impl/facade/PermanentGraphImpl.kt +++ b/platform/vcs-log/graph/src/com/intellij/vcs/log/graph/impl/facade/PermanentGraphImpl.kt @@ -38,29 +38,35 @@ class PermanentGraphImpl private constructor(private val permane private val reachableNodes = ReachableNodes(LinearGraphUtils.asLiteLinearGraph(permanentLinearGraph)) private fun createFilteredController(options: PermanentGraph.Options, visibleHeads: Set?, matchingCommits: Set?): LinearGraphController { - val baseController = when (options) { - PermanentGraph.Options.LinearBek -> LinearBekController(BekBaseController(this, bekIntMap), this) + val visibleHeadsIds = if (visibleHeads != null) permanentCommitsInfo.convertToNodeIds(visibleHeads) else null + val matchingCommitIds = if (matchingCommits != null) permanentCommitsInfo.convertToNodeIds(matchingCommits) else null + + when (options) { is PermanentGraph.Options.Base -> { - when (options.sortType) { + val baseController = when (options.sortType) { PermanentGraph.SortType.Normal -> BaseController(this) PermanentGraph.SortType.Bek -> BekBaseController(this, bekIntMap) } + if (matchingCommitIds != null) { + return FilteredController.create(baseController, this, matchingCommitIds, visibleHeadsIds) + } + return CollapsedController(baseController, this, visibleHeadsIds) + } + PermanentGraph.Options.FirstParent -> { + val baseController = BaseController(this) + return FirstParentController.create(baseController, this, matchingCommitIds, visibleHeadsIds) + } + PermanentGraph.Options.LinearBek -> { + val baseController = LinearBekController(BekBaseController(this, bekIntMap), this) + if (matchingCommitIds != null) { + return FilteredController.create(baseController, this, matchingCommitIds, visibleHeadsIds) + } + if (visibleHeadsIds != null) { + return BranchFilterController(baseController, this, visibleHeadsIds) + } + return baseController } } - - val visibleHeadsIds = if (visibleHeads != null) permanentCommitsInfo.convertToNodeIds(visibleHeads) else null - if (matchingCommits != null) { - return FilteredController(baseController, this, permanentCommitsInfo.convertToNodeIds(matchingCommits), visibleHeadsIds) - } - - if (options == PermanentGraph.Options.LinearBek) { - if (visibleHeadsIds != null) { - return BranchFilterController(baseController, this, visibleHeadsIds) - } - return baseController - } - - return CollapsedController(baseController, this, visibleHeadsIds) } fun createVisibleGraph(options: PermanentGraph.Options, diff --git a/platform/vcs-log/graph/src/com/intellij/vcs/log/graph/impl/permanent/GraphLayoutBuilder.kt b/platform/vcs-log/graph/src/com/intellij/vcs/log/graph/impl/permanent/GraphLayoutBuilder.kt index 06135ea9d1d4..42adce72abef 100644 --- a/platform/vcs-log/graph/src/com/intellij/vcs/log/graph/impl/permanent/GraphLayoutBuilder.kt +++ b/platform/vcs-log/graph/src/com/intellij/vcs/log/graph/impl/permanent/GraphLayoutBuilder.kt @@ -43,7 +43,7 @@ object GraphLayoutBuilder { return this } - private fun LinearGraph.getHeads(): IntList { + internal fun LinearGraph.getHeads(): IntList { val heads = IntArrayList() for (i in 0 until nodesCount()) { if (LinearGraphUtils.getUpNodes(this, i).isEmpty()) { diff --git a/platform/vcs-log/graph/test/com/intellij/vcs/log/graph/impl/facade/FirstParentTest.kt b/platform/vcs-log/graph/test/com/intellij/vcs/log/graph/impl/facade/FirstParentTest.kt new file mode 100644 index 000000000000..fabcc5553e1e --- /dev/null +++ b/platform/vcs-log/graph/test/com/intellij/vcs/log/graph/impl/facade/FirstParentTest.kt @@ -0,0 +1,103 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.vcs.log.graph.impl.facade + +import com.intellij.vcs.log.graph.TestGraphBuilder +import com.intellij.vcs.log.graph.api.LinearGraph +import com.intellij.vcs.log.graph.graph +import com.intellij.vcs.log.graph.linearBek.assertEquals +import org.junit.Test + +class FirstParentTest { + private fun LinearGraph.assertFirstParent(startNodes: Set, matchedNodes: Set? = null, expected: TestGraphBuilder.() -> Unit) { + val actualGraph = FirstParentController.buildCollapsedGraph(this, startNodes, matchedNodes).compiledGraph + assertEquals(expected, actualGraph) + } + + @Test + fun simpleMerge() { + graph { + 0(1) + 1(2, 3) + 2(4) + 3(4) + 4() + }.assertFirstParent(setOf(0)) { + 0(1) + 1(2) + 2(4) + 4() + } + } + + @Test + fun twoBranches() { + graph { + 0(2) + 1(5) + 2(3, 4) + 3(6) + 4(5) + 5(6) + 6() + }.assertFirstParent(setOf(0, 1)) { + 0(2) + 1(5) + 2(3) + 3(6) + 5(6) + 6() + } + } + + @Test + fun startNotAtHead() { + graph { + 0(1) + 1(2, 3) + 2(4) + 3(4) + 4() + }.assertFirstParent(setOf(3)) { + 3(4) + 4() + } + } + + @Test + fun featureBranchMerges() { + // main branch merged into feature, then feature merged into master + graph { + 0(2) + 1(3) + 2(5, 3) + 3(4) + 4(6, 5) + 5(7) + 6(7) + 7() + }.assertFirstParent(setOf(0, 1)) { + 0(2) + 1(3) + 2(5) + 3(4) + 4(6) + 5(7) + 6(7) + 7() + } + } + + @Test + fun filteredMerge() { + graph { + 0(1) + 1(2, 3) + 2(4) + 3(4) + 4() + }.assertFirstParent(setOf(0), setOf(1, 3, 4)) { + 1(4.dot) + 4() + } + } +} \ No newline at end of file diff --git a/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/actions/VcsLogGraphOptionsChooserGroup.java b/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/actions/VcsLogGraphOptionsChooserGroup.java index c7dab8d56aae..3d72d0fd16d1 100644 --- a/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/actions/VcsLogGraphOptionsChooserGroup.java +++ b/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/actions/VcsLogGraphOptionsChooserGroup.java @@ -37,10 +37,11 @@ public class VcsLogGraphOptionsChooserGroup extends DefaultActionGroup { actions.addAll(ContainerUtil.map(sortTypes, sortType -> { return new SelectOptionsAction(logUI, properties, new PermanentGraph.Options.Base(sortType)); })); + actions.add(Separator.create(VcsLogBundle.message("action.vcs.log.graph.options.separator"))); if (BekUtil.isLinearBekEnabled()) { - actions.add(Separator.create(VcsLogBundle.message("action.vcs.log.graph.options.separator"))); - actions.add(new SelectOptionsAction(logUI, properties, PermanentGraph.Options.LinearBek.INSTANCE)); + actions.add(new SelectNonBaseOptionsAction(logUI, properties, PermanentGraph.Options.LinearBek.INSTANCE)); } + actions.add(new SelectNonBaseOptionsAction(logUI, properties, PermanentGraph.Options.FirstParent.INSTANCE)); return actions.toArray(EMPTY_ARRAY); } @@ -64,7 +65,7 @@ public class VcsLogGraphOptionsChooserGroup extends DefaultActionGroup { } private static class SelectOptionsAction extends ToggleAction implements DumbAware { - private final PermanentGraph.Options myGraphOptions; + protected final PermanentGraph.Options myGraphOptions; private final VcsLogUi myUI; private final VcsLogUiProperties myProperties; @@ -93,14 +94,31 @@ public class VcsLogGraphOptionsChooserGroup extends DefaultActionGroup { @Override public void setSelected(@NotNull AnActionEvent e, boolean state) { - if (state && myProperties.exists(MainVcsLogUiProperties.GRAPH_OPTIONS)) { - myProperties.set(MainVcsLogUiProperties.GRAPH_OPTIONS, myGraphOptions); + if (myProperties.exists(MainVcsLogUiProperties.GRAPH_OPTIONS)) { + myProperties.set(MainVcsLogUiProperties.GRAPH_OPTIONS, getOptionsToSet(state)); } } + protected @NotNull PermanentGraph.Options getOptionsToSet(boolean state) { + return myGraphOptions; + } + @Override public @NotNull ActionUpdateThread getActionUpdateThread() { return ActionUpdateThread.EDT; } } + + private static class SelectNonBaseOptionsAction extends SelectOptionsAction { + SelectNonBaseOptionsAction(@NotNull VcsLogUi ui, + @NotNull VcsLogUiProperties properties, + @NotNull PermanentGraph.Options options) { + super(ui, properties, options); + } + + @Override + protected @NotNull PermanentGraph.Options getOptionsToSet(boolean state) { + return state ? myGraphOptions : PermanentGraph.Options.Default; + } + } } diff --git a/platform/vcs-log/impl/src/com/intellij/vcs/log/util/GraphOptionsUtil.kt b/platform/vcs-log/impl/src/com/intellij/vcs/log/util/GraphOptionsUtil.kt index 26a936c10d03..33b213058f3e 100644 --- a/platform/vcs-log/impl/src/com/intellij/vcs/log/util/GraphOptionsUtil.kt +++ b/platform/vcs-log/impl/src/com/intellij/vcs/log/util/GraphOptionsUtil.kt @@ -26,6 +26,7 @@ object GraphOptionsUtil { @NlsActions.ActionText get() = when (this) { is PermanentGraph.Options.Base -> sortType.localizedName PermanentGraph.Options.LinearBek -> VcsLogBundle.message("graph.options.linear") + PermanentGraph.Options.FirstParent -> VcsLogBundle.message("graph.options.first.parent") } @JvmStatic @@ -33,25 +34,28 @@ object GraphOptionsUtil { @NlsActions.ActionDescription get() = when (this) { is PermanentGraph.Options.Base -> sortType.localizedDescription PermanentGraph.Options.LinearBek -> VcsLogBundle.message("graph.options.linear.description") + PermanentGraph.Options.FirstParent -> VcsLogBundle.message("graph.options.first.parent.description") } private const val BASE = "Base" private const val LINEAR_BEK = "LinearBek" + private const val FIRST_PARENT = "FirstParent" @JvmStatic - val optionKindNames = listOf(BASE, LINEAR_BEK) + val optionKindNames = listOf(BASE, LINEAR_BEK, FIRST_PARENT) @JvmStatic val PermanentGraph.Options.kindName get() = when (this) { is PermanentGraph.Options.Base -> BASE PermanentGraph.Options.LinearBek -> LINEAR_BEK + PermanentGraph.Options.FirstParent -> FIRST_PARENT } internal fun PermanentGraph.Options.toStringList(): List { return when (this) { is PermanentGraph.Options.Base -> listOf(kindName, sortType.presentation) - PermanentGraph.Options.LinearBek -> listOf(kindName) + PermanentGraph.Options.LinearBek, PermanentGraph.Options.FirstParent -> listOf(kindName) } } @@ -61,6 +65,7 @@ object GraphOptionsUtil { val kind = first() return when (kind) { LINEAR_BEK -> PermanentGraph.Options.LinearBek + FIRST_PARENT -> PermanentGraph.Options.FirstParent BASE -> { if (size != 2) return null val sortType = PermanentGraph.SortType.entries.find { it.presentation == get(1) } ?: return null diff --git a/platform/vcs-log/impl/test/com/intellij/vcs/log/history/FileHistoryTest.kt b/platform/vcs-log/impl/test/com/intellij/vcs/log/history/FileHistoryTest.kt index cd1a1ec943e6..1a8534af27d8 100644 --- a/platform/vcs-log/impl/test/com/intellij/vcs/log/history/FileHistoryTest.kt +++ b/platform/vcs-log/impl/test/com/intellij/vcs/log/history/FileHistoryTest.kt @@ -25,7 +25,7 @@ class FileHistoryTest { fun LinearGraph.assert(startCommit: Int, startPath: FilePath, fileNamesData: FileHistoryData, result: TestGraphBuilder.() -> Unit) { val permanentGraphInfo = TestPermanentGraphInfo(this) val baseController = BaseController(permanentGraphInfo) - val filteredController = FilteredController(baseController, permanentGraphInfo, fileNamesData.getCommits()) + val filteredController = FilteredController.create(baseController, permanentGraphInfo, fileNamesData.getCommits()) val historyBuilder = FileHistoryBuilder(startCommit, startPath, fileNamesData, FileHistory.EMPTY) historyBuilder.accept(filteredController, permanentGraphInfo)