[vcs/mappings] IJPL-181017 Deduplicate symlink roots

Skip root if it is located in a symbolic link hierarchy and the target path is already registered

(cherry picked from commit d0b280bfb2014dbaaf66ead771e0b8b157097e05)


(cherry picked from commit 96aaec2e174f62b5c2d89ed23836190ee05bd424)

IJ-MR-163036

GitOrigin-RevId: fa9a68a264cc33a0dbb485e41dec81723e64c3ef
This commit is contained in:
Ilia.Shulgin
2025-05-08 16:01:16 +02:00
committed by intellij-monorepo-bot
parent d7a23e3832
commit 8b47332de2
2 changed files with 131 additions and 3 deletions

View File

@@ -77,7 +77,7 @@ final class VcsRootDetectorImpl implements VcsRootDetector {
detectedRoots.addAll(scanForRootsInsideDir(myProject, dirToScan, null, scannedDirs));
detectedRoots.addAll(scanForRootsAboveDirs(Collections.singletonList(dirToScan), scannedDirs, detectedRoots));
return detectedRoots;
return deduplicate(detectedRoots);
}
private @NotNull Collection<VcsRoot> scanForRootsInContentRoots() {
@@ -106,7 +106,43 @@ final class VcsRootDetectorImpl implements VcsRootDetector {
detectedAndKnownRoots.addAll(Arrays.asList(ProjectLevelVcsManager.getInstance(myProject).getAllVcsRoots()));
detectedRoots.addAll(scanDependentRoots(scannedDirs, detectedAndKnownRoots));
return detectedRoots;
return deduplicate(detectedRoots);
}
/**
* @return a deduplicated set of {@code detectedRoots} with removed links pointing to the same canonical file.
*/
private static @NotNull Set<VcsRoot> deduplicate(@NotNull Set<VcsRoot> detectedRoots) {
if (detectedRoots.size() <= 1) return detectedRoots;
Set<VcsRoot> result = new HashSet<>();
Set<VirtualFile> processedCanonicalRoots = new HashSet<>();
Set<VcsRoot> rootsUnderSymlink = new HashSet<>();
for (VcsRoot root : detectedRoots) {
VirtualFile path = root.getPath();
VirtualFile canonicalPath = path.getCanonicalFile();
if (canonicalPath != null && !path.equals(canonicalPath)) {
rootsUnderSymlink.add(root);
} else {
processedCanonicalRoots.add(path);
result.add(root);
}
}
if (rootsUnderSymlink.isEmpty()) return detectedRoots;
for (VcsRoot root : ContainerUtil.sorted(rootsUnderSymlink, Comparator.comparing(root -> root.getPath().toNioPath()))) {
VirtualFile canonicalFile = root.getPath().getCanonicalFile();
if (processedCanonicalRoots.contains(canonicalFile)) {
LOG.debug("Skipping duplicate VCS root %s: root for canonical file '%s' is already detected".formatted(root, canonicalFile));
} else {
processedCanonicalRoots.add(canonicalFile);
result.add(root);
}
}
return result;
}
private @NotNull Set<VcsRoot> scanForRootsInsideDir(@NotNull Project project,

View File

@@ -1,4 +1,4 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.openapi.vcs.roots
import com.intellij.openapi.components.service
@@ -8,8 +8,11 @@ import com.intellij.openapi.vcs.VcsTestUtil.assertEqualCollections
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.testFramework.PsiTestUtil
import com.intellij.testFramework.VfsTestUtil
import com.intellij.testFramework.utils.io.createDirectory
import com.intellij.testFramework.utils.io.createFile
import org.jetbrains.jps.model.serialization.PathMacroUtil
import java.util.Collections.emptyList
import kotlin.io.path.createSymbolicLinkPointingTo
class VcsRootDetectorTest : VcsRootBaseTest() {
fun `test no roots`() {
@@ -164,6 +167,95 @@ class VcsRootDetectorTest : VcsRootBaseTest() {
expect(projectRoot)
}
// IJPL-172487
fun `test root referenced via child symlink should not be duplicated`() {
initRepository(projectRoot)
// Created layout:
// project/
// -child/
// --test
// --root-symlink -> project
val targetDir = projectNioRoot.createDirectory("child")
targetDir.createFile("test")
targetDir.resolve("root-symlink").createSymbolicLinkPointingTo(projectNioRoot)
VfsTestUtil.syncRefresh()
PsiTestUtil.addContentRoot(rootModule, projectRoot)
expect(projectRoot)
}
// IJPL-181017
fun `test root has symlink parent`() {
val symlink = projectNioRoot.resolve("symlink")
val projectSibling = testNioRoot.createDirectory("project-sibling")
symlink.createSymbolicLinkPointingTo(projectSibling)
VfsTestUtil.syncRefresh()
val vcsRoot = createVcsRoots("symlink/repo-root")
// Expected `project/symlink/repo-root` which is not a canonical path `project-sibling/repo-root`
expect(vcsRoot)
}
fun `test multiple roots under symlink`() {
val symlink = projectNioRoot.resolve("symlink")
val projectSibling = testNioRoot.createDirectory("project-sibling")
symlink.createSymbolicLinkPointingTo(projectSibling)
VfsTestUtil.syncRefresh()
// Created layout:
// project/
// -symlink -> project-sibling
// project-sibling/
// -root1
// -root2
// -root3
val roots = createVcsRoots("symlink/root1", "symlink/root2", "symlink/root3")
expect(roots)
}
fun `test multiple roots with symlink target outside project`() {
initRepository(projectRoot)
val symlink = projectNioRoot.resolve("symlink")
val projectSibling = testNioRoot.createDirectory("project-sibling")
symlink.createSymbolicLinkPointingTo(projectSibling)
VfsTestUtil.syncRefresh()
val vcsRoot = createVcsRoots("symlink/repo-root")
expect(vcsRoot + projectRoot)
}
// Combined scenario for IJPL-172487 and IJPL-181017
fun `test symlinks under symlinks`() {
val symlink = projectNioRoot.resolve("symlink")
val projectSibling = testNioRoot.createDirectory("project-sibling")
symlink.createSymbolicLinkPointingTo(projectSibling)
VfsTestUtil.syncRefresh()
val vcsRoot = createVcsRoots("symlink/repo-root")
// Created layout:
// project/
// -symlink -> project-sibling/
// project-sibling/
// -repo-root
// --test
// --sublink -> repo-root
val repoRoot = symlink.resolve("repo-root")
repoRoot.createFile("test")
val linkToRepo = repoRoot.resolve("sublink")
linkToRepo.createSymbolicLinkPointingTo(repoRoot)
VfsTestUtil.syncRefresh()
// Expected only `project/symlink/repo-root`
expect(vcsRoot)
}
private fun createVcsRoots(vararg relativePaths: String, registerContentRoot: Boolean = true): List<VirtualFile> {
return createVcsRoots(listOf(*relativePaths), registerContentRoot)
}