1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
|
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import * as fs from 'fs/promises';
import * as path from 'path';
import { minimatch } from 'minimatch';
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;
constructor(projectRoot: string) {
this.projectRoot = path.resolve(projectRoot);
}
async initialize(): Promise<void> {
this.isGitRepo = isGitRepository(this.projectRoot);
if (this.isGitRepo) {
const gitIgnoreFiles = [
path.join(this.projectRoot, '.gitignore'),
path.join(this.projectRoot, '.git', 'info', 'exclude'),
];
// Always ignore .git directory regardless of .gitignore content
this.ignorePatterns = ['.git/**', '.git'];
for (const gitIgnoreFile of gitIgnoreFiles) {
try {
const content = await fs.readFile(gitIgnoreFile, 'utf-8');
const patterns = this.parseGitIgnoreContent(content);
this.ignorePatterns.push(...patterns);
} catch (_error) {
// File doesn't exist or can't be read, continue silently
}
}
}
}
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[];
}
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);
}
// 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('..')) {
return false;
}
// Normalize path separators for cross-platform compatibility
const normalizedPath = relativePath.replace(/\\/g, '/');
return this.ignorePatterns.some((pattern) =>
minimatch(normalizedPath, pattern, {
dot: true,
matchBase: false,
flipNegate: false,
}),
);
}
getIgnoredPatterns(): string[] {
return [...this.ignorePatterns];
}
}
|