1
>
/*---------------------------------------------------------------------------------------------
ignoreFile.ts
2
>
* Copyright (c) Microsoft Corporation. All rights reserved.
3
>
* Licensed under the MIT License. See License.txt in the project root for license information.
4
>
*--------------------------------------------------------------------------------------------*/
5
>
6
>
import * as glob from '../../../../base/common/glob.js';
7
>
import { startsWithIgnoreCase } from '../../../../base/common/strings.js';
8
>
9
>
export class IgnoreFile {
10
>
11
>
private isPathIgnored: (path: string, isDir: boolean, parent?: IgnoreFile) => boolean;
12
>
13
>
constructor(
14
>
contents: string,
15
>
private readonly location: string,
16
>
private readonly parent?: IgnoreFile,
17
>
private readonly ignoreCase = false) {
18
>
if (location[location.length - 1] === '\\') {
19
throw Error('Unexpected path format, do not use trailing backslashes');
20
}
22
location += '/';
23
}
24
>
this.isPathIgnored = this.parseIgnoreFile(contents, this.location, this.parent);
ignoreFile.ts
25
>
}
26
>
27
>
/**
28
>
* Updates the contents of the ignore file. Preserving the location and parent
29
>
* @param contents The new contents of the gitignore file
30
>
*/
31
>
updateContents(contents: string) {
32
this.isPathIgnored = this.parseIgnoreFile(contents, this.location, this.parent);
33
}
35
>
/**
36
>
* Returns true if a path in a traversable directory has not been ignored.
37
>
*
38
>
* Note: For performance reasons this does not check if the parent directories have been ignored,
39
>
* so it should always be used in tandem with `shouldTraverseDir` when walking a directory.
40
>
*
41
>
* In cases where a path must be tested in isolation, `isArbitraryPathIncluded` should be used.
42
>
*/
43
>
isPathIncludedInTraversal(path: string, isDir: boolean): boolean {
44
>
if (path[0] !== '/' || path[path.length - 1] === '/') {
45
throw Error('Unexpected path format, expected to begin with slash and end without. got:' + path);
46
}
48
>
const ignored = this.isPathIgnored(path, isDir);
49
>
50
>
return !ignored;
51
>
}
52
>
53
>
/**
54
>
* Returns true if an arbitrary path has not been ignored.
55
>
* This is an expensive operation and should only be used outside of traversals.
56
>
*/
57
>
isArbitraryPathIgnored(path: string, isDir: boolean): boolean {
58
>
if (path[0] !== '/' || path[path.length - 1] === '/') {
59
throw Error('Unexpected path format, expected to begin with slash and end without. got:' + path);
60
}
62
>
const segments = path.split('/').filter(x => x);
63
>
let ignored = false;
64
>
65
>
let walkingPath = '';
66
>
67
>
for (let i = 0; i < segments.length; i++) {
68
>
const isLast = i === segments.length - 1;
69
>
const segment = segments[i];
70
>
71
>
walkingPath = walkingPath + '/' + segment;
72
>
73
>
if (!this.isPathIncludedInTraversal(walkingPath, isLast ? isDir : true)) {
74
>
ignored = true;
75
>
break;
76
>
}
77
>
}
78
>
79
>
return ignored;
80
>
}
81
>
82
>
private gitignoreLinesToExpression(lines: string[], dirPath: string, trimForExclusions: boolean): glob.ParsedExpression {
83
>
const includeLines = lines.map(line => this.gitignoreLineToGlob(line, dirPath));
84
>
85
>
const includeExpression: glob.IExpression = Object.create(null);
86
>
for (const line of includeLines) {
87
>
includeExpression[line] = true;
88
>
}
89
>
90
>
return glob.parse(includeExpression, { trimForExclusions, ignoreCase: this.ignoreCase });
91
>
}
92
>
93
>
private parseIgnoreFile(ignoreContents: string, dirPath: string, parent: IgnoreFile | undefined): (path: string, isDir: boolean) => boolean {
94
>
const contentLines = ignoreContents
95
>
.split('\n')
96
>
.map(line => line.trim())
97
>
.filter(line => line && line[0] !== '#');
98
>
99
>
// Pull out all the lines that end with `/`, those only apply to directories
100
>
const fileLines = contentLines.filter(line => !line.endsWith('/'));
101
>
102
>
const fileIgnoreLines = fileLines.filter(line => !line.includes('!'));
103
>
const isFileIgnored = this.gitignoreLinesToExpression(fileIgnoreLines, dirPath, true);
104
>
105
>
// TODO: Slight hack... this naive approach may reintroduce too many files in cases of weirdly complex .gitignores
106
>
const fileIncludeLines = fileLines.filter(line => line.includes('!')).map(line => line.replace(/!/g, ''));
107
>
const isFileIncluded = this.gitignoreLinesToExpression(fileIncludeLines, dirPath, false);
108
>
109
>
// When checking if a dir is ignored we can use all lines
110
>
const dirIgnoreLines = contentLines.filter(line => !line.includes('!'));
111
>
const isDirIgnored = this.gitignoreLinesToExpression(dirIgnoreLines, dirPath, true);
112
>
113
>
// Same hack.
114
>
const dirIncludeLines = contentLines.filter(line => line.includes('!')).map(line => line.replace(/!/g, ''));
115
>
const isDirIncluded = this.gitignoreLinesToExpression(dirIncludeLines, dirPath, false);
116
>
117
>
const isPathIgnored = (path: string, isDir: boolean) => {
118
>
if (!(this.ignoreCase ? startsWithIgnoreCase(path, dirPath) : path.startsWith(dirPath))) { return false; }
119
>
120
>
const dirIncluded = isDir && isDirIncluded(path);
121
>
if (isDir && isDirIgnored(path) && !dirIncluded) { return true; }
122
>
123
>
const fileIncluded = isFileIncluded(path);
124
>
if (isFileIgnored(path) && !fileIncluded) { return true; }
125
>
126
>
// If this file explicitly un-ignores a path via a negation pattern
127
>
// (e.g., `!.myconfig/`), do not delegate to the parent. In git, a
128
>
// negation in a child .gitignore overrides a positive pattern in a
129
>
// parent or global .gitignore.
130
>
if (dirIncluded || fileIncluded) { return false; }
131
>
132
>
if (parent) { return parent.isPathIgnored(path, isDir); }
133
134
return false;
136
>
137
>
return isPathIgnored;
138
>
}
139
>
140
>
private gitignoreLineToGlob(line: string, dirPath: string): string {
141
>
const firstSep = line.indexOf('/');
142
>
if (firstSep === -1 || firstSep === line.length - 1) {
143
line = '**/' + line;
145
if (firstSep === 0) {
146
if (dirPath.slice(-1) === '/') {