From c34e0f1f35bc45587d1cdb8f497ef0928b1ef07f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kasprzyk?= Date: Fri, 6 Feb 2026 14:43:13 +0000 Subject: [PATCH] BAZEL-2845 [lvcs]: Fix algorithm deciding if the file is versioned MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before the change, IdeaGateway#isVersioned incorrectly returned true when a file was marked as ignored, and the user opened two or more projects. Merge-request: IJ-MR-190455 Merged-by: MichaƂ Kasprzyk [External] GitOrigin-RevId: effa2e900d6cb5f5849b7f6c2a40afe75bf84953 --- platform/lvcs-impl/BUILD.bazel | 1 + .../lvcs-impl/intellij.platform.lvcs.impl.iml | 1 + .../history/integration/IdeaGateway.java | 47 ++++--- .../LocalHistoryEventDispatcher.kt | 3 +- .../history/integration/IdeaGatewayTest.java | 128 ++++++++++++++++++ 5 files changed, 163 insertions(+), 17 deletions(-) diff --git a/platform/lvcs-impl/BUILD.bazel b/platform/lvcs-impl/BUILD.bazel index bc9fc16d7c0b..02b04c88296e 100644 --- a/platform/lvcs-impl/BUILD.bazel +++ b/platform/lvcs-impl/BUILD.bazel @@ -85,6 +85,7 @@ jvm_library( "//platform/vcs-impl/shared:shared_test_lib", "@lib//:jetbrains-annotations", "//platform/projectModel-impl", + "//platform/lang-core", ] ) ### auto-generated section `build intellij.platform.lvcs.impl` end diff --git a/platform/lvcs-impl/intellij.platform.lvcs.impl.iml b/platform/lvcs-impl/intellij.platform.lvcs.impl.iml index 95138f59d431..19891e2701a4 100644 --- a/platform/lvcs-impl/intellij.platform.lvcs.impl.iml +++ b/platform/lvcs-impl/intellij.platform.lvcs.impl.iml @@ -39,6 +39,7 @@ + diff --git a/platform/lvcs-impl/src/com/intellij/history/integration/IdeaGateway.java b/platform/lvcs-impl/src/com/intellij/history/integration/IdeaGateway.java index cfc494e45301..8d92921f3c98 100644 --- a/platform/lvcs-impl/src/com/intellij/history/integration/IdeaGateway.java +++ b/platform/lvcs-impl/src/com/intellij/history/integration/IdeaGateway.java @@ -66,7 +66,7 @@ public class IdeaGateway { return isVersioned(f, false); } - public boolean isVersioned(@NotNull VirtualFile f, boolean shouldBeInContent) { + public boolean isVersioned(@NotNull VirtualFile f, boolean shouldBeUnderAnyContentRoot) { if (VersionManagingFileSystem.isDisabled(f)) { return false; } @@ -84,31 +84,48 @@ public class IdeaGateway { VersionedFilterData versionedFilterData = getVersionedFilterData(); - int numberOfOpenProjects = versionedFilterData.myOpenedProjects.size(); + int numberOfOpenProjects = versionedFilterData.myProjectFileIndices.size(); // optimisation: FileTypeManager.isFileIgnored(f) will be checked inside ProjectFileIndex.isUnderIgnored() if (numberOfOpenProjects == 0) { - if (shouldBeInContent) return false; // there is no project, so the file can't be in content + if (shouldBeUnderAnyContentRoot) return false; // there is no project, so the file can't be in content if (FileTypeManager.getInstance().isFileIgnored(f)) return false; return true; } - boolean isExcludedFromAll = true; - boolean isInContent = false; - + boolean underAnyProjectContentRoot = false; for (int i = 0; i < numberOfOpenProjects; ++i) { - ProjectFileIndex index = versionedFilterData.myProjectFileIndices.get(i); - - if (index.isUnderIgnored(f)) return false; - isInContent |= index.isInContent(f); - isExcludedFromAll &= index.isExcluded(f); + ProjectFileIndex projectFileIndex = versionedFilterData.myProjectFileIndices.get(i); + boolean isProjectRelated = isProjectRelated(projectFileIndex, f); + // isIsContent returns true when the file or directory is under the content root of the project, + // AND is not excluded or ignored + if (isProjectRelated && projectFileIndex.isInContent(f)) { + // File is under the content root and isn't ignored/excluded. Track it in LVCS. + return true; + } + underAnyProjectContentRoot |= isProjectRelated; } - if (isExcludedFromAll) return false; - if (shouldBeInContent && !isInContent) return false; + if (underAnyProjectContentRoot) { + // File is under the content root of at least one project, but it is ignored/excluded by all these projects. + // Do not track it in LVCS. + return false; + } + else if (shouldBeUnderAnyContentRoot) { + // At this moment the file isn't under any content root, and callsite asks us only for files that + // are under any content root. Do not track it in LVCS. + return false; + } + else { + // File isn't under the content root of any project, call-site doesn't care if it is. + // Check only if the file matches any ignored pattern. + return !FileTypeManager.getInstance().isFileIgnored(f); + } + } - return true; + static boolean isProjectRelated(ProjectFileIndex projectFileIndex, VirtualFile file) { + return projectFileIndex.isInProjectOrExcluded(file) || projectFileIndex.isUnderIgnored(file); } public @NotNull String getPathOrUrl(@NotNull VirtualFile file) { @@ -172,7 +189,6 @@ public class IdeaGateway { } protected static final class VersionedFilterData { - final List myOpenedProjects = new ArrayList<>(); final List myProjectFileIndices = new ArrayList<>(); VersionedFilterData() { @@ -182,7 +198,6 @@ public class IdeaGateway { if (each.isDefault()) continue; if (!each.isInitialized()) continue; - myOpenedProjects.add(each); myProjectFileIndices.add(ProjectRootManager.getInstance(each).getFileIndex()); } } diff --git a/platform/lvcs-impl/src/com/intellij/history/integration/LocalHistoryEventDispatcher.kt b/platform/lvcs-impl/src/com/intellij/history/integration/LocalHistoryEventDispatcher.kt index a58846ebaf95..fec8fe14249d 100644 --- a/platform/lvcs-impl/src/com/intellij/history/integration/LocalHistoryEventDispatcher.kt +++ b/platform/lvcs-impl/src/com/intellij/history/integration/LocalHistoryEventDispatcher.kt @@ -3,6 +3,7 @@ package com.intellij.history.integration import com.intellij.history.ActivityId import com.intellij.history.core.LocalHistoryFacade +import com.intellij.history.integration.IdeaGateway.isProjectRelated import com.intellij.history.integration.LocalHistoryImpl.Companion.getInstanceImpl import com.intellij.openapi.Disposable import com.intellij.openapi.command.CommandEvent @@ -63,7 +64,7 @@ internal class LocalHistoryEventDispatcher(private val facade: LocalHistoryFacad val projectIndexes = IdeaGateway.getVersionedFilterData().myProjectFileIndices var containingProjectIndex: ProjectFileIndex? = null for (projectIndex in projectIndexes) { - val isProjectRelated = projectIndex.isInProjectOrExcluded(dir) || projectIndex.isUnderIgnored(dir) + val isProjectRelated = isProjectRelated(projectIndex, dir) if (!isProjectRelated) continue if (containingProjectIndex != null) return false // more than 1 project contains this dir diff --git a/platform/lvcs-impl/testSrc/com/intellij/history/integration/IdeaGatewayTest.java b/platform/lvcs-impl/testSrc/com/intellij/history/integration/IdeaGatewayTest.java index 72e4984c1bdb..01c7029939d0 100644 --- a/platform/lvcs-impl/testSrc/com/intellij/history/integration/IdeaGatewayTest.java +++ b/platform/lvcs-impl/testSrc/com/intellij/history/integration/IdeaGatewayTest.java @@ -3,10 +3,24 @@ package com.intellij.history.integration; import com.intellij.history.core.tree.Entry; import com.intellij.history.core.tree.RootEntry; +import com.intellij.ide.scratch.ScratchFileActions; +import com.intellij.ide.scratch.ScratchFileCreationHelper; import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.WriteAction; +import com.intellij.openapi.command.WriteCommandAction; +import com.intellij.openapi.fileTypes.FileTypeManager; +import com.intellij.openapi.module.ModifiableModuleModel; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.module.ModuleManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.project.ex.ProjectManagerEx; +import com.intellij.openapi.roots.ModuleRootModificationUtil; +import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.ThrowableComputable; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.testFramework.OpenProjectTaskBuilder; +import com.intellij.testFramework.PlatformTestUtil; import com.intellij.util.containers.ContainerUtil; import org.jetbrains.annotations.NotNull; @@ -18,6 +32,8 @@ import java.util.Collection; import java.util.Comparator; import java.util.List; +import static com.intellij.openapi.roots.ModuleRootModificationUtil.updateExcludedFolders; + public class IdeaGatewayTest extends IntegrationTestCase { public void testFindingFile() { assertEquals(myRoot, myGateway.findVirtualFile(myRoot.getPath())); @@ -148,6 +164,118 @@ public class IdeaGatewayTest extends IntegrationTestCase { getAllPaths(rootEntry)); } + public void testFileVersionedWhenInContentRoot() throws Exception { + // GIVEN + VirtualFile sourceFile = createFile("src/main/java/Main.java"); + VirtualFile sourceDirectory = sourceFile.getParent(); + + // WHEN - THEN + assertTrue(myGateway.isVersioned(sourceDirectory)); + assertTrue(myGateway.isVersioned(sourceFile)); + assertTrue(myGateway.isVersioned(sourceDirectory, true)); + assertTrue(myGateway.isVersioned(sourceFile, true)); + } + + public void testScratchFileVersionedOnlyWhenCallerDontCareAboutContentRoot() { + // GIVEN there is no module, so the project doesn't have content roots + ModifiableModuleModel model = ModuleManager.getInstance(getProject()).getModifiableModel(); + model.disposeModule(getModule()); + WriteCommandAction.runWriteCommandAction(getProject(), () -> model.commit()); + + // AND scratch file + ScratchFileCreationHelper.Context context = new ScratchFileCreationHelper.Context(); + VirtualFile scratchFile = ScratchFileActions.doCreateNewScratch(getProject(), context).getVirtualFile(); + + // WHEN - THEN + assertTrue(myGateway.isVersioned(scratchFile)); + assertFalse(myGateway.isVersioned(scratchFile, true)); + } + + public void testFileNotVersionedWhenHasSpecificName() throws Exception { + // GIVEN + List ignoredFiles = List.of( + createFile("workspace.xml"), + createFile("project.iws"), + createFile("some.class") + ); + + // WHEN - THEN + ignoredFiles.forEach(file -> { + assertFalse(myGateway.isVersioned(file)); + assertFalse(myGateway.isVersioned(file, true)); + }); + } + + public void testFileNotVersionedWhenInExcludedDirectory() throws Exception { + // GIVEN + VirtualFile fileInExcludedDirectory = createFile("out/example-file.txt"); + VirtualFile excludedDirectory = fileInExcludedDirectory.getParent(); + + updateExcludedFolders(myModule, myRoot, List.of(), List.of(excludedDirectory.getUrl())); + + // WHEN - THEN + assertFalse(myGateway.isVersioned(excludedDirectory)); + assertFalse(myGateway.isVersioned(fileInExcludedDirectory)); + assertFalse(myGateway.isVersioned(excludedDirectory, true)); + assertFalse(myGateway.isVersioned(fileInExcludedDirectory, true)); + } + + public void testFileNotVersionedWhenGloballyIgnored() throws Exception { + // GIVEN + VirtualFile ignoredFile = createFile("src/main/resources/ignored.file"); + VirtualFile resourcesDirectory = ignoredFile.getParent(); + + FileTypeManager fileTypeManager = FileTypeManager.getInstance(); + String originalIgnoredFileList = fileTypeManager.getIgnoredFilesList(); + try { + WriteAction.runAndWait(() -> fileTypeManager.setIgnoredFilesList(originalIgnoredFileList + ";ignored.file")); + + // WHEN - THEN + assertTrue(myGateway.isVersioned(resourcesDirectory)); + assertFalse(myGateway.isVersioned(ignoredFile)); + assertTrue(myGateway.isVersioned(resourcesDirectory, true)); + assertFalse(myGateway.isVersioned(ignoredFile, true)); + } finally { + WriteAction.runAndWait(() -> fileTypeManager.setIgnoredFilesList(originalIgnoredFileList)); + } + } + + public void testFileNotVersionedWhenIgnoredByOneOfTheProjects() throws Exception { + // GIVEN a second opened project + VirtualFile anotherProjectRootDir = getTempDir().createVirtualDir(); + Project anotherProject = ProjectManagerEx.getInstanceEx().openProject(anotherProjectRootDir.toNioPath(), new OpenProjectTaskBuilder().build()); + Disposer.register(getTestRootDisposable(), () -> PlatformTestUtil.forceCloseProjectWithoutSaving(anotherProject)); + + // AND a module in the second project + Module module = doCreateRealModuleIn(anotherProject.getName(), anotherProject, getModuleType()); + ModuleRootModificationUtil.addContentRoot(module, anotherProjectRootDir.getPath()); + + // AND an ignored directory + VirtualFile ignoredDirectory = WriteAction.compute(() -> anotherProjectRootDir.createChildDirectory(this, "out")); + updateExcludedFolders(module, anotherProjectRootDir, List.of(), List.of(ignoredDirectory.getUrl())); + + // AND a file inside the ignored directory + VirtualFile fileInIgnoredDirectory = WriteAction.compute(() -> ignoredDirectory.createChildData(this, "fileInAnotherProject.txt")); + + // WHEN - THEN + assertSize(2, ProjectManagerEx.getInstanceEx().getOpenProjects()); + assertFalse(myGateway.isVersioned(fileInIgnoredDirectory)); + assertFalse(myGateway.isVersioned(fileInIgnoredDirectory, true)); + } + + public void testFileVersionedInOptimizedPath() throws Exception { + // GIVEN there are no opened projects + PlatformTestUtil.forceCloseProjectWithoutSaving(myProject); + + // AND a file outside the content root (because the project isn't opened) + VirtualFile fileOutsideContentRoot = createFile("file/outside/content/root/Main.java"); + + // WHEN - THEN + assertEmpty(ProjectManagerEx.getInstanceEx().getOpenProjects()); + assertTrue(myGateway.isVersioned(fileOutsideContentRoot)); + assertFalse(myGateway.isVersioned(fileOutsideContentRoot, true)); + } + public static @NotNull String getAllPaths(@NotNull RootEntry rootEntry) { List result = new ArrayList<>(); printAllPaths(rootEntry, result);