mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-03-22 15:19:59 +07:00
[coverage] IDEA-353814 Files filtering based on git commit history
IJ-CR-135534 GitOrigin-RevId: 93c6aa5c27f75224a2819ea8097c1a749d5434d4
This commit is contained in:
committed by
intellij-monorepo-bot
parent
7cfb5a7419
commit
c0e9691d2b
1
.idea/modules.xml
generated
1
.idea/modules.xml
generated
@@ -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" />
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user