[vcs-log] introduce support for the "--first-parent" option

IDEA-127140

GitOrigin-RevId: cb4ad33bd66bafc950d6b679c2d3895f05841a7e
This commit is contained in:
Julia Beliaeva
2024-04-03 01:35:25 +00:00
committed by intellij-monorepo-bot
parent 7cecc1ed74
commit bbaf4f9f4d
10 changed files with 242 additions and 39 deletions
@@ -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
@@ -67,6 +67,11 @@ interface PermanentGraph<Id> {
*/
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)
@@ -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<Int>,
visibleHeadsIds: Set<Int>? = 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<Int>,
visibleHeadsIds: Set<Int>? = 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)
}
}
}
}
}
@@ -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<Int>?, visibleHeadsIds: Set<Int>? = 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<Int>, matchedNodes: Set<Int>?): 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<Int>, matchedNodes: Set<Int>?): Pair<UnsignedBitSet, EdgeStorageWrapper> {
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)
}
}
@@ -38,29 +38,35 @@ class PermanentGraphImpl<CommitId : Any> private constructor(private val permane
private val reachableNodes = ReachableNodes(LinearGraphUtils.asLiteLinearGraph(permanentLinearGraph))
private fun createFilteredController(options: PermanentGraph.Options, visibleHeads: Set<CommitId>?, matchingCommits: Set<CommitId>?): 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,
@@ -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()) {
@@ -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<Int>, matchedNodes: Set<Int>? = 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()
}
}
}
@@ -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;
}
}
}
@@ -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<String> {
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
@@ -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)