From c0e9691d2b3f566abbdfb8dddfd5d23d1afe6695 Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Wed, 22 May 2024 12:30:15 +0200 Subject: [PATCH] [coverage] IDEA-353814 Files filtering based on git commit history IJ-CR-135534 GitOrigin-RevId: 93c6aa5c27f75224a2819ea8097c1a749d5434d4 --- .idea/modules.xml | 1 + .../resources/intellij.platform.coverage.xml | 1 + .../messages/CoverageBundle.properties | 1 + .../coverage/BaseCoverageAnnotator.java | 7 +- .../coverage/filters/ModifiedFilesFilter.kt | 13 +- .../filters/ModifiedFilesFilterFactory.kt | 16 ++ .../intellij/coverage/view/CoverageView.java | 30 +++- .../intellij.vcs.git.coverage.iml | 23 +++ .../resources/intellij.vcs.git.coverage.xml | 11 ++ .../CurrentFeatureBranchBaseDetector.kt | 146 ++++++++++++++++++ .../coverage/GitModifiedFilesFilterFactory.kt | 72 +++++++++ .../CurrentFeatureBranchBaseDetectorTest.kt | 136 ++++++++++++++++ .../git4idea/resources/META-INF/plugin.xml | 1 + 13 files changed, 446 insertions(+), 12 deletions(-) create mode 100644 plugins/coverage-common/src/com/intellij/coverage/filters/ModifiedFilesFilterFactory.kt create mode 100644 plugins/git4idea/intellij.vcs.git.coverage/intellij.vcs.git.coverage.iml create mode 100644 plugins/git4idea/intellij.vcs.git.coverage/resources/intellij.vcs.git.coverage.xml create mode 100644 plugins/git4idea/intellij.vcs.git.coverage/src/com/intellij/vcs/git/coverage/CurrentFeatureBranchBaseDetector.kt create mode 100644 plugins/git4idea/intellij.vcs.git.coverage/src/com/intellij/vcs/git/coverage/GitModifiedFilesFilterFactory.kt create mode 100644 plugins/git4idea/intellij.vcs.git.coverage/testSrc/com/intellij/vcs/git/coverage/CurrentFeatureBranchBaseDetectorTest.kt diff --git a/.idea/modules.xml b/.idea/modules.xml index 06c5870dadf1..b5bc5ff3740e 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -957,6 +957,7 @@ + diff --git a/plugins/coverage-common/resources/intellij.platform.coverage.xml b/plugins/coverage-common/resources/intellij.platform.coverage.xml index d8df14ae58e5..2757c9a7180f 100644 --- a/plugins/coverage-common/resources/intellij.platform.coverage.xml +++ b/plugins/coverage-common/resources/intellij.platform.coverage.xml @@ -9,6 +9,7 @@ + diff --git a/plugins/coverage-common/resources/messages/CoverageBundle.properties b/plugins/coverage-common/resources/messages/CoverageBundle.properties index 2367a0d7fc44..77355d0f0297 100644 --- a/plugins/coverage-common/resources/messages/CoverageBundle.properties +++ b/plugins/coverage-common/resources/messages/CoverageBundle.properties @@ -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 diff --git a/plugins/coverage-common/src/com/intellij/coverage/BaseCoverageAnnotator.java b/plugins/coverage-common/src/com/intellij/coverage/BaseCoverageAnnotator.java index 8839c174e79e..1b69c4df24d8 100644 --- a/plugins/coverage-common/src/com/intellij/coverage/BaseCoverageAnnotator.java +++ b/plugins/coverage-common/src/com/intellij/coverage/BaseCoverageAnnotator.java @@ -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; } diff --git a/plugins/coverage-common/src/com/intellij/coverage/filters/ModifiedFilesFilter.kt b/plugins/coverage-common/src/com/intellij/coverage/filters/ModifiedFilesFilter.kt index 1a3654fd491b..91de6f6e64bf 100644 --- a/plugins/coverage-common/src/com/intellij/coverage/filters/ModifiedFilesFilter.kt +++ b/plugins/coverage-common/src/com/intellij/coverage/filters/ModifiedFilesFilter.kt @@ -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) } } diff --git a/plugins/coverage-common/src/com/intellij/coverage/filters/ModifiedFilesFilterFactory.kt b/plugins/coverage-common/src/com/intellij/coverage/filters/ModifiedFilesFilterFactory.kt new file mode 100644 index 000000000000..e8c6da980b4c --- /dev/null +++ b/plugins/coverage-common/src/com/intellij/coverage/filters/ModifiedFilesFilterFactory.kt @@ -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 = ExtensionPointName.create("com.intellij.coverageModifiedFilesFilterFactory") + } +} diff --git a/plugins/coverage-common/src/com/intellij/coverage/view/CoverageView.java b/plugins/coverage-common/src/com/intellij/coverage/view/CoverageView.java index 27804afc6acc..f37a91819b43 100644 --- a/plugins/coverage-common/src/com/intellij/coverage/view/CoverageView.java +++ b/plugins/coverage-common/src/com/intellij/coverage/view/CoverageView.java @@ -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); diff --git a/plugins/git4idea/intellij.vcs.git.coverage/intellij.vcs.git.coverage.iml b/plugins/git4idea/intellij.vcs.git.coverage/intellij.vcs.git.coverage.iml new file mode 100644 index 000000000000..ad8fd7316a81 --- /dev/null +++ b/plugins/git4idea/intellij.vcs.git.coverage/intellij.vcs.git.coverage.iml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/git4idea/intellij.vcs.git.coverage/resources/intellij.vcs.git.coverage.xml b/plugins/git4idea/intellij.vcs.git.coverage/resources/intellij.vcs.git.coverage.xml new file mode 100644 index 000000000000..677639601c4f --- /dev/null +++ b/plugins/git4idea/intellij.vcs.git.coverage/resources/intellij.vcs.git.coverage.xml @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/plugins/git4idea/intellij.vcs.git.coverage/src/com/intellij/vcs/git/coverage/CurrentFeatureBranchBaseDetector.kt b/plugins/git4idea/intellij.vcs.git.coverage/src/com/intellij/vcs/git/coverage/CurrentFeatureBranchBaseDetector.kt new file mode 100644 index 000000000000..8449f4fe8abe --- /dev/null +++ b/plugins/git4idea/intellij.vcs.git.coverage/src/com/intellij/vcs/git/coverage/CurrentFeatureBranchBaseDetector.kt @@ -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 + + 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) : Status + data class InternalSuccess(val commits: List) : 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): 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() + while (true) { + val nextLayer = bfsWalk.step() + if (nextLayer.isEmpty()) break + val protectedCommits = hashSetOf() + 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? { + var protectedNodeId: Int? = null + DfsWalk(listOf(commitId), linearGraph, visited).walk(goDown = false) { nodeId -> + if (nodeId in protectedNodeIds) { + protectedNodeId = nodeId + false + } + else true + } + return protectedNodeId +} + diff --git a/plugins/git4idea/intellij.vcs.git.coverage/src/com/intellij/vcs/git/coverage/GitModifiedFilesFilterFactory.kt b/plugins/git4idea/intellij.vcs.git.coverage/src/com/intellij/vcs/git/coverage/GitModifiedFilesFilterFactory.kt new file mode 100644 index 000000000000..fd49f0e8ea4e --- /dev/null +++ b/plugins/git4idea/intellij.vcs.git.coverage/src/com/intellij/vcs/git/coverage/GitModifiedFilesFilterFactory.kt @@ -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? { + return when (val baseCommit = CurrentFeatureBranchBaseDetector(repository).findBaseCommit()) { + is CurrentFeatureBranchBaseDetector.Status.Success -> baseCommit.commits + else -> null + } +} + +private fun createModifiedScope(repository: GitRepository, baseRevision: Hash): Set? { + val currentRevision = repository.currentRevision ?: return null + val diff = GitChangeUtils.getDiff(repository, baseRevision.asString(), currentRevision, false) ?: return null + val scope = hashSetOf() + for (change in diff) { + val virtualFile = change.afterRevision?.file?.virtualFile ?: continue + scope.add(virtualFile) + } + return scope +} + +private class GitModifiedFilesFilter( + project: Project, + val modifiedScope: Set, + 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, +) : ModifiedFilesFilter(project) { + override fun isInModifiedScope(file: VirtualFile) = filters.any { it.isFileModified(file) } + override fun getBranchName() = "Protected" +} diff --git a/plugins/git4idea/intellij.vcs.git.coverage/testSrc/com/intellij/vcs/git/coverage/CurrentFeatureBranchBaseDetectorTest.kt b/plugins/git4idea/intellij.vcs.git.coverage/testSrc/com/intellij/vcs/git/coverage/CurrentFeatureBranchBaseDetectorTest.kt new file mode 100644 index 000000000000..0422974ce711 --- /dev/null +++ b/plugins/git4idea/intellij.vcs.git.coverage/testSrc/com/intellij/vcs/git/coverage/CurrentFeatureBranchBaseDetectorTest.kt @@ -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) { + assertCommitFound(graph, listOf(CurrentFeatureBranchBaseDetector.BaseCommit(expectedCommitId, expectedProtectedId)), protectedNodeIds) + } + + private fun assertCommitFound(graph: LinearGraph, commits: List, protectedNodeIds: Set) { + val baseCommit = findBaseCommit(graph, 0, protectedNodeIds) + assertEquals(CurrentFeatureBranchBaseDetector.Status.InternalSuccess(commits), baseCommit) + } +} diff --git a/plugins/git4idea/resources/META-INF/plugin.xml b/plugins/git4idea/resources/META-INF/plugin.xml index df9d29b17481..48734ee12035 100644 --- a/plugins/git4idea/resources/META-INF/plugin.xml +++ b/plugins/git4idea/resources/META-INF/plugin.xml @@ -26,6 +26,7 @@ +