From 5a0dcfb2e5be82a88f5223fad5d84a9f06e169af Mon Sep 17 00:00:00 2001 From: neverland Date: Sat, 8 Aug 2026 10:33:27 +0800 Subject: [PATCH] perf(fmt): avoid absolute path joins in gitignore matching --- packages/rstack/src/fmt/discoverPaths.ts | 18 ++++++++++-------- .../rstack/tests/fmt/discoverPaths.test.ts | 17 +++++++++++++++++ 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/packages/rstack/src/fmt/discoverPaths.ts b/packages/rstack/src/fmt/discoverPaths.ts index 6b36951c..fba8da05 100644 --- a/packages/rstack/src/fmt/discoverPaths.ts +++ b/packages/rstack/src/fmt/discoverPaths.ts @@ -153,7 +153,8 @@ class GitIgnoreMatcher { // Ignore files may disappear or become unreadable during traversal. const loading = readFile(path.join(directoryPath, '.gitignore'), 'utf8') .then((content) => { - this.#matchers.set(directoryPath, ignore().add(content)); + const relativePath = toPosixPath(this.#resolveRelativePath(directoryPath)); + this.#matchers.set(relativePath, ignore().add(content)); }) .catch(() => undefined); @@ -179,18 +180,19 @@ class GitIgnoreMatcher { } #matches(relativePath: string, isDirectory: boolean): boolean { + const pathFromRoot = toPosixPath(relativePath); + // Most repositories only use a root `.gitignore`. Avoid checking every path // segment when no nested matcher can override its result. - const rootMatcher = this.#matchers.size === 1 ? this.#matchers.get(this.#rootPath) : undefined; + const rootMatcher = this.#matchers.size === 1 ? this.#matchers.get('') : undefined; if (rootMatcher) { // `ignore` expects POSIX separators and uses a trailing slash to distinguish directories. - const pathFromMatcher = toPosixPath(relativePath); - return rootMatcher.test(isDirectory ? `${pathFromMatcher}/` : pathFromMatcher).ignored; + return rootMatcher.test(isDirectory ? `${pathFromRoot}/` : pathFromRoot).ignored; } - const segments = relativePath.split(path.sep); - let directoryPath = this.#rootPath; - let pathFromMatcher = segments.join('/'); + const segments = pathFromRoot.split('/'); + let directoryPath = ''; + let pathFromMatcher = pathFromRoot; let ignored = false; for (const segment of segments) { @@ -205,7 +207,7 @@ class GitIgnoreMatcher { } } - directoryPath = path.join(directoryPath, segment); + directoryPath = directoryPath ? `${directoryPath}/${segment}` : segment; pathFromMatcher = pathFromMatcher.slice(segment.length + 1); } diff --git a/packages/rstack/tests/fmt/discoverPaths.test.ts b/packages/rstack/tests/fmt/discoverPaths.test.ts index 6210ba78..22f88b99 100644 --- a/packages/rstack/tests/fmt/discoverPaths.test.ts +++ b/packages/rstack/tests/fmt/discoverPaths.test.ts @@ -122,6 +122,23 @@ test('applies nested gitignore rules with child negation', async () => { }); }); +test('applies a nested gitignore without a root matcher', async () => { + await withTempProject(async (rootPath) => { + writeProjectFile(rootPath, 'src/.gitignore', '*.js\n'); + writeProjectFile(rootPath, 'src/drop.js'); + writeProjectFile(rootPath, 'src/keep.ts'); + writeProjectFile(rootPath, 'root.js'); + + const files = await discoverFmtPaths({ cwd: rootPath }); + + expect(relativePaths(rootPath, files)).toEqual([ + 'root.js', + path.join('src', '.gitignore'), + path.join('src', 'keep.ts'), + ]); + }); +}); + test('lets explicit files bypass gitignore', async () => { await withTempProject(async (rootPath) => { writeProjectFile(rootPath, '.gitignore', '/generated/\n');