-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathgitignore-utils.ts
More file actions
199 lines (176 loc) · 6.14 KB
/
gitignore-utils.ts
File metadata and controls
199 lines (176 loc) · 6.14 KB
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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
import * as fs from 'fs';
import * as path from 'path';
/**
* Represents a parsed gitignore pattern
*/
export interface GitignorePattern {
pattern: string;
isNegated: boolean;
isDirectory: boolean;
isAbsolute: boolean;
}
/**
* Parses a .gitignore file and returns an array of patterns
* @param gitignorePath Path to the .gitignore file
* @returns Array of parsed gitignore patterns
*/
export function parseGitignoreFile(gitignorePath: string): GitignorePattern[] {
if (!fs.existsSync(gitignorePath)) {
return [];
}
const content = fs.readFileSync(gitignorePath, 'utf8');
return parseGitignoreContent(content);
}
/**
* Parses gitignore content and returns an array of patterns
* @param content Content of the .gitignore file
* @returns Array of parsed gitignore patterns
*/
export function parseGitignoreContent(content: string): GitignorePattern[] {
return content
.split('\n')
.map(line => line.trim())
.filter(line => line && !line.startsWith('#')) // Remove empty lines and comments
.map(line => {
const isNegated = line.startsWith('!');
const pattern = isNegated ? line.substring(1) : line;
const isDirectory = pattern.endsWith('/');
const isAbsolute = pattern.startsWith('/') || pattern.startsWith('./');
return {
pattern: isAbsolute ? pattern.substring(pattern.startsWith('./') ? 2 : 1) : pattern,
isNegated,
isDirectory,
isAbsolute
};
});
}
/**
* Checks if a file or directory matches any of the gitignore patterns
* @param filePath Path to the file or directory (relative to the directory containing the .gitignore)
* @param patterns Array of gitignore patterns
* @param isDirectory Whether the path is a directory
* @returns True if the file or directory should be ignored
*/
export function matchesGitignorePatterns(
filePath: string,
patterns: GitignorePattern[],
isDirectory: boolean
): boolean {
// Normalize path for matching
const normalizedPath = filePath.replace(/\\/g, '/');
// Start with not ignored, then apply patterns in order
let ignored = false;
for (const pattern of patterns) {
// Skip directory-only patterns if this is a file
if (pattern.isDirectory && !isDirectory) {
continue;
}
if (matchesPattern(normalizedPath, pattern, isDirectory)) {
// If pattern matches, set ignored based on whether it's negated
ignored = !pattern.isNegated;
}
}
return ignored;
}
/**
* Checks if a path matches a gitignore pattern
* @param normalizedPath Normalized path to check
* @param pattern Gitignore pattern
* @param isDirectory Whether the path is a directory
* @returns True if the path matches the pattern
*/
function matchesPattern(
normalizedPath: string,
pattern: GitignorePattern,
isDirectory: boolean
): boolean {
const patternStr = pattern.pattern.replace(/\\/g, '/');
// For directory patterns (ending with /), also check without the trailing slash
// when matching against directories
if (pattern.isDirectory && isDirectory && patternStr.endsWith('/')) {
const patternWithoutSlash = patternStr.slice(0, -1);
if (normalizedPath === patternWithoutSlash) {
return true;
}
}
// Handle exact matches
if (!patternStr.includes('*')) {
if (pattern.isAbsolute) {
// For absolute patterns, match from the beginning
return normalizedPath === patternStr ||
(isDirectory && normalizedPath.startsWith(patternStr + '/'));
} else {
// For relative patterns, match anywhere in the path
return normalizedPath === patternStr ||
normalizedPath.endsWith('/' + patternStr) ||
normalizedPath.includes('/' + patternStr + '/') ||
(isDirectory && (
normalizedPath.endsWith('/' + patternStr) ||
normalizedPath.includes('/' + patternStr + '/')
));
}
}
// Handle wildcard patterns
const regexPattern = patternStr
.replace(/\./g, '\\.') // Escape dots
.replace(/\*/g, '.*') // Convert * to .*
.replace(/\?/g, '.'); // Convert ? to .
const regex = pattern.isAbsolute
? new RegExp(`^${regexPattern}$`)
: new RegExp(`(^|/)${regexPattern}$`);
return regex.test(normalizedPath);
}
/**
* Collects gitignore patterns from a specific directory
* @param dirPath Path to the directory
* @returns Array of gitignore patterns from this directory
*/
export function collectDirectoryGitignorePatterns(dirPath: string): GitignorePattern[] {
const gitignorePath = path.join(dirPath, '.gitignore');
if (fs.existsSync(gitignorePath)) {
return parseGitignoreFile(gitignorePath);
}
return [];
}
/**
* Collects all gitignore patterns that apply to a given directory
* @param dirPath Path to the directory
* @returns Array of gitignore patterns that apply to the directory
*/
export function collectGitignorePatterns(dirPath: string): GitignorePattern[] {
const patterns: GitignorePattern[] = [];
let currentDir = dirPath;
// Collect patterns from all parent directories up to the root
while (true) {
const gitignorePath = path.join(currentDir, '.gitignore');
if (fs.existsSync(gitignorePath)) {
const dirPatterns = parseGitignoreFile(gitignorePath);
patterns.push(...dirPatterns);
}
const parentDir = path.dirname(currentDir);
if (parentDir === currentDir) {
break; // Reached the root
}
currentDir = parentDir;
}
return patterns;
}
/**
* Determines if a file or directory should be excluded based on gitignore patterns
* @param fullPath Full path to the file or directory
* @param baseDir Base directory for relative path calculation
* @param patterns Gitignore patterns to check against
* @param isDirectory Whether the path is a directory
* @returns True if the file or directory should be excluded
*/
export function shouldExcludeByGitignore(
fullPath: string,
baseDir: string,
patterns: GitignorePattern[],
isDirectory: boolean
): boolean {
// Calculate path relative to the base directory
const relativePath = path.relative(baseDir, fullPath).replace(/\\/g, '/');
// Check if the path matches any gitignore pattern
return matchesGitignorePatterns(relativePath, patterns, isDirectory);
}