feat(git): Refactor gitignore handling to use ignore library instead of minimatch (#864)

This commit is contained in:
N. Taylor Mullen
2025-06-08 18:32:19 -07:00
committed by GitHub
parent d061419452
commit 72fa01f62d
6 changed files with 65 additions and 238 deletions

View File

@@ -6,18 +6,17 @@
import * as fs from 'fs/promises';
import * as path from 'path';
import { minimatch } from 'minimatch';
import ignore, { Ignore } from 'ignore';
import { isGitRepository } from './gitUtils.js';
export interface GitIgnoreFilter {
isIgnored(filePath: string): boolean;
getIgnoredPatterns(): string[];
}
export class GitIgnoreParser implements GitIgnoreFilter {
private ignorePatterns: string[] = [];
private projectRoot: string;
private isGitRepo: boolean = false;
private ig: Ignore = ignore();
constructor(projectRoot: string) {
this.projectRoot = path.resolve(projectRoot);
@@ -32,13 +31,13 @@ export class GitIgnoreParser implements GitIgnoreFilter {
];
// Always ignore .git directory regardless of .gitignore content
this.ignorePatterns = ['.git/**', '.git'];
this.addPatterns(['.git']);
for (const gitIgnoreFile of gitIgnoreFiles) {
try {
const content = await fs.readFile(gitIgnoreFile, 'utf-8');
const patterns = this.parseGitIgnoreContent(content);
this.ignorePatterns.push(...patterns);
const patterns = content.split('\n').map((p) => p.trim());
this.addPatterns(patterns);
} catch (_error) {
// File doesn't exist or can't be read, continue silently
}
@@ -46,71 +45,33 @@ export class GitIgnoreParser implements GitIgnoreFilter {
}
}
private parseGitIgnoreContent(content: string): string[] {
return content
.split('\n')
.map((line) => line.trim())
.filter((line) => line && !line.startsWith('#'))
.map((pattern) => {
// Handle negation patterns (!) - for now we'll skip them
if (pattern.startsWith('!')) {
return null;
}
// Convert gitignore patterns to minimatch-compatible patterns
if (pattern.endsWith('/')) {
// Directory pattern - match directory and all contents
const dirPattern = pattern.slice(0, -1); // Remove trailing slash
return [dirPattern, dirPattern + '/**'];
}
// If pattern doesn't contain /, it should match at any level
if (!pattern.includes('/') && !pattern.startsWith('**/')) {
return '**/' + pattern;
}
return pattern;
})
.filter((pattern) => pattern !== null)
.flat() as string[];
private addPatterns(patterns: string[]) {
this.ig.add(patterns);
}
isIgnored(filePath: string): boolean {
// If not a git repository, nothing is ignored
if (!this.isGitRepo) {
return false;
}
// Normalize the input path (handle ./ prefixes)
let cleanPath = filePath;
if (cleanPath.startsWith('./')) {
cleanPath = cleanPath.slice(2);
}
const relativePath = path.isAbsolute(filePath)
? path.relative(this.projectRoot, filePath)
: filePath;
// Convert to relative path from project root
const relativePath = path.relative(
this.projectRoot,
path.resolve(this.projectRoot, cleanPath),
);
// Handle paths that go outside project root
if (relativePath.startsWith('..')) {
if (relativePath === '' || relativePath.startsWith('..')) {
return false;
}
// Normalize path separators for cross-platform compatibility
const normalizedPath = relativePath.replace(/\\/g, '/');
let normalizedPath = relativePath.replace(/\\/g, '/');
if (normalizedPath.startsWith('./')) {
normalizedPath = normalizedPath.substring(2);
}
return this.ignorePatterns.some((pattern) =>
minimatch(normalizedPath, pattern, {
dot: true,
matchBase: false,
flipNegate: false,
}),
);
const ignored = this.ig.ignores(normalizedPath);
return ignored;
}
getIgnoredPatterns(): string[] {
return [...this.ignorePatterns];
getGitRepoRoot(): string {
return this.projectRoot;
}
}