fix(server): keep the Git-ignore ancestor walk for symlinked entries

A symlink resolves to a path whose ancestors were never checked during
traversal, so a link into an ignored directory was discovered. Named
children keep the single-entry check.
This commit is contained in:
paseo-bot[bot]
2026-09-23 21:06:43 +02:00
parent 434cc1ef80
commit dfb5a478c7
2 changed files with 29 additions and 4 deletions
@@ -170,6 +170,26 @@ describe("searchDirectoryEntries", () => {
);
});
it.skipIf(isWindows)(
"prunes a symlink whose target is inside a Git-ignored directory",
async () => {
initGitRepo(searchRoot, "generated/\n");
mkdirSync(path.join(searchRoot, "generated", "output"), { recursive: true });
symlinkSync(path.join(searchRoot, "generated", "output"), path.join(searchRoot, "linked"));
await expect(
searchDirectoryEntries({
root: searchRoot,
query: "linked",
pathFormat: "relative",
includeFiles: false,
includeDirectories: true,
respectGitIgnore: true,
}),
).resolves.toEqual([]);
},
);
it("configures raw blank queries independently from explicit root aliases", async () => {
const rootEntries = [
{ path: "projects", kind: "directory" as const },
@@ -337,10 +337,7 @@ function staysInsideRoot(entry: ChildEntry, root: string): boolean {
}
function shouldDiscover(entry: ChildEntry, input: SearchInput): boolean {
// Traversal only descends through entries that already passed this check, so every ancestor
// is known discoverable and only the entry itself can be newly ignored. Walking the ancestor
// chain here would re-derive that answer once per level, for every entry scanned.
if (input.gitIgnoredPaths.has(entry.resolvedPath)) return false;
if (isNewlyGitIgnored(entry, input)) return false;
if (entry.kind === "file") {
return input.includeFiles && !entry.name.startsWith(".");
}
@@ -349,6 +346,14 @@ function shouldDiscover(entry: ChildEntry, input: SearchInput): boolean {
return input.hiddenDirectoryNames.has(entry.name);
}
// Traversal only descends through entries that already passed this check, so every ancestor of
// a named child is known discoverable and only the entry itself can be newly ignored. A symlink
// resolves to a path with different ancestors, so it still needs the full walk.
function isNewlyGitIgnored(entry: ChildEntry, input: SearchInput): boolean {
if (entry.viaSymlink) return isGitIgnoredPath(entry.resolvedPath, input);
return input.gitIgnoredPaths.has(entry.resolvedPath);
}
function isGitIgnoredPath(absolutePath: string, input: SearchInput): boolean {
let candidate = absolutePath;
while (candidate !== input.root && isPathInsideRoot(input.root, candidate)) {