BAZEL-2845 [lvcs]: Fix algorithm deciding if the file is versioned

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] <michal.kasprzyk.external@jetbrains.com>

GitOrigin-RevId: effa2e900d6cb5f5849b7f6c2a40afe75bf84953
This commit is contained in:
Michał Kasprzyk
2026-02-06 14:43:13 +00:00
committed by intellij-monorepo-bot
parent fcd775c683
commit c34e0f1f35
5 changed files with 163 additions and 17 deletions
+1
View File
@@ -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
@@ -39,6 +39,7 @@
<orderEntry type="module" module-name="intellij.platform.vcs.impl.shared" />
<orderEntry type="library" name="jetbrains-annotations" level="project" />
<orderEntry type="module" module-name="intellij.platform.projectModel.impl" />
<orderEntry type="module" module-name="intellij.platform.lang.core" scope="TEST" />
</component>
<component name="copyright">
<Base>
@@ -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<Project> myOpenedProjects = new ArrayList<>();
final List<ProjectFileIndex> 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());
}
}
@@ -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
@@ -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<VirtualFile> 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<String> result = new ArrayList<>();
printAllPaths(rootEntry, result);