feat: handle nested gitignore files (#7645)

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
This commit is contained in:
Gaurav
2025-09-03 07:23:27 -07:00
committed by GitHub
parent 93ec574f68
commit abddd2b6ee
2 changed files with 169 additions and 4 deletions

View File

@@ -29,9 +29,38 @@ export class GitIgnoreParser implements GitIgnoreFilter {
// Always ignore .git directory regardless of .gitignore content
this.addPatterns(['.git']);
const patternFiles = ['.gitignore', path.join('.git', 'info', 'exclude')];
for (const pf of patternFiles) {
this.loadPatterns(pf);
this.loadPatterns(path.join('.git', 'info', 'exclude'));
this.findAndLoadGitignoreFiles(this.projectRoot);
}
private findAndLoadGitignoreFiles(dir: string): void {
const relativeDir = path.relative(this.projectRoot, dir);
// For sub-directories, check if they are ignored before proceeding.
// The root directory (relativeDir === '') should not be checked.
if (relativeDir && this.isIgnored(relativeDir)) {
return;
}
// Load patterns from .gitignore in the current directory
const gitignorePath = path.join(dir, '.gitignore');
if (fs.existsSync(gitignorePath)) {
this.loadPatterns(path.relative(this.projectRoot, gitignorePath));
}
// Recurse into subdirectories
try {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
if (entry.name === '.git') {
continue;
}
if (entry.isDirectory()) {
this.findAndLoadGitignoreFiles(path.join(dir, entry.name));
}
}
} catch (_error) {
// ignore readdir errors
}
}
@@ -44,10 +73,72 @@ export class GitIgnoreParser implements GitIgnoreFilter {
// ignore file not found
return;
}
// .git/info/exclude file patterns are relative to project root and not file directory
const isExcludeFile =
patternsFileName.replace(/\\/g, '/') === '.git/info/exclude';
const relativeBaseDir = isExcludeFile
? '.'
: path.dirname(patternsFileName);
const patterns = (content ?? '')
.split('\n')
.map((p) => p.trim())
.filter((p) => p !== '' && !p.startsWith('#'));
.filter((p) => p !== '' && !p.startsWith('#'))
.map((p) => {
const isNegative = p.startsWith('!');
if (isNegative) {
p = p.substring(1);
}
const isAnchoredInFile = p.startsWith('/');
if (isAnchoredInFile) {
p = p.substring(1);
}
// An empty pattern can result from a negated pattern like `!`,
// which we can ignore.
if (p === '') {
return '';
}
let newPattern = p;
if (relativeBaseDir && relativeBaseDir !== '.') {
// Only in nested .gitignore files, the patterns need to be modified according to:
// - If `a/b/.gitignore` defines `/c` then it needs to be changed to `/a/b/c`
// - If `a/b/.gitignore` defines `c` then it needs to be changed to `/a/b/**/c`
// - If `a/b/.gitignore` defines `c/d` then it needs to be changed to `/a/b/c/d`
if (!isAnchoredInFile && !p.includes('/')) {
// If no slash and not anchored in file, it matches files in any
// subdirectory.
newPattern = path.join('**', p);
}
// Prepend the .gitignore file's directory.
newPattern = path.join(relativeBaseDir, newPattern);
// Anchor the pattern to a nested gitignore directory.
if (!newPattern.startsWith('/')) {
newPattern = '/' + newPattern;
}
}
// Anchor the pattern if originally anchored
if (isAnchoredInFile && !newPattern.startsWith('/')) {
newPattern = '/' + newPattern;
}
if (isNegative) {
newPattern = '!' + newPattern;
}
// Even in windows, Ignore expects forward slashes.
newPattern = newPattern.replace(/\\/g, '/');
return newPattern;
})
.filter((p) => p !== '');
this.addPatterns(patterns);
}