[coverage] IDEA-353814 Files filtering based on git commit history

IJ-CR-135534

GitOrigin-RevId: 93c6aa5c27f75224a2819ea8097c1a749d5434d4
This commit is contained in:
Maksim Zuev
2024-05-22 12:30:15 +02:00
committed by intellij-monorepo-bot
parent 7cfb5a7419
commit c0e9691d2b
13 changed files with 446 additions and 12 deletions

1
.idea/modules.xml generated
View File

@@ -957,6 +957,7 @@
<module fileurl="file://$PROJECT_DIR$/plugins/ui-designer-core/intellij.uiDesigner.iml" filepath="$PROJECT_DIR$/plugins/ui-designer-core/intellij.uiDesigner.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/changeReminder/intellij.vcs.changeReminder.iml" filepath="$PROJECT_DIR$/plugins/changeReminder/intellij.vcs.changeReminder.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/git4idea/intellij.vcs.git.iml" filepath="$PROJECT_DIR$/plugins/git4idea/intellij.vcs.git.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/git4idea/intellij.vcs.git.coverage/intellij.vcs.git.coverage.iml" filepath="$PROJECT_DIR$/plugins/git4idea/intellij.vcs.git.coverage/intellij.vcs.git.coverage.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/git-features-trainer/intellij.vcs.git.featuresTrainer.iml" filepath="$PROJECT_DIR$/plugins/git-features-trainer/intellij.vcs.git.featuresTrainer.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/git4idea/rt/intellij.vcs.git.rt.iml" filepath="$PROJECT_DIR$/plugins/git4idea/rt/intellij.vcs.git.rt.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/github/intellij.vcs.github.iml" filepath="$PROJECT_DIR$/plugins/github/intellij.vcs.github.iml" />

View File

@@ -9,6 +9,7 @@
<extensionPoint qualifiedName="com.intellij.coverageRunner" interface="com.intellij.coverage.CoverageRunner" dynamic="true"/>
<extensionPoint qualifiedName="com.intellij.coverageEngine" interface="com.intellij.coverage.CoverageEngine" dynamic="true"/>
<extensionPoint qualifiedName="com.intellij.coverageOptions" interface="com.intellij.coverage.CoverageOptions" area="IDEA_PROJECT" dynamic="true"/>
<extensionPoint qualifiedName="com.intellij.coverageModifiedFilesFilterFactory" interface="com.intellij.coverage.filters.ModifiedFilesFilterFactory" dynamic="true"/>
</extensionPoints>
<extensions defaultExtensionNs="com.intellij">

View File

@@ -86,6 +86,7 @@ coverage.error.loading.report=Error loading coverage report
coverage.flatten.packages=Flatten packages
coverage.hide.fully.covered.elements=Hide Fully Covered {0}
coverage.show.only.modified.elements=Show Only {0} with Uncommitted Changes
coverage.show.only.elements.in.feature.branch=Show Only Modified {0} (Compared to {1} Branch)
coverage.show.fully.covered.elements=Show fully covered {0}
coverage.show.unmodified.elements=Show {0} without uncommitted changes
coverage.view.filters.group=Filters

View File

@@ -39,7 +39,8 @@ public abstract class BaseCoverageAnnotator implements CoverageAnnotator {
ProgressManager.getInstance().run(new Task.Backgroundable(project, CoverageBundle.message("coverage.view.loading.data"), true) {
@Override
public void run(@NotNull ProgressIndicator indicator) {
request.run();
myModifiedFilesFilter = ModifiedFilesFilter.create(project);
request.run();
}
@Override
@@ -64,9 +65,7 @@ public abstract class BaseCoverageAnnotator implements CoverageAnnotator {
@ApiStatus.Internal
@Override
public synchronized @Nullable ModifiedFilesFilter getModifiedFilesFilter() {
if (myModifiedFilesFilter != null) return myModifiedFilesFilter;
myModifiedFilesFilter = ModifiedFilesFilter.create(myProject);
public @Nullable ModifiedFilesFilter getModifiedFilesFilter() {
return myModifiedFilesFilter;
}

View File

@@ -6,9 +6,10 @@ import com.intellij.openapi.vcs.FileStatus
import com.intellij.openapi.vcs.FileStatusManager
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.Nls
@ApiStatus.Internal
class ModifiedFilesFilter(private val project: Project) {
open class ModifiedFilesFilter(private val project: Project) {
@Volatile
var hasFilteredFiles: Boolean = false
@@ -19,7 +20,7 @@ class ModifiedFilesFilter(private val project: Project) {
}
fun isFileModified(file: VirtualFile): Boolean {
val isModified = isModifiedLocally(file)
val isModified = isModifiedLocally(file) || isInModifiedScope(file)
return isModified.also {
if (!isModified && !hasFilteredFiles) {
hasFilteredFiles = true
@@ -27,6 +28,10 @@ class ModifiedFilesFilter(private val project: Project) {
}
}
open fun isInModifiedScope(file: VirtualFile) = false
open fun getBranchName(): @Nls String? = null
private fun isModifiedLocally(file: VirtualFile): Boolean {
val status = FileStatusManager.getInstance(project).getStatus(file)
val isCurrentlyChanged = status === FileStatus.MODIFIED || status === FileStatus.ADDED || status === FileStatus.UNKNOWN
@@ -36,6 +41,10 @@ class ModifiedFilesFilter(private val project: Project) {
companion object {
@JvmStatic
fun create(project: Project): ModifiedFilesFilter {
val filter = ModifiedFilesFilterFactory.EP_NAME.computeSafeIfAny {
it.createFilter(project)
}
if (filter != null) return filter
return ModifiedFilesFilter(project)
}
}

View File

@@ -0,0 +1,16 @@
// 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.coverage.filters
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.project.Project
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
interface ModifiedFilesFilterFactory {
fun createFilter(project: Project): ModifiedFilesFilter?
companion object {
@JvmStatic
val EP_NAME: ExtensionPointName<ModifiedFilesFilterFactory> = ExtensionPointName.create("com.intellij.coverageModifiedFilesFilterFactory")
}
}

View File

@@ -26,6 +26,7 @@ import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.NlsActions;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.vcs.FileStatusListener;
import com.intellij.openapi.vcs.FileStatusManager;
@@ -46,6 +47,7 @@ import com.intellij.util.ui.StatusText;
import com.intellij.util.ui.UIUtil;
import com.intellij.util.ui.components.BorderLayoutPanel;
import com.intellij.util.ui.tree.TreeUtil;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -64,6 +66,7 @@ import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
public class CoverageView extends BorderLayoutPanel implements DataProvider, Disposable {
@NonNls private static final String ACTION_DRILL_DOWN = "DrillDown";
@@ -192,11 +195,15 @@ public class CoverageView extends BorderLayoutPanel implements DataProvider, Dis
}
private boolean hasVCSFilteredNodes() {
CoverageAnnotator annotator = mySuitesBundle.getCoverageEngine().getCoverageAnnotator(myProject);
ModifiedFilesFilter filter = annotator.getModifiedFilesFilter();
var filter = getModifiedFilesFilter();
return filter != null && filter.getHasFilteredFiles();
}
private @Nullable ModifiedFilesFilter getModifiedFilesFilter() {
CoverageAnnotator annotator = mySuitesBundle.getCoverageEngine().getCoverageAnnotator(myProject);
return annotator.getModifiedFilesFilter();
}
private void setUpShowRootNode(ActionToolbar actionToolbar) {
final var showFull = new Ref<>(false);
myModel.addTreeModelListener(new TreeModelListener() {
@@ -386,8 +393,8 @@ public class CoverageView extends BorderLayoutPanel implements DataProvider, Dis
boolean hasFilters = false;
final DefaultActionGroup filtersActionGroup = new DefaultActionGroup();
if (ProjectLevelVcsManager.getInstance(myProject).hasActiveVcss()) {
filtersActionGroup.add(new ShowOnlyModifiedAction());
if (ProjectLevelVcsManager.getInstance(myProject).hasActiveVcss() && getModifiedFilesFilter() != null) {
filtersActionGroup.add(new ShowOnlyModifiedAction(getModifiedActionName()));
hasFilters = true;
myHasVCSFilter = true;
}
@@ -574,8 +581,8 @@ public class CoverageView extends BorderLayoutPanel implements DataProvider, Dis
private final class ShowOnlyModifiedAction extends ToggleAction {
private ShowOnlyModifiedAction() {
super(CoverageBundle.messagePointer("coverage.show.only.modified.elements", myViewExtension.getElementsCapitalisedName()));
private ShowOnlyModifiedAction(@NlsActions.ActionText String name) {
super(name);
}
@Override
@@ -594,6 +601,17 @@ public class CoverageView extends BorderLayoutPanel implements DataProvider, Dis
}
}
private @Nls @NotNull String getModifiedActionName() {
String elementName = myViewExtension.getElementsCapitalisedName();
ModifiedFilesFilter filter = getModifiedFilesFilter();
String branchName = Objects.requireNonNull(filter).getBranchName();
if (branchName != null) {
return CoverageBundle.message("coverage.show.only.elements.in.feature.branch", elementName, branchName);
} else {
return CoverageBundle.message("coverage.show.only.modified.elements", elementName);
}
}
private void select(Object object) {
ReadAction.nonBlocking(() -> {
final PsiElement element = myViewExtension.getElementToSelect(object);

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/testSrc" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/resources" type="java-resource" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" scope="TEST" name="JUnit5" level="project" />
<orderEntry type="library" name="kotlin-stdlib" level="project" />
<orderEntry type="module" module-name="intellij.platform.ide" />
<orderEntry type="module" module-name="intellij.platform.ide.core" />
<orderEntry type="module" module-name="intellij.platform.coverage" />
<orderEntry type="module" module-name="intellij.vcs.git" />
<orderEntry type="module" module-name="intellij.platform.vcs.core" />
<orderEntry type="module" module-name="intellij.platform.vcs.log" />
<orderEntry type="module" module-name="intellij.platform.vcs.log.impl" />
<orderEntry type="module" module-name="intellij.platform.vcs.log.graph.impl" />
</component>
</module>

View File

@@ -0,0 +1,11 @@
<idea-plugin package="com.intellij.vcs.git.coverage">
<dependencies>
<module name="intellij.platform.coverage"/>
</dependencies>
<extensions defaultExtensionNs="com.intellij">
<coverageModifiedFilesFilterFactory implementation="com.intellij.vcs.git.coverage.GitModifiedFilesFilterFactory"/>
<registryKey key="coverage.filter.based.on.feature.branch"
defaultValue="true"
description="Show only files modified in the current feature branch in the coverage view"/>
</extensions>
</idea-plugin>

View File

@@ -0,0 +1,146 @@
// 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.git.coverage
import com.intellij.vcs.git.coverage.CurrentFeatureBranchBaseDetector.Status
import com.intellij.openapi.diagnostic.ControlFlowException
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.vcs.log.Hash
import com.intellij.vcs.log.graph.api.LinearGraph
import com.intellij.vcs.log.graph.api.LiteLinearGraph
import com.intellij.vcs.log.graph.api.permanent.PermanentGraphInfo
import com.intellij.vcs.log.graph.utils.BfsWalk
import com.intellij.vcs.log.graph.utils.DfsWalk
import com.intellij.vcs.log.graph.utils.LinearGraphUtils
import com.intellij.vcs.log.graph.utils.impl.BitSetFlags
import com.intellij.vcs.log.impl.HashImpl
import com.intellij.vcs.log.impl.VcsProjectLog
import git4idea.GitRemoteBranch
import git4idea.config.GitSharedSettings
import git4idea.repo.GitRepository
import org.jetbrains.annotations.VisibleForTesting
internal class CurrentFeatureBranchBaseDetector(private val repository: GitRepository) {
private val logData = VcsProjectLog.getInstance(repository.project).dataManager
private val storage = logData?.storage
private val pack = logData?.dataPack
@Suppress("UNCHECKED_CAST")
private val permanentGraph = pack?.permanentGraph as? PermanentGraphInfo<Int>
fun findBaseCommit(): Status {
val project = repository.project
val remoteBranches = repository.branches.remoteBranches
val protectedBranches = remoteBranches.filter { GitSharedSettings.getInstance(project).isBranchProtected(it.nameForRemoteOperations) }
if (protectedBranches.isEmpty()) {
// This filter can only be applied when there is a pushed protected branch.
return Status.NoProtectedBranches
}
val headNodeId = getHeadCommitId() ?: return Status.GitDataNotFound
val protectedNodeIds = protectedBranches.associateBy { branch ->
val hash = repository.branches.getHash(branch) ?: return Status.GitDataNotFound
val nodeId = getCommitNodeId(hash) ?: return Status.GitDataNotFound
nodeId
}
val linearGraph = permanentGraph?.linearGraph ?: return Status.GitDataNotFound
return when (val status = findBaseCommit(linearGraph, headNodeId, protectedNodeIds.keys)) {
is Status.InternalSuccess -> {
val commits = status.commits.map { (commitId, protectedBranchId) ->
val hash = getHash(commitId) ?: return Status.GitDataNotFound
val branch = protectedNodeIds[protectedBranchId] ?: return Status.GitDataNotFound
BaseCommitAndBranch(hash, branch)
}
Status.Success(commits)
}
else -> status
}
}
private fun getHeadCommitId(): Int? {
val headRevision = repository.currentRevision ?: return null
val headHash = try {
HashImpl.build(headRevision)
} catch (e: Throwable) {
if (e is ControlFlowException) {
throw e
}
thisLogger().warn(e)
return null
}
return getCommitNodeId(headHash)
}
private fun getCommitNodeId(hash: Hash): Int? {
val commitIndex = storage?.getCommitIndex(hash, repository.root) ?: return null
val nodeId = permanentGraph?.permanentCommitsInfo?.getNodeId(commitIndex) ?: return null
return nodeId.takeIf { it >= 0 }
}
private fun getHash(nodeId: Int): Hash? {
val commitId = permanentGraph?.permanentCommitsInfo?.getCommitId(nodeId) ?: return null
val commit = storage?.getCommitId(commitId)
return commit?.hash
}
internal sealed interface Status {
data class Success(val commits: List<BaseCommitAndBranch>) : Status
data class InternalSuccess(val commits: List<BaseCommit>) : Status
data object NoProtectedBranches : Status
data object HeadInProtectedBranch : Status
data object GitDataNotFound : Status
data object CommitHasNoProtectedParents : Status
}
internal data class BaseCommit(val commitId: Int, val protectedNodeId: Int)
internal data class BaseCommitAndBranch(val hash: Hash, val protectedBranch: GitRemoteBranch)
}
@VisibleForTesting
internal fun findBaseCommit(linearGraph: LinearGraph, headNodeId: Int, protectedNodeIds: Set<Int>): Status {
val graph = LinearGraphUtils.asLiteLinearGraph(linearGraph)
val visited = BitSetFlags(graph.nodesCount())
if (findProtectedBranchNodeId(headNodeId, graph, visited, protectedNodeIds) != null) {
// The current commit is already a part of a protected branch.
// No feature branch filtering could be applied.
return Status.HeadInProtectedBranch
}
else {
visited.set(headNodeId, false)
}
val bfsWalk = BfsWalk(headNodeId, graph, visited)
val foundCommits = mutableListOf<CurrentFeatureBranchBaseDetector.BaseCommit>()
while (true) {
val nextLayer = bfsWalk.step()
if (nextLayer.isEmpty()) break
val protectedCommits = hashSetOf<Int>()
for (commit in nextLayer) {
val protectedNodeId = findProtectedBranchNodeId(commit, graph, visited, protectedNodeIds) ?: continue
foundCommits += CurrentFeatureBranchBaseDetector.BaseCommit(commit, protectedNodeId)
protectedCommits.add(commit)
}
// reset visited marks to continue searching down
for (nodeId in nextLayer) {
if (nodeId in protectedCommits) continue
visited.set(nodeId, false)
}
}
if (foundCommits.isEmpty()) return Status.CommitHasNoProtectedParents
return Status.InternalSuccess(foundCommits)
}
private fun findProtectedBranchNodeId(commitId: Int, linearGraph: LiteLinearGraph, visited: BitSetFlags, protectedNodeIds: Set<Int>): Int? {
var protectedNodeId: Int? = null
DfsWalk(listOf(commitId), linearGraph, visited).walk(goDown = false) { nodeId ->
if (nodeId in protectedNodeIds) {
protectedNodeId = nodeId
false
}
else true
}
return protectedNodeId
}

View File

@@ -0,0 +1,72 @@
// 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.git.coverage
import com.intellij.coverage.filters.ModifiedFilesFilter
import com.intellij.coverage.filters.ModifiedFilesFilterFactory
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.registry.Registry
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.intellij.vcs.log.Hash
import git4idea.GitRemoteBranch
import git4idea.GitUtil
import git4idea.changes.GitChangeUtils
import git4idea.repo.GitRepository
internal class GitModifiedFilesFilterFactory : ModifiedFilesFilterFactory {
override fun createFilter(project: Project): ModifiedFilesFilter? {
if (!Registry.`is`("coverage.filter.based.on.feature.branch")) return null
return createGitFilter(project)
}
}
@RequiresBackgroundThread
private fun createGitFilter(project: Project): ModifiedFilesFilter? {
val repositories = GitUtil.getRepositories(project)
val filters = repositories.mapNotNull { createFilterForRepository(it) }
if (filters.isEmpty()) return null
return filters.singleOrNull() ?: MultiRepoGitModifiedFilesFilter(project, filters)
}
private fun createFilterForRepository(repository: GitRepository): GitModifiedFilesFilter? {
val candidates = findBaseCommitCandidates(repository) ?: return null
return candidates.mapNotNull { (hash, branch) ->
val scope = createModifiedScope(repository, hash) ?: return@mapNotNull null
GitModifiedFilesFilter(repository.project, scope, branch)
}.minByOrNull { it.modifiedScope.size }
}
private fun findBaseCommitCandidates(repository: GitRepository): List<CurrentFeatureBranchBaseDetector.BaseCommitAndBranch>? {
return when (val baseCommit = CurrentFeatureBranchBaseDetector(repository).findBaseCommit()) {
is CurrentFeatureBranchBaseDetector.Status.Success -> baseCommit.commits
else -> null
}
}
private fun createModifiedScope(repository: GitRepository, baseRevision: Hash): Set<VirtualFile>? {
val currentRevision = repository.currentRevision ?: return null
val diff = GitChangeUtils.getDiff(repository, baseRevision.asString(), currentRevision, false) ?: return null
val scope = hashSetOf<VirtualFile>()
for (change in diff) {
val virtualFile = change.afterRevision?.file?.virtualFile ?: continue
scope.add(virtualFile)
}
return scope
}
private class GitModifiedFilesFilter(
project: Project,
val modifiedScope: Set<VirtualFile>,
private val branch: GitRemoteBranch,
) : ModifiedFilesFilter(project) {
override fun isInModifiedScope(file: VirtualFile) = file in modifiedScope
override fun getBranchName(): String = branch.nameForLocalOperations
}
private class MultiRepoGitModifiedFilesFilter(
project: Project,
private val filters: List<GitModifiedFilesFilter>,
) : ModifiedFilesFilter(project) {
override fun isInModifiedScope(file: VirtualFile) = filters.any { it.isFileModified(file) }
override fun getBranchName() = "Protected"
}

View File

@@ -0,0 +1,136 @@
// 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.git.coverage
import com.intellij.vcs.log.graph.api.LinearGraph
import com.intellij.vcs.log.graph.graph
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
internal class CurrentFeatureBranchBaseDetectorTest {
@Test
fun `test linear history`() {
// 0 <- HEAD
// 1
// 2 <- master
// 3
val graph = graph {
0(1)
1(2)
2(3)
3()
}
assertCommitFound(graph, 2, 2, setOf(2))
}
@Test
fun `test master ahead`() {
// 0 <- HEAD
// 1 2 <- master
// | 3
// 4 /
val graph = graph {
0(1)
1(4)
2(3)
3(4)
4()
}
assertCommitFound(graph, 4, 2, setOf(2))
}
@Test
fun `test merge with master`() {
// 0 <- HEAD
// 1 (merge)
// | \
// 2 3 <- master
// 4 /
val graph = graph {
0(1)
1(2, 3)
2(4)
3(4)
4()
}
assertCommitFound(graph, 3, 3, setOf(3))
}
@Test
fun `test master is ahead after merge`() {
// 0 <- HEAD
// 1 (merge) 3 <- master
// | \ /
// 2 4
// 5 /
val graph = graph {
0(1)
1(2, 4)
2(5)
3(4)
4(5)
5()
}
assertCommitFound(graph, 4, 3, setOf(3))
}
@Test
fun `test merge with other protected branch`() {
// Here
// 0 <- HEAD
// 1 (merge)
// | \
// 2 4 <- protected
// 3
// 5 <- master
val graph = graph {
0(1)
1(2, 4)
2(3)
3(5)
4()
5()
}
val expected = listOf(CurrentFeatureBranchBaseDetector.BaseCommit(4, 4), CurrentFeatureBranchBaseDetector.BaseCommit(5, 5))
assertCommitFound(graph, expected, setOf(4, 5))
}
@Test
fun `test commit in protected branch`() {
// 0 <- master
// 1
// 2 <- HEAD
// 3
val graph = graph {
0(1)
1(2)
2(3)
3()
}
assertEquals(CurrentFeatureBranchBaseDetector.Status.HeadInProtectedBranch, findBaseCommit(graph, 2, setOf(0)))
}
@Test
fun `test protected branch in inaccessable`() {
// 0 <- HEAD
// 1
// 2
// 3
val graph = graph {
0(1)
1(2)
2(3)
3()
}
assertEquals(CurrentFeatureBranchBaseDetector.Status.CommitHasNoProtectedParents, findBaseCommit(graph, 0, emptySet()))
}
private fun assertCommitFound(graph: LinearGraph, expectedCommitId: Int, expectedProtectedId: Int, protectedNodeIds: Set<Int>) {
assertCommitFound(graph, listOf(CurrentFeatureBranchBaseDetector.BaseCommit(expectedCommitId, expectedProtectedId)), protectedNodeIds)
}
private fun assertCommitFound(graph: LinearGraph, commits: List<CurrentFeatureBranchBaseDetector.BaseCommit>, protectedNodeIds: Set<Int>) {
val baseCommit = findBaseCommit(graph, 0, protectedNodeIds)
assertEquals(CurrentFeatureBranchBaseDetector.Status.InternalSuccess(commits), baseCommit)
}
}

View File

@@ -26,6 +26,7 @@
<content>
<module name="intellij.vcs.git/newUiOnboarding"/>
<module name="intellij.vcs.git/terminal"/>
<module name="intellij.vcs.git.coverage"/>
</content>
<actions>