Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions packages/rstack/src/fmt/discoverPaths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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) {
Expand All @@ -205,7 +207,7 @@ class GitIgnoreMatcher {
}
}

directoryPath = path.join(directoryPath, segment);
directoryPath = directoryPath ? `${directoryPath}/${segment}` : segment;
pathFromMatcher = pathFromMatcher.slice(segment.length + 1);
}

Expand Down
17 changes: 17 additions & 0 deletions packages/rstack/tests/fmt/discoverPaths.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down