@gitkraken/core-gitlens 0.17.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -1
- package/dist/git/cache.d.ts +7 -0
- package/dist/git/cache.js +21 -3
- package/dist/git/cache.js.map +1 -1
- package/dist/git/watching/gitIgnoreFilter.d.ts +21 -3
- package/dist/git/watching/gitIgnoreFilter.js +74 -14
- package/dist/git/watching/gitIgnoreFilter.js.map +1 -1
- package/dist/git/watching/provider.d.ts +9 -1
- package/dist/git/watching/watchService.d.ts +14 -1
- package/dist/git/watching/watchService.js +57 -16
- package/dist/git/watching/watchService.js.map +1 -1
- package/dist/git/watching/worktreeSummary.d.ts +38 -10
- package/dist/git/watching/worktreeSummary.js +164 -50
- package/dist/git/watching/worktreeSummary.js.map +1 -1
- package/dist/git-cli/cliGitProvider.js +4 -4
- package/dist/git-cli/cliGitProvider.js.map +1 -1
- package/dist/git-cli/providers/branches.js +1 -32
- package/dist/git-cli/providers/branches.js.map +1 -1
- package/dist/git-cli/providers/config.d.ts +21 -0
- package/dist/git-cli/providers/config.js +72 -1
- package/dist/git-cli/providers/config.js.map +1 -1
- package/dist/plus/integrations/providers/issueSorts.d.ts +0 -1
- package/dist/plus/integrations/reads/drains.d.ts +0 -1
- package/package.json +1 -1
|
@@ -51,6 +51,9 @@ export declare class GitIgnoreFilter {
|
|
|
51
51
|
private inflight;
|
|
52
52
|
/** The one load queued behind {@link inflight}, which every refresh made meanwhile shares */
|
|
53
53
|
private trailing;
|
|
54
|
+
/** The running {@link revalidate}, which {@link ready} also waits for */
|
|
55
|
+
private revalidating;
|
|
56
|
+
private sources;
|
|
54
57
|
private readonly _generation;
|
|
55
58
|
private readonly _scratchMatchers;
|
|
56
59
|
private readonly _scratchBases;
|
|
@@ -62,7 +65,7 @@ export declare class GitIgnoreFilter {
|
|
|
62
65
|
private readonly getGitIgnorePaths?;
|
|
63
66
|
private readonly getIgnoreCase?;
|
|
64
67
|
constructor(options: GitIgnoreFilterOptions);
|
|
65
|
-
/** Resolves when patterns are loaded. Safe to call multiple times. */
|
|
68
|
+
/** Resolves when patterns are loaded and any {@link revalidate} has settled. Safe to call multiple times. */
|
|
66
69
|
ready(): Promise<void>;
|
|
67
70
|
/**
|
|
68
71
|
* Check if a path is ignored. Must be called after `ready()`.
|
|
@@ -84,6 +87,15 @@ export declare class GitIgnoreFilter {
|
|
|
84
87
|
* queued behind it — a checkout touching many `.gitignore` files costs two reloads, not one per file.
|
|
85
88
|
*/
|
|
86
89
|
refresh(): Promise<void>;
|
|
90
|
+
/**
|
|
91
|
+
* Reloads when an ignore source changed since the last load, for a caller that stopped watching them for a while.
|
|
92
|
+
* Resolves true once that reload has committed; false when nothing changed, when no load has committed yet, or when
|
|
93
|
+
* a load is running, queued, or started meanwhile (it reads the sources afresh).
|
|
94
|
+
*
|
|
95
|
+
* Only the sources the last load read are stat'ed — the nested list isn't rediscovered, which would cost a git
|
|
96
|
+
* spawn — so a nested `.gitignore` created since then goes unseen until the next {@link refresh}.
|
|
97
|
+
*/
|
|
98
|
+
revalidate(): Promise<boolean>;
|
|
87
99
|
private startLoad;
|
|
88
100
|
/**
|
|
89
101
|
* Mirrors git's directory walk: each ancestor directory is checked shallow→deep, and the first one found ignored
|
|
@@ -96,12 +108,18 @@ export declare class GitIgnoreFilter {
|
|
|
96
108
|
/** Returns the matcher's decisive verdict for `path`, or undefined when it has none */
|
|
97
109
|
private testLevel;
|
|
98
110
|
private loadPatterns;
|
|
111
|
+
/** Whether anything {@link loadPatterns} built `sources` from differs now; a source that can't be stat'ed has */
|
|
112
|
+
private sourcesChanged;
|
|
99
113
|
/** Reads every nested `.gitignore`, keyed by its repo-relative directory (no trailing slash) */
|
|
100
114
|
private loadNested;
|
|
115
|
+
/** Stamps an ignore file, then reads it — stamped first, so an edit landing in between reads as a change later */
|
|
116
|
+
private readSource;
|
|
117
|
+
/** `mtime:size`, `''` when the file is absent, or undefined when it can't be stat'ed */
|
|
118
|
+
private stamp;
|
|
101
119
|
private readFile;
|
|
102
|
-
private
|
|
120
|
+
private resolveInfoExcludePath;
|
|
103
121
|
private readIgnoreCase;
|
|
104
|
-
private
|
|
122
|
+
private resolveGlobalExcludesPath;
|
|
105
123
|
}
|
|
106
124
|
//#endregion
|
|
107
125
|
//# sourceMappingURL=gitIgnoreFilter.d.ts.map
|
|
@@ -33,6 +33,9 @@ var GitIgnoreFilter = class {
|
|
|
33
33
|
inflight;
|
|
34
34
|
/** The one load queued behind {@link inflight}, which every refresh made meanwhile shares */
|
|
35
35
|
trailing;
|
|
36
|
+
/** The running {@link revalidate}, which {@link ready} also waits for */
|
|
37
|
+
revalidating;
|
|
38
|
+
sources;
|
|
36
39
|
_generation = getScopedCounter();
|
|
37
40
|
_scratchMatchers = [];
|
|
38
41
|
_scratchBases = [];
|
|
@@ -53,8 +56,9 @@ var GitIgnoreFilter = class {
|
|
|
53
56
|
this.getIgnoreCase = options.getIgnoreCase;
|
|
54
57
|
this.startLoad();
|
|
55
58
|
}
|
|
56
|
-
/** Resolves when patterns are loaded. Safe to call multiple times. */
|
|
59
|
+
/** Resolves when patterns are loaded and any {@link revalidate} has settled. Safe to call multiple times. */
|
|
57
60
|
async ready() {
|
|
61
|
+
if (this.revalidating != null) await this.revalidating;
|
|
58
62
|
if (this.loading != null) await this.loading;
|
|
59
63
|
}
|
|
60
64
|
/**
|
|
@@ -98,6 +102,29 @@ var GitIgnoreFilter = class {
|
|
|
98
102
|
this.loading = this.trailing;
|
|
99
103
|
return this.trailing;
|
|
100
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Reloads when an ignore source changed since the last load, for a caller that stopped watching them for a while.
|
|
107
|
+
* Resolves true once that reload has committed; false when nothing changed, when no load has committed yet, or when
|
|
108
|
+
* a load is running, queued, or started meanwhile (it reads the sources afresh).
|
|
109
|
+
*
|
|
110
|
+
* Only the sources the last load read are stat'ed — the nested list isn't rediscovered, which would cost a git
|
|
111
|
+
* spawn — so a nested `.gitignore` created since then goes unseen until the next {@link refresh}.
|
|
112
|
+
*/
|
|
113
|
+
revalidate() {
|
|
114
|
+
if (this.revalidating != null) return this.revalidating;
|
|
115
|
+
const sources = this.sources;
|
|
116
|
+
if (sources == null || this.loading != null) return Promise.resolve(false);
|
|
117
|
+
const generation = this._generation.current;
|
|
118
|
+
const revalidation = this.sourcesChanged(sources).then(async (changed) => {
|
|
119
|
+
if (!changed || this._generation.current !== generation) return false;
|
|
120
|
+
await this.refresh();
|
|
121
|
+
return true;
|
|
122
|
+
}).finally(() => {
|
|
123
|
+
if (this.revalidating === revalidation) this.revalidating = void 0;
|
|
124
|
+
});
|
|
125
|
+
this.revalidating = revalidation;
|
|
126
|
+
return revalidation;
|
|
127
|
+
}
|
|
101
128
|
startLoad() {
|
|
102
129
|
const load = this.loadPatterns().finally(() => {
|
|
103
130
|
if (this.inflight === load) this.inflight = void 0;
|
|
@@ -152,13 +179,17 @@ var GitIgnoreFilter = class {
|
|
|
152
179
|
}
|
|
153
180
|
async loadPatterns() {
|
|
154
181
|
const id = this._generation.next();
|
|
182
|
+
const stamps = /* @__PURE__ */ new Map();
|
|
183
|
+
const globalExcludesPath = this.resolveGlobalExcludesPath();
|
|
184
|
+
const infoExcludePath = this.resolveInfoExcludePath();
|
|
155
185
|
const [globalResult, infoResult, rootResult, nestedResult, ignoreCaseResult] = await Promise.allSettled([
|
|
156
|
-
this.
|
|
157
|
-
this.
|
|
158
|
-
this.
|
|
159
|
-
this.loadNested(),
|
|
186
|
+
globalExcludesPath.then((path) => this.readSource(path, stamps)),
|
|
187
|
+
infoExcludePath.then((path) => this.readSource(path, stamps)),
|
|
188
|
+
this.readSource(`${this.repoPath}/.gitignore`, stamps),
|
|
189
|
+
this.loadNested(stamps),
|
|
160
190
|
this.readIgnoreCase()
|
|
161
191
|
]);
|
|
192
|
+
const [globalPathResult, infoPathResult] = await Promise.allSettled([globalExcludesPath, infoExcludePath]);
|
|
162
193
|
if (id !== this._generation.current) return;
|
|
163
194
|
const options = { ignorecase: getSettledValue(ignoreCaseResult) ?? defaultIgnoreCase };
|
|
164
195
|
const ig = ignore(options);
|
|
@@ -182,9 +213,25 @@ var GitIgnoreFilter = class {
|
|
|
182
213
|
}
|
|
183
214
|
this.ig = ig;
|
|
184
215
|
this.nested = nested?.size ? nested : void 0;
|
|
216
|
+
this.sources = {
|
|
217
|
+
ignoreCase: options.ignorecase,
|
|
218
|
+
globalExcludesPath: getSettledValue(globalPathResult),
|
|
219
|
+
infoExcludePath: getSettledValue(infoPathResult),
|
|
220
|
+
stamps
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
/** Whether anything {@link loadPatterns} built `sources` from differs now; a source that can't be stat'ed has */
|
|
224
|
+
async sourcesChanged(sources) {
|
|
225
|
+
const [globalExcludesPath, infoExcludePath, ignoreCase, unchanged] = await Promise.allSettled([
|
|
226
|
+
this.resolveGlobalExcludesPath(),
|
|
227
|
+
this.resolveInfoExcludePath(),
|
|
228
|
+
this.readIgnoreCase(),
|
|
229
|
+
Promise.allSettled(Array.from(sources.stamps, async ([path, stamp]) => stamp != null && stamp === await this.stamp(path)))
|
|
230
|
+
]);
|
|
231
|
+
return getSettledValue(globalExcludesPath) !== sources.globalExcludesPath || getSettledValue(infoExcludePath) !== sources.infoExcludePath || (getSettledValue(ignoreCase) ?? defaultIgnoreCase) !== sources.ignoreCase || getSettledValue(unchanged)?.every((r) => getSettledValue(r) === true) !== true;
|
|
185
232
|
}
|
|
186
233
|
/** Reads every nested `.gitignore`, keyed by its repo-relative directory (no trailing slash) */
|
|
187
|
-
async loadNested() {
|
|
234
|
+
async loadNested(stamps) {
|
|
188
235
|
if (this.getGitIgnorePaths == null) return void 0;
|
|
189
236
|
let paths;
|
|
190
237
|
try {
|
|
@@ -200,7 +247,7 @@ var GitIgnoreFilter = class {
|
|
|
200
247
|
}
|
|
201
248
|
if (!dirs.size) return void 0;
|
|
202
249
|
const entries = await Promise.allSettled(Array.from(dirs, async (dir) => {
|
|
203
|
-
const content = await this.
|
|
250
|
+
const content = await this.readSource(`${this.repoPath}/${dir}${suffix}`, stamps);
|
|
204
251
|
return content ? [dir, content] : void 0;
|
|
205
252
|
}));
|
|
206
253
|
const nested = /* @__PURE__ */ new Map();
|
|
@@ -210,6 +257,21 @@ var GitIgnoreFilter = class {
|
|
|
210
257
|
}
|
|
211
258
|
return nested;
|
|
212
259
|
}
|
|
260
|
+
/** Stamps an ignore file, then reads it — stamped first, so an edit landing in between reads as a change later */
|
|
261
|
+
async readSource(path, stamps) {
|
|
262
|
+
if (path == null) return void 0;
|
|
263
|
+
stamps.set(path, await this.stamp(path));
|
|
264
|
+
return this.readFile(path);
|
|
265
|
+
}
|
|
266
|
+
/** `mtime:size`, `''` when the file is absent, or undefined when it can't be stat'ed */
|
|
267
|
+
async stamp(path) {
|
|
268
|
+
try {
|
|
269
|
+
const stat = await this.fs.stat(fileUri(path));
|
|
270
|
+
return stat != null ? `${stat.mtime}:${stat.size}` : "";
|
|
271
|
+
} catch {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
213
275
|
async readFile(path) {
|
|
214
276
|
try {
|
|
215
277
|
const data = await this.fs.readFile(fileUri(path));
|
|
@@ -218,11 +280,10 @@ var GitIgnoreFilter = class {
|
|
|
218
280
|
return;
|
|
219
281
|
}
|
|
220
282
|
}
|
|
221
|
-
async
|
|
222
|
-
if (this.getInfoExcludePath == null) return
|
|
283
|
+
async resolveInfoExcludePath() {
|
|
284
|
+
if (this.getInfoExcludePath == null) return `${this.gitDirPath}/info/exclude`;
|
|
223
285
|
try {
|
|
224
|
-
|
|
225
|
-
return path != null ? await this.readFile(path) : void 0;
|
|
286
|
+
return await this.getInfoExcludePath();
|
|
226
287
|
} catch {
|
|
227
288
|
return;
|
|
228
289
|
}
|
|
@@ -235,11 +296,10 @@ var GitIgnoreFilter = class {
|
|
|
235
296
|
return;
|
|
236
297
|
}
|
|
237
298
|
}
|
|
238
|
-
async
|
|
299
|
+
async resolveGlobalExcludesPath() {
|
|
239
300
|
if (this.getGlobalExcludesPath == null) return void 0;
|
|
240
301
|
try {
|
|
241
|
-
|
|
242
|
-
return path != null ? await this.readFile(path) : void 0;
|
|
302
|
+
return await this.getGlobalExcludesPath();
|
|
243
303
|
} catch {
|
|
244
304
|
return;
|
|
245
305
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gitIgnoreFilter.js","names":[],"sources":["../../../../git/src/watching/gitIgnoreFilter.ts"],"sourcesContent":["import type { Ignore } from 'ignore';\nimport ignore from 'ignore';\nimport { getScopedCounter } from '@gitlens/utils/counter.js';\nimport { isLinux } from '@gitlens/utils/platform.js';\nimport { getSettledValue } from '@gitlens/utils/promise.js';\nimport { fileUri } from '@gitlens/utils/uri.js';\nimport type { FileSystemProvider } from '../context.js';\n\n/**\n * Case sensitivity when `core.ignorecase` is unset or unreadable. `ignore` matches case-insensitively by default, but\n * git follows `core.ignorecase` — false on Linux (and on a case-sensitive macOS volume), where matching `Build/`\n * against `build/` would call a real change ignored and drop it.\n */\nconst defaultIgnoreCase = !isLinux;\n\nexport interface GitIgnoreFilterOptions {\n\t/** Absolute path to the repository root (working tree) */\n\treadonly repoPath: string;\n\t/** Absolute path to the .git directory (for info/exclude when `getInfoExcludePath` isn't provided) */\n\treadonly gitDirPath: string;\n\t/** File system provider for reading ignore files */\n\treadonly fs: FileSystemProvider;\n\t/**\n\t * Returns the path to the global excludes file (core.excludesFile).\n\t * May return undefined if not configured.\n\t */\n\treadonly getGlobalExcludesPath?: () => Promise<string | undefined>;\n\t/**\n\t * Returns the absolute path to `info/exclude`, which lives in the COMMON git dir for a linked worktree.\n\t * When provided, replaces the `gitDirPath`-based resolution; undefined means there is none to read.\n\t */\n\treadonly getInfoExcludePath?: () => Promise<string | undefined>;\n\t/**\n\t * Returns the repo-relative, `/`-separated paths of every `.gitignore` git reads (root included).\n\t * Without it, only the root `.gitignore` is honoured.\n\t */\n\treadonly getGitIgnorePaths?: () => Promise<readonly string[]>;\n\t/**\n\t * Returns `core.ignorecase`, which decides whether patterns match case-insensitively. Undefined (or a failed read)\n\t * falls back to the platform default (case-sensitive on Linux only).\n\t */\n\treadonly getIgnoreCase?: () => Promise<boolean | undefined>;\n}\n\n/**\n * In-memory gitignore filter using the `ignore` npm package.\n * Fully library-owned — no VS Code dependencies.\n *\n * Loads patterns eagerly on creation, in git's precedence order (lowest first):\n * 1. Global excludes file (`core.excludesFile`)\n * 2. `info/exclude`\n * 3. `<repoPath>/.gitignore`\n * 4. Nested `.gitignore` files (via `getGitIgnorePaths`), deeper overriding shallower\n *\n * Once loaded, all checks are **synchronous** for maximum performance.\n * Call `ready()` before first use to ensure patterns are loaded.\n */\nexport class GitIgnoreFilter {\n\tprivate ig: Ignore = ignore({ ignorecase: defaultIgnoreCase });\n\t/** Matchers for nested `.gitignore` files, keyed by their repo-relative directory (no trailing slash) */\n\tprivate nested: Map<string, Ignore> | undefined;\n\t/** The newest load, which {@link ready} waits for; cleared once it settles */\n\tprivate loading: Promise<void> | undefined;\n\t/** The load running now; at most one runs at a time */\n\tprivate inflight: Promise<void> | undefined;\n\t/** The one load queued behind {@link inflight}, which every refresh made meanwhile shares */\n\tprivate trailing: Promise<void> | undefined;\n\tprivate readonly _generation = getScopedCounter();\n\n\t// Reused across `isIgnored` calls (which are synchronous, so never interleave) to avoid per-event allocations\n\tprivate readonly _scratchMatchers: Ignore[] = [];\n\tprivate readonly _scratchBases: number[] = [];\n\n\tprivate readonly repoPath: string;\n\tprivate readonly gitDirPath: string;\n\tprivate readonly fs: FileSystemProvider;\n\tprivate readonly getGlobalExcludesPath?: () => Promise<string | undefined>;\n\tprivate readonly getInfoExcludePath?: () => Promise<string | undefined>;\n\tprivate readonly getGitIgnorePaths?: () => Promise<readonly string[]>;\n\tprivate readonly getIgnoreCase?: () => Promise<boolean | undefined>;\n\n\tconstructor(options: GitIgnoreFilterOptions) {\n\t\tthis.repoPath = options.repoPath;\n\t\tthis.gitDirPath = options.gitDirPath;\n\t\tthis.fs = options.fs;\n\t\tthis.getGlobalExcludesPath = options.getGlobalExcludesPath;\n\t\tthis.getInfoExcludePath = options.getInfoExcludePath;\n\t\tthis.getGitIgnorePaths = options.getGitIgnorePaths;\n\t\tthis.getIgnoreCase = options.getIgnoreCase;\n\n\t\t// Start loading immediately\n\t\tvoid this.startLoad();\n\t}\n\n\t/** Resolves when patterns are loaded. Safe to call multiple times. */\n\tasync ready(): Promise<void> {\n\t\tif (this.loading != null) {\n\t\t\tawait this.loading;\n\t\t}\n\t}\n\n\t/**\n\t * Check if a path is ignored. Must be called after `ready()`.\n\t * Errs toward \"not ignored\": a false \"ignored\" drops a real change, a false \"not ignored\" only costs a refresh.\n\t * @param relativePath Path relative to the repository root (forward slashes; a trailing slash marks a directory)\n\t * @returns true if the path should be ignored\n\t */\n\tisIgnored(relativePath: string): boolean {\n\t\tif (!relativePath) return false;\n\n\t\ttry {\n\t\t\tif (this.nested == null) return this.ig.ignores(relativePath);\n\n\t\t\treturn this.isIgnoredNested(relativePath, this.nested);\n\t\t} catch {\n\t\t\t// `ignore` can throw on edge-case paths\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * Filter an array of relative paths, returning only non-ignored ones.\n\t * Must be called after `ready()`.\n\t */\n\tfilter(relativePaths: readonly string[]): string[] {\n\t\treturn relativePaths.filter(p => !this.isIgnored(p));\n\t}\n\n\t/**\n\t * Reload all patterns (e.g., after a .gitignore or info/exclude changes), rediscovering nested `.gitignore` files.\n\t * Returns a promise that resolves when a reload started after this call is complete.\n\t *\n\t * Single-flight: an idle filter reloads at once, but while a reload runs, every further call shares one reload\n\t * queued behind it — a checkout touching many `.gitignore` files costs two reloads, not one per file.\n\t */\n\trefresh(): Promise<void> {\n\t\tif (this.inflight == null) return this.startLoad();\n\n\t\tif (this.trailing == null) {\n\t\t\tconst run = (): Promise<void> => {\n\t\t\t\tthis.trailing = undefined;\n\t\t\t\treturn this.startLoad();\n\t\t\t};\n\t\t\tthis.trailing = this.inflight.then(run, run);\n\t\t}\n\t\tthis.loading = this.trailing;\n\t\treturn this.trailing;\n\t}\n\n\tprivate startLoad(): Promise<void> {\n\t\tconst load: Promise<void> = this.loadPatterns().finally(() => {\n\t\t\tif (this.inflight === load) {\n\t\t\t\tthis.inflight = undefined;\n\t\t\t}\n\t\t\tif (this.loading === load) {\n\t\t\t\tthis.loading = undefined;\n\t\t\t}\n\t\t});\n\t\tthis.inflight = load;\n\t\tthis.loading = load;\n\t\treturn load;\n\t}\n\n\t/**\n\t * Mirrors git's directory walk: each ancestor directory is checked shallow→deep, and the first one found ignored\n\t * ignores the whole path (git never descends into it, so nothing below can re-include it). At each level, the\n\t * matchers whose directory is a proper ancestor apply, and the deepest decisive one wins, falling back to the root.\n\t */\n\tprivate isIgnoredNested(path: string, nested: Map<string, Ignore>): boolean {\n\t\tconst matchers = this._scratchMatchers;\n\t\tconst bases = this._scratchBases;\n\n\t\ttry {\n\t\t\t// A trailing slash belongs to the path itself (a directory form), not to an ancestor\n\t\t\tconst last = path.length - 1;\n\n\t\t\tlet slash = path.indexOf('/');\n\t\t\twhile (slash !== -1 && slash < last) {\n\t\t\t\tif (this.isLevelIgnored(path, slash + 1, matchers, bases)) return true;\n\n\t\t\t\t// A directory's own `.gitignore` applies below it, never to the directory itself\n\t\t\t\tconst matcher = nested.get(path.slice(0, slash));\n\t\t\t\tif (matcher != null) {\n\t\t\t\t\tmatchers.push(matcher);\n\t\t\t\t\tbases.push(slash + 1);\n\t\t\t\t}\n\n\t\t\t\tslash = path.indexOf('/', slash + 1);\n\t\t\t}\n\n\t\t\treturn this.isLevelIgnored(path, path.length, matchers, bases);\n\t\t} finally {\n\t\t\tmatchers.length = 0;\n\t\t\tbases.length = 0;\n\t\t}\n\t}\n\n\t/** Whether `path.slice(0, end)` is ignored, given its ancestors aren't */\n\tprivate isLevelIgnored(path: string, end: number, matchers: readonly Ignore[], bases: readonly number[]): boolean {\n\t\tfor (let i = matchers.length - 1; i >= 0; i--) {\n\t\t\tconst verdict = this.testLevel(matchers[i], path.slice(bases[i], end));\n\t\t\tif (verdict != null) return verdict;\n\t\t}\n\n\t\treturn this.testLevel(this.ig, path.slice(0, end)) ?? false;\n\t}\n\n\t/** Returns the matcher's decisive verdict for `path`, or undefined when it has none */\n\tprivate testLevel(matcher: Ignore, path: string): boolean | undefined {\n\t\tconst result = matcher.test(path);\n\t\tif (result.unignored) return false;\n\t\tif (!result.ignored) return undefined;\n\n\t\t// `ignore` folds its own verdict for each ancestor into a path's result: once the matcher considers a parent\n\t\t// ignored, every child reports that parent's result. Reaching here means a deeper matcher re-included that\n\t\t// parent, so this result says nothing about the path itself — the matcher's own rules are unknowable, so\n\t\t// answer \"not ignored\" rather than risk dropping a real change.\n\t\tconst parentEnd = path.lastIndexOf('/', path.length - 2);\n\t\tif (parentEnd !== -1 && matcher.test(path.slice(0, parentEnd + 1)).ignored) return false;\n\n\t\treturn true;\n\t}\n\n\tprivate async loadPatterns(): Promise<void> {\n\t\tconst id = this._generation.next();\n\n\t\tconst [globalResult, infoResult, rootResult, nestedResult, ignoreCaseResult] = await Promise.allSettled([\n\t\t\tthis.readGlobalExcludes(),\n\t\t\tthis.readInfoExclude(),\n\t\t\tthis.readFile(`${this.repoPath}/.gitignore`),\n\t\t\tthis.loadNested(),\n\t\t\tthis.readIgnoreCase(),\n\t\t]);\n\n\t\t// Only commit results if no newer refresh has started\n\t\tif (id !== this._generation.current) return;\n\n\t\tconst options = { ignorecase: getSettledValue(ignoreCaseResult) ?? defaultIgnoreCase };\n\n\t\t// Added in git's precedence order (lowest first), since the last matching pattern wins\n\t\tconst ig = ignore(options);\n\t\tfor (const content of [\n\t\t\tgetSettledValue(globalResult),\n\t\t\tgetSettledValue(infoResult),\n\t\t\tgetSettledValue(rootResult),\n\t\t]) {\n\t\t\tif (!content) continue;\n\n\t\t\ttry {\n\t\t\t\tig.add(content);\n\t\t\t} catch {\n\t\t\t\t// A source `ignore` can't parse is skipped rather than failing the whole load\n\t\t\t}\n\t\t}\n\n\t\tlet nested: Map<string, Ignore> | undefined;\n\t\tconst nestedContents = getSettledValue(nestedResult);\n\t\tif (nestedContents?.size) {\n\t\t\tnested = new Map();\n\t\t\tfor (const [dir, content] of nestedContents) {\n\t\t\t\ttry {\n\t\t\t\t\tnested.set(dir, ignore(options).add(content));\n\t\t\t\t} catch {\n\t\t\t\t\t// A file `ignore` can't parse is skipped rather than failing the whole load\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tthis.ig = ig;\n\t\tthis.nested = nested?.size ? nested : undefined;\n\t}\n\n\t/** Reads every nested `.gitignore`, keyed by its repo-relative directory (no trailing slash) */\n\tprivate async loadNested(): Promise<Map<string, string> | undefined> {\n\t\tif (this.getGitIgnorePaths == null) return undefined;\n\n\t\tlet paths: readonly string[];\n\t\ttry {\n\t\t\tpaths = await this.getGitIgnorePaths();\n\t\t} catch {\n\t\t\t// Discovery failed — fall back to the root `.gitignore` alone\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst suffix = '/.gitignore';\n\t\tconst dirs = new Set<string>();\n\t\tfor (const path of paths) {\n\t\t\t// The root `.gitignore` (no directory) is loaded with the other root sources\n\t\t\tif (path.length <= suffix.length || !path.endsWith(suffix)) continue;\n\n\t\t\tdirs.add(path.slice(0, -suffix.length));\n\t\t}\n\t\tif (!dirs.size) return undefined;\n\n\t\tconst entries = await Promise.allSettled(\n\t\t\tArray.from(dirs, async dir => {\n\t\t\t\tconst content = await this.readFile(`${this.repoPath}/${dir}${suffix}`);\n\t\t\t\treturn content ? ([dir, content] as const) : undefined;\n\t\t\t}),\n\t\t);\n\n\t\tconst nested = new Map<string, string>();\n\t\tfor (const entry of entries) {\n\t\t\tconst value = getSettledValue(entry);\n\t\t\tif (value != null) {\n\t\t\t\tnested.set(value[0], value[1]);\n\t\t\t}\n\t\t}\n\t\treturn nested;\n\t}\n\n\tprivate async readFile(path: string): Promise<string | undefined> {\n\t\ttry {\n\t\t\tconst data = await this.fs.readFile(fileUri(path));\n\t\t\treturn new TextDecoder().decode(data);\n\t\t} catch {\n\t\t\t// File might not exist or be unreadable — that's fine\n\t\t\treturn undefined;\n\t\t}\n\t}\n\n\tprivate async readInfoExclude(): Promise<string | undefined> {\n\t\tif (this.getInfoExcludePath == null) return this.readFile(`${this.gitDirPath}/info/exclude`);\n\n\t\ttry {\n\t\t\tconst path = await this.getInfoExcludePath();\n\t\t\treturn path != null ? await this.readFile(path) : undefined;\n\t\t} catch {\n\t\t\t// The git dir couldn't be resolved\n\t\t\treturn undefined;\n\t\t}\n\t}\n\n\tprivate async readIgnoreCase(): Promise<boolean | undefined> {\n\t\tif (this.getIgnoreCase == null) return undefined;\n\n\t\ttry {\n\t\t\treturn await this.getIgnoreCase();\n\t\t} catch {\n\t\t\t// Unreadable — the platform default applies\n\t\t\treturn undefined;\n\t\t}\n\t}\n\n\tprivate async readGlobalExcludes(): Promise<string | undefined> {\n\t\tif (this.getGlobalExcludesPath == null) return undefined;\n\n\t\ttry {\n\t\t\tconst path = await this.getGlobalExcludesPath();\n\t\t\treturn path != null ? await this.readFile(path) : undefined;\n\t\t} catch {\n\t\t\t// Global excludes might not be configured\n\t\t\treturn undefined;\n\t\t}\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"gitIgnoreFilter.js","names":[],"sources":["../../../../git/src/watching/gitIgnoreFilter.ts"],"sourcesContent":["import type { Ignore } from 'ignore';\nimport ignore from 'ignore';\nimport { getScopedCounter } from '@gitlens/utils/counter.js';\nimport { isLinux } from '@gitlens/utils/platform.js';\nimport { getSettledValue } from '@gitlens/utils/promise.js';\nimport { fileUri } from '@gitlens/utils/uri.js';\nimport type { FileSystemProvider } from '../context.js';\n\n/**\n * Case sensitivity when `core.ignorecase` is unset or unreadable. `ignore` matches case-insensitively by default, but\n * git follows `core.ignorecase` — false on Linux (and on a case-sensitive macOS volume), where matching `Build/`\n * against `build/` would call a real change ignored and drop it.\n */\nconst defaultIgnoreCase = !isLinux;\n\nexport interface GitIgnoreFilterOptions {\n\t/** Absolute path to the repository root (working tree) */\n\treadonly repoPath: string;\n\t/** Absolute path to the .git directory (for info/exclude when `getInfoExcludePath` isn't provided) */\n\treadonly gitDirPath: string;\n\t/** File system provider for reading ignore files */\n\treadonly fs: FileSystemProvider;\n\t/**\n\t * Returns the path to the global excludes file (core.excludesFile).\n\t * May return undefined if not configured.\n\t */\n\treadonly getGlobalExcludesPath?: () => Promise<string | undefined>;\n\t/**\n\t * Returns the absolute path to `info/exclude`, which lives in the COMMON git dir for a linked worktree.\n\t * When provided, replaces the `gitDirPath`-based resolution; undefined means there is none to read.\n\t */\n\treadonly getInfoExcludePath?: () => Promise<string | undefined>;\n\t/**\n\t * Returns the repo-relative, `/`-separated paths of every `.gitignore` git reads (root included).\n\t * Without it, only the root `.gitignore` is honoured.\n\t */\n\treadonly getGitIgnorePaths?: () => Promise<readonly string[]>;\n\t/**\n\t * Returns `core.ignorecase`, which decides whether patterns match case-insensitively. Undefined (or a failed read)\n\t * falls back to the platform default (case-sensitive on Linux only).\n\t */\n\treadonly getIgnoreCase?: () => Promise<boolean | undefined>;\n}\n\n/** What a committed load was built from, which {@link GitIgnoreFilter.revalidate} compares against */\ninterface LoadedSources {\n\treadonly ignoreCase: boolean;\n\treadonly globalExcludesPath: string | undefined;\n\treadonly infoExcludePath: string | undefined;\n\t/** Every ignore file the load read, by absolute path, with its {@link GitIgnoreFilter.stamp} taken before the read */\n\treadonly stamps: ReadonlyMap<string, string | undefined>;\n}\n\n/**\n * In-memory gitignore filter using the `ignore` npm package.\n * Fully library-owned — no VS Code dependencies.\n *\n * Loads patterns eagerly on creation, in git's precedence order (lowest first):\n * 1. Global excludes file (`core.excludesFile`)\n * 2. `info/exclude`\n * 3. `<repoPath>/.gitignore`\n * 4. Nested `.gitignore` files (via `getGitIgnorePaths`), deeper overriding shallower\n *\n * Once loaded, all checks are **synchronous** for maximum performance.\n * Call `ready()` before first use to ensure patterns are loaded.\n */\nexport class GitIgnoreFilter {\n\tprivate ig: Ignore = ignore({ ignorecase: defaultIgnoreCase });\n\t/** Matchers for nested `.gitignore` files, keyed by their repo-relative directory (no trailing slash) */\n\tprivate nested: Map<string, Ignore> | undefined;\n\t/** The newest load, which {@link ready} waits for; cleared once it settles */\n\tprivate loading: Promise<void> | undefined;\n\t/** The load running now; at most one runs at a time */\n\tprivate inflight: Promise<void> | undefined;\n\t/** The one load queued behind {@link inflight}, which every refresh made meanwhile shares */\n\tprivate trailing: Promise<void> | undefined;\n\t/** The running {@link revalidate}, which {@link ready} also waits for */\n\tprivate revalidating: Promise<boolean> | undefined;\n\tprivate sources: LoadedSources | undefined;\n\tprivate readonly _generation = getScopedCounter();\n\n\t// Reused across `isIgnored` calls (which are synchronous, so never interleave) to avoid per-event allocations\n\tprivate readonly _scratchMatchers: Ignore[] = [];\n\tprivate readonly _scratchBases: number[] = [];\n\n\tprivate readonly repoPath: string;\n\tprivate readonly gitDirPath: string;\n\tprivate readonly fs: FileSystemProvider;\n\tprivate readonly getGlobalExcludesPath?: () => Promise<string | undefined>;\n\tprivate readonly getInfoExcludePath?: () => Promise<string | undefined>;\n\tprivate readonly getGitIgnorePaths?: () => Promise<readonly string[]>;\n\tprivate readonly getIgnoreCase?: () => Promise<boolean | undefined>;\n\n\tconstructor(options: GitIgnoreFilterOptions) {\n\t\tthis.repoPath = options.repoPath;\n\t\tthis.gitDirPath = options.gitDirPath;\n\t\tthis.fs = options.fs;\n\t\tthis.getGlobalExcludesPath = options.getGlobalExcludesPath;\n\t\tthis.getInfoExcludePath = options.getInfoExcludePath;\n\t\tthis.getGitIgnorePaths = options.getGitIgnorePaths;\n\t\tthis.getIgnoreCase = options.getIgnoreCase;\n\n\t\t// Start loading immediately\n\t\tvoid this.startLoad();\n\t}\n\n\t/** Resolves when patterns are loaded and any {@link revalidate} has settled. Safe to call multiple times. */\n\tasync ready(): Promise<void> {\n\t\tif (this.revalidating != null) {\n\t\t\tawait this.revalidating;\n\t\t}\n\t\t// Read after the revalidation: a refresh made during it supersedes it\n\t\tif (this.loading != null) {\n\t\t\tawait this.loading;\n\t\t}\n\t}\n\n\t/**\n\t * Check if a path is ignored. Must be called after `ready()`.\n\t * Errs toward \"not ignored\": a false \"ignored\" drops a real change, a false \"not ignored\" only costs a refresh.\n\t * @param relativePath Path relative to the repository root (forward slashes; a trailing slash marks a directory)\n\t * @returns true if the path should be ignored\n\t */\n\tisIgnored(relativePath: string): boolean {\n\t\tif (!relativePath) return false;\n\n\t\ttry {\n\t\t\tif (this.nested == null) return this.ig.ignores(relativePath);\n\n\t\t\treturn this.isIgnoredNested(relativePath, this.nested);\n\t\t} catch {\n\t\t\t// `ignore` can throw on edge-case paths\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * Filter an array of relative paths, returning only non-ignored ones.\n\t * Must be called after `ready()`.\n\t */\n\tfilter(relativePaths: readonly string[]): string[] {\n\t\treturn relativePaths.filter(p => !this.isIgnored(p));\n\t}\n\n\t/**\n\t * Reload all patterns (e.g., after a .gitignore or info/exclude changes), rediscovering nested `.gitignore` files.\n\t * Returns a promise that resolves when a reload started after this call is complete.\n\t *\n\t * Single-flight: an idle filter reloads at once, but while a reload runs, every further call shares one reload\n\t * queued behind it — a checkout touching many `.gitignore` files costs two reloads, not one per file.\n\t */\n\trefresh(): Promise<void> {\n\t\tif (this.inflight == null) return this.startLoad();\n\n\t\tif (this.trailing == null) {\n\t\t\tconst run = (): Promise<void> => {\n\t\t\t\tthis.trailing = undefined;\n\t\t\t\treturn this.startLoad();\n\t\t\t};\n\t\t\tthis.trailing = this.inflight.then(run, run);\n\t\t}\n\t\tthis.loading = this.trailing;\n\t\treturn this.trailing;\n\t}\n\n\t/**\n\t * Reloads when an ignore source changed since the last load, for a caller that stopped watching them for a while.\n\t * Resolves true once that reload has committed; false when nothing changed, when no load has committed yet, or when\n\t * a load is running, queued, or started meanwhile (it reads the sources afresh).\n\t *\n\t * Only the sources the last load read are stat'ed — the nested list isn't rediscovered, which would cost a git\n\t * spawn — so a nested `.gitignore` created since then goes unseen until the next {@link refresh}.\n\t */\n\trevalidate(): Promise<boolean> {\n\t\tif (this.revalidating != null) return this.revalidating;\n\n\t\tconst sources = this.sources;\n\t\tif (sources == null || this.loading != null) return Promise.resolve(false);\n\n\t\tconst generation = this._generation.current;\n\t\tconst revalidation: Promise<boolean> = this.sourcesChanged(sources)\n\t\t\t.then(async changed => {\n\t\t\t\tif (!changed || this._generation.current !== generation) return false;\n\n\t\t\t\tawait this.refresh();\n\t\t\t\treturn true;\n\t\t\t})\n\t\t\t.finally(() => {\n\t\t\t\tif (this.revalidating === revalidation) {\n\t\t\t\t\tthis.revalidating = undefined;\n\t\t\t\t}\n\t\t\t});\n\t\tthis.revalidating = revalidation;\n\t\treturn revalidation;\n\t}\n\n\tprivate startLoad(): Promise<void> {\n\t\tconst load: Promise<void> = this.loadPatterns().finally(() => {\n\t\t\tif (this.inflight === load) {\n\t\t\t\tthis.inflight = undefined;\n\t\t\t}\n\t\t\tif (this.loading === load) {\n\t\t\t\tthis.loading = undefined;\n\t\t\t}\n\t\t});\n\t\tthis.inflight = load;\n\t\tthis.loading = load;\n\t\treturn load;\n\t}\n\n\t/**\n\t * Mirrors git's directory walk: each ancestor directory is checked shallow→deep, and the first one found ignored\n\t * ignores the whole path (git never descends into it, so nothing below can re-include it). At each level, the\n\t * matchers whose directory is a proper ancestor apply, and the deepest decisive one wins, falling back to the root.\n\t */\n\tprivate isIgnoredNested(path: string, nested: Map<string, Ignore>): boolean {\n\t\tconst matchers = this._scratchMatchers;\n\t\tconst bases = this._scratchBases;\n\n\t\ttry {\n\t\t\t// A trailing slash belongs to the path itself (a directory form), not to an ancestor\n\t\t\tconst last = path.length - 1;\n\n\t\t\tlet slash = path.indexOf('/');\n\t\t\twhile (slash !== -1 && slash < last) {\n\t\t\t\tif (this.isLevelIgnored(path, slash + 1, matchers, bases)) return true;\n\n\t\t\t\t// A directory's own `.gitignore` applies below it, never to the directory itself\n\t\t\t\tconst matcher = nested.get(path.slice(0, slash));\n\t\t\t\tif (matcher != null) {\n\t\t\t\t\tmatchers.push(matcher);\n\t\t\t\t\tbases.push(slash + 1);\n\t\t\t\t}\n\n\t\t\t\tslash = path.indexOf('/', slash + 1);\n\t\t\t}\n\n\t\t\treturn this.isLevelIgnored(path, path.length, matchers, bases);\n\t\t} finally {\n\t\t\tmatchers.length = 0;\n\t\t\tbases.length = 0;\n\t\t}\n\t}\n\n\t/** Whether `path.slice(0, end)` is ignored, given its ancestors aren't */\n\tprivate isLevelIgnored(path: string, end: number, matchers: readonly Ignore[], bases: readonly number[]): boolean {\n\t\tfor (let i = matchers.length - 1; i >= 0; i--) {\n\t\t\tconst verdict = this.testLevel(matchers[i], path.slice(bases[i], end));\n\t\t\tif (verdict != null) return verdict;\n\t\t}\n\n\t\treturn this.testLevel(this.ig, path.slice(0, end)) ?? false;\n\t}\n\n\t/** Returns the matcher's decisive verdict for `path`, or undefined when it has none */\n\tprivate testLevel(matcher: Ignore, path: string): boolean | undefined {\n\t\tconst result = matcher.test(path);\n\t\tif (result.unignored) return false;\n\t\tif (!result.ignored) return undefined;\n\n\t\t// `ignore` folds its own verdict for each ancestor into a path's result: once the matcher considers a parent\n\t\t// ignored, every child reports that parent's result. Reaching here means a deeper matcher re-included that\n\t\t// parent, so this result says nothing about the path itself — the matcher's own rules are unknowable, so\n\t\t// answer \"not ignored\" rather than risk dropping a real change.\n\t\tconst parentEnd = path.lastIndexOf('/', path.length - 2);\n\t\tif (parentEnd !== -1 && matcher.test(path.slice(0, parentEnd + 1)).ignored) return false;\n\n\t\treturn true;\n\t}\n\n\tprivate async loadPatterns(): Promise<void> {\n\t\tconst id = this._generation.next();\n\t\tconst stamps = new Map<string, string | undefined>();\n\n\t\tconst globalExcludesPath = this.resolveGlobalExcludesPath();\n\t\tconst infoExcludePath = this.resolveInfoExcludePath();\n\t\tconst [globalResult, infoResult, rootResult, nestedResult, ignoreCaseResult] = await Promise.allSettled([\n\t\t\tglobalExcludesPath.then(path => this.readSource(path, stamps)),\n\t\t\tinfoExcludePath.then(path => this.readSource(path, stamps)),\n\t\t\tthis.readSource(`${this.repoPath}/.gitignore`, stamps),\n\t\t\tthis.loadNested(stamps),\n\t\t\tthis.readIgnoreCase(),\n\t\t]);\n\t\tconst [globalPathResult, infoPathResult] = await Promise.allSettled([globalExcludesPath, infoExcludePath]);\n\n\t\t// Only commit results if no newer refresh has started\n\t\tif (id !== this._generation.current) return;\n\n\t\tconst options = { ignorecase: getSettledValue(ignoreCaseResult) ?? defaultIgnoreCase };\n\n\t\t// Added in git's precedence order (lowest first), since the last matching pattern wins\n\t\tconst ig = ignore(options);\n\t\tfor (const content of [\n\t\t\tgetSettledValue(globalResult),\n\t\t\tgetSettledValue(infoResult),\n\t\t\tgetSettledValue(rootResult),\n\t\t]) {\n\t\t\tif (!content) continue;\n\n\t\t\ttry {\n\t\t\t\tig.add(content);\n\t\t\t} catch {\n\t\t\t\t// A source `ignore` can't parse is skipped rather than failing the whole load\n\t\t\t}\n\t\t}\n\n\t\tlet nested: Map<string, Ignore> | undefined;\n\t\tconst nestedContents = getSettledValue(nestedResult);\n\t\tif (nestedContents?.size) {\n\t\t\tnested = new Map();\n\t\t\tfor (const [dir, content] of nestedContents) {\n\t\t\t\ttry {\n\t\t\t\t\tnested.set(dir, ignore(options).add(content));\n\t\t\t\t} catch {\n\t\t\t\t\t// A file `ignore` can't parse is skipped rather than failing the whole load\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tthis.ig = ig;\n\t\tthis.nested = nested?.size ? nested : undefined;\n\t\tthis.sources = {\n\t\t\tignoreCase: options.ignorecase,\n\t\t\tglobalExcludesPath: getSettledValue(globalPathResult),\n\t\t\tinfoExcludePath: getSettledValue(infoPathResult),\n\t\t\tstamps: stamps,\n\t\t};\n\t}\n\n\t/** Whether anything {@link loadPatterns} built `sources` from differs now; a source that can't be stat'ed has */\n\tprivate async sourcesChanged(sources: LoadedSources): Promise<boolean> {\n\t\tconst [globalExcludesPath, infoExcludePath, ignoreCase, unchanged] = await Promise.allSettled([\n\t\t\tthis.resolveGlobalExcludesPath(),\n\t\t\tthis.resolveInfoExcludePath(),\n\t\t\tthis.readIgnoreCase(),\n\t\t\tPromise.allSettled(\n\t\t\t\tArray.from(\n\t\t\t\t\tsources.stamps,\n\t\t\t\t\tasync ([path, stamp]) => stamp != null && stamp === (await this.stamp(path)),\n\t\t\t\t),\n\t\t\t),\n\t\t]);\n\n\t\treturn (\n\t\t\tgetSettledValue(globalExcludesPath) !== sources.globalExcludesPath ||\n\t\t\tgetSettledValue(infoExcludePath) !== sources.infoExcludePath ||\n\t\t\t(getSettledValue(ignoreCase) ?? defaultIgnoreCase) !== sources.ignoreCase ||\n\t\t\tgetSettledValue(unchanged)?.every(r => getSettledValue(r) === true) !== true\n\t\t);\n\t}\n\n\t/** Reads every nested `.gitignore`, keyed by its repo-relative directory (no trailing slash) */\n\tprivate async loadNested(stamps: Map<string, string | undefined>): Promise<Map<string, string> | undefined> {\n\t\tif (this.getGitIgnorePaths == null) return undefined;\n\n\t\tlet paths: readonly string[];\n\t\ttry {\n\t\t\tpaths = await this.getGitIgnorePaths();\n\t\t} catch {\n\t\t\t// Discovery failed — fall back to the root `.gitignore` alone\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst suffix = '/.gitignore';\n\t\tconst dirs = new Set<string>();\n\t\tfor (const path of paths) {\n\t\t\t// The root `.gitignore` (no directory) is loaded with the other root sources\n\t\t\tif (path.length <= suffix.length || !path.endsWith(suffix)) continue;\n\n\t\t\tdirs.add(path.slice(0, -suffix.length));\n\t\t}\n\t\tif (!dirs.size) return undefined;\n\n\t\tconst entries = await Promise.allSettled(\n\t\t\tArray.from(dirs, async dir => {\n\t\t\t\tconst content = await this.readSource(`${this.repoPath}/${dir}${suffix}`, stamps);\n\t\t\t\treturn content ? ([dir, content] as const) : undefined;\n\t\t\t}),\n\t\t);\n\n\t\tconst nested = new Map<string, string>();\n\t\tfor (const entry of entries) {\n\t\t\tconst value = getSettledValue(entry);\n\t\t\tif (value != null) {\n\t\t\t\tnested.set(value[0], value[1]);\n\t\t\t}\n\t\t}\n\t\treturn nested;\n\t}\n\n\t/** Stamps an ignore file, then reads it — stamped first, so an edit landing in between reads as a change later */\n\tprivate async readSource(\n\t\tpath: string | undefined,\n\t\tstamps: Map<string, string | undefined>,\n\t): Promise<string | undefined> {\n\t\tif (path == null) return undefined;\n\n\t\tstamps.set(path, await this.stamp(path));\n\t\treturn this.readFile(path);\n\t}\n\n\t/** `mtime:size`, `''` when the file is absent, or undefined when it can't be stat'ed */\n\tprivate async stamp(path: string): Promise<string | undefined> {\n\t\ttry {\n\t\t\tconst stat = await this.fs.stat(fileUri(path));\n\t\t\treturn stat != null ? `${stat.mtime}:${stat.size}` : '';\n\t\t} catch {\n\t\t\treturn undefined;\n\t\t}\n\t}\n\n\tprivate async readFile(path: string): Promise<string | undefined> {\n\t\ttry {\n\t\t\tconst data = await this.fs.readFile(fileUri(path));\n\t\t\treturn new TextDecoder().decode(data);\n\t\t} catch {\n\t\t\t// File might not exist or be unreadable — that's fine\n\t\t\treturn undefined;\n\t\t}\n\t}\n\n\tprivate async resolveInfoExcludePath(): Promise<string | undefined> {\n\t\tif (this.getInfoExcludePath == null) return `${this.gitDirPath}/info/exclude`;\n\n\t\ttry {\n\t\t\treturn await this.getInfoExcludePath();\n\t\t} catch {\n\t\t\t// The git dir couldn't be resolved\n\t\t\treturn undefined;\n\t\t}\n\t}\n\n\tprivate async readIgnoreCase(): Promise<boolean | undefined> {\n\t\tif (this.getIgnoreCase == null) return undefined;\n\n\t\ttry {\n\t\t\treturn await this.getIgnoreCase();\n\t\t} catch {\n\t\t\t// Unreadable — the platform default applies\n\t\t\treturn undefined;\n\t\t}\n\t}\n\n\tprivate async resolveGlobalExcludesPath(): Promise<string | undefined> {\n\t\tif (this.getGlobalExcludesPath == null) return undefined;\n\n\t\ttry {\n\t\t\treturn await this.getGlobalExcludesPath();\n\t\t} catch {\n\t\t\t// Global excludes might not be configured\n\t\t\treturn undefined;\n\t\t}\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -45,7 +45,8 @@ export interface FileWatcherOptions {
|
|
|
45
45
|
/**
|
|
46
46
|
* Called once, when a watch the provider places asynchronously goes live: whatever changed under `basePath` between
|
|
47
47
|
* `createWatcher` and then went unseen, so core rescans. A provider whose watch is live when `createWatcher` returns
|
|
48
|
-
*
|
|
48
|
+
* needn't call it, unless it declares `callsOnDidStart`: then it calls it inside `createWatcher`, which costs no
|
|
49
|
+
* rescan. Passed only to working-tree watchers, whether or not the host opted into polling.
|
|
49
50
|
*/
|
|
50
51
|
readonly onDidStart?: () => void;
|
|
51
52
|
}
|
|
@@ -66,6 +67,13 @@ export interface FileWatchingProvider {
|
|
|
66
67
|
* set for a provider that can use it.
|
|
67
68
|
*/
|
|
68
69
|
readonly supportsExcludes?: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* True when the provider calls `options.onDidStart` for every working-tree watch once it goes live (unless the watch
|
|
72
|
+
* reports `unwatchable` first). Core then leaves to that rescan what it would otherwise read at once (a newly
|
|
73
|
+
* subscribed worktree summary's dirty flag); without it, core can't tell a watch still starting from one that was
|
|
74
|
+
* live on return, since such a provider never calls `onDidStart`.
|
|
75
|
+
*/
|
|
76
|
+
readonly callsOnDidStart?: boolean;
|
|
69
77
|
}
|
|
70
78
|
//#endregion
|
|
71
79
|
//# sourceMappingURL=provider.d.ts.map
|
|
@@ -156,6 +156,11 @@ export declare class RepositoryWatchService implements UnifiedDisposable {
|
|
|
156
156
|
private readonly sessionMap;
|
|
157
157
|
private readonly watchGroups;
|
|
158
158
|
private readonly wtWatchers;
|
|
159
|
+
/**
|
|
160
|
+
* The last git-ignored directory read under each filter's current rules, kept across a working-tree watcher's
|
|
161
|
+
* stop and start so a restart can skip the read; dropped with the filter
|
|
162
|
+
*/
|
|
163
|
+
private readonly keptIgnoredDirectories;
|
|
159
164
|
private _disposed;
|
|
160
165
|
/** Global suspend state (driven by window focus); inherited by sessions created via {@link watch}. */
|
|
161
166
|
private _suspended;
|
|
@@ -186,6 +191,11 @@ export declare class RepositoryWatchService implements UnifiedDisposable {
|
|
|
186
191
|
}): void;
|
|
187
192
|
/** Get an existing session by repo path */
|
|
188
193
|
getSession(repoPath: string): RepositoryWatchSession | undefined;
|
|
194
|
+
/**
|
|
195
|
+
* Whether the working tree's watch has yet to go live, owing a rescan once it does (see
|
|
196
|
+
* `FileWatchingProvider.callsOnDidStart`). Not while it's `unwatchable`: it rescans when it comes back.
|
|
197
|
+
*/
|
|
198
|
+
isWorkingTreeStarting(repoPath: string): boolean;
|
|
189
199
|
/**
|
|
190
200
|
* `'poll'` while a watcher the repository is watched through — its `.git` watchers or its working tree's — can't
|
|
191
201
|
* watch, and is polled instead; `'events'` while it is watched natively (polled beside an `unreliable` watcher
|
|
@@ -206,7 +216,10 @@ export declare class RepositoryWatchService implements UnifiedDisposable {
|
|
|
206
216
|
private updateWatchMode;
|
|
207
217
|
private startWorkingTreeWatcher;
|
|
208
218
|
private stopWorkingTreeWatcher;
|
|
209
|
-
/**
|
|
219
|
+
/**
|
|
220
|
+
* Creates a working-tree watcher, with the reliability it reported before returning, and whether it has yet to call
|
|
221
|
+
* the `onDidStart` its provider promised
|
|
222
|
+
*/
|
|
210
223
|
private createWorkingTreeWatcher;
|
|
211
224
|
private setWorkingTreeReliability;
|
|
212
225
|
/** Polls the host's working-tree fingerprint, when it gives one, while the watcher isn't `reliable` */
|
|
@@ -70,6 +70,11 @@ var RepositoryWatchService = class {
|
|
|
70
70
|
sessionMap = /* @__PURE__ */ new Map();
|
|
71
71
|
watchGroups = /* @__PURE__ */ new Map();
|
|
72
72
|
wtWatchers = /* @__PURE__ */ new Map();
|
|
73
|
+
/**
|
|
74
|
+
* The last git-ignored directory read under each filter's current rules, kept across a working-tree watcher's
|
|
75
|
+
* stop and start so a restart can skip the read; dropped with the filter
|
|
76
|
+
*/
|
|
77
|
+
keptIgnoredDirectories = /* @__PURE__ */ new WeakMap();
|
|
73
78
|
_disposed = false;
|
|
74
79
|
/** Global suspend state (driven by window focus); inherited by sessions created via {@link watch}. */
|
|
75
80
|
_suspended = false;
|
|
@@ -201,6 +206,14 @@ var RepositoryWatchService = class {
|
|
|
201
206
|
return this.sessionMap.get(getRepositoryKey(repoPath))?.session;
|
|
202
207
|
}
|
|
203
208
|
/**
|
|
209
|
+
* Whether the working tree's watch has yet to go live, owing a rescan once it does (see
|
|
210
|
+
* `FileWatchingProvider.callsOnDidStart`). Not while it's `unwatchable`: it rescans when it comes back.
|
|
211
|
+
*/
|
|
212
|
+
isWorkingTreeStarting(repoPath) {
|
|
213
|
+
const entry = this.wtWatchers.get(getRepositoryKey(repoPath));
|
|
214
|
+
return entry?.starting === true && entry.reliability !== "unwatchable";
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
204
217
|
* `'poll'` while a watcher the repository is watched through — its `.git` watchers or its working tree's — can't
|
|
205
218
|
* watch, and is polled instead; `'events'` while it is watched natively (polled beside an `unreliable` watcher
|
|
206
219
|
* included); `undefined` when nothing watches it, which {@link onDidChangeWatchMode} doesn't announce.
|
|
@@ -318,6 +331,7 @@ var RepositoryWatchService = class {
|
|
|
318
331
|
let filterReady = filter == null;
|
|
319
332
|
let buffered = filter != null ? [] : void 0;
|
|
320
333
|
let bufferIncomplete = false;
|
|
334
|
+
const revalidated = filter?.revalidate();
|
|
321
335
|
if (filter != null) filter.ready().then(() => {
|
|
322
336
|
filterReady = true;
|
|
323
337
|
if (buffered?.length) {
|
|
@@ -330,8 +344,8 @@ var RepositoryWatchService = class {
|
|
|
330
344
|
filterReady = true;
|
|
331
345
|
buffered = void 0;
|
|
332
346
|
});
|
|
333
|
-
const onIgnoreRulesChanged = () => {
|
|
334
|
-
if (filter != null) filter.refresh();
|
|
347
|
+
const onIgnoreRulesChanged = (refreshFilter) => {
|
|
348
|
+
if (refreshFilter && filter != null) filter.refresh();
|
|
335
349
|
if (entry != null) this.scheduleIgnoredDirectoriesRefresh(entry);
|
|
336
350
|
for (const h of watchHooks) h.onGitIgnoreChanged?.(repoPath);
|
|
337
351
|
session.pushUnfencedRepoChanges(["ignores"]);
|
|
@@ -339,16 +353,16 @@ var RepositoryWatchService = class {
|
|
|
339
353
|
const onEvent = (event) => {
|
|
340
354
|
if (event.reason === "overflow") {
|
|
341
355
|
session.pushWorkingTreeChanges([], true);
|
|
342
|
-
onIgnoreRulesChanged();
|
|
356
|
+
onIgnoreRulesChanged(true);
|
|
343
357
|
return;
|
|
344
358
|
}
|
|
345
359
|
if (shouldIgnoreWorkingTreePath(event.path)) return;
|
|
346
360
|
const relativePath = isDescendant(event.path, repoPath) ? relative(repoPath, event.path) : void 0;
|
|
347
361
|
if (relativePath === ".gitignore") {
|
|
348
|
-
onIgnoreRulesChanged();
|
|
362
|
+
onIgnoreRulesChanged(true);
|
|
349
363
|
return;
|
|
350
364
|
}
|
|
351
|
-
if (relativePath?.endsWith("/.gitignore") && !(filterReady && filter?.isIgnored(relativePath))) onIgnoreRulesChanged();
|
|
365
|
+
if (relativePath?.endsWith("/.gitignore") && !(filterReady && filter?.isIgnored(relativePath))) onIgnoreRulesChanged(true);
|
|
352
366
|
if (!filterReady) {
|
|
353
367
|
if (buffered != null && buffered.length >= maxBufferedWorkingTreePaths) {
|
|
354
368
|
bufferIncomplete = true;
|
|
@@ -363,21 +377,31 @@ var RepositoryWatchService = class {
|
|
|
363
377
|
}
|
|
364
378
|
session.pushWorkingTreeChanges([event.path]);
|
|
365
379
|
};
|
|
366
|
-
const
|
|
380
|
+
const kept = canPrune && filter != null ? this.keptIgnoredDirectories.get(filter) : void 0;
|
|
381
|
+
const created = this.createWorkingTreeWatcher(repoPath, onEvent, kept != null ? [...workingTreeExcludes, ...kept] : workingTreeExcludes);
|
|
367
382
|
entry = {
|
|
368
383
|
session,
|
|
369
384
|
onEvent,
|
|
370
385
|
filter,
|
|
371
386
|
watcher: created.watcher,
|
|
372
|
-
ignoredDirectories: [],
|
|
387
|
+
ignoredDirectories: kept ?? [],
|
|
373
388
|
generation: 0,
|
|
374
389
|
reading: false,
|
|
375
390
|
rereadQueued: false,
|
|
376
|
-
reliability: "reliable"
|
|
391
|
+
reliability: "reliable",
|
|
392
|
+
starting: created.starting
|
|
377
393
|
};
|
|
378
394
|
this.wtWatchers.set(repoPath, entry);
|
|
379
395
|
this.setWorkingTreeReliability(entry, created.reliability, true);
|
|
380
|
-
if (canPrune) this.refreshIgnoredDirectories(entry, repoPath);
|
|
396
|
+
if (canPrune && kept == null) this.refreshIgnoredDirectories(entry, repoPath);
|
|
397
|
+
revalidated?.then((changed) => {
|
|
398
|
+
if (!changed) return;
|
|
399
|
+
if (filter != null) this.keptIgnoredDirectories.delete(filter);
|
|
400
|
+
if (this.wtWatchers.get(repoPath) === entry) onIgnoreRulesChanged(false);
|
|
401
|
+
}, () => {
|
|
402
|
+
if (filter != null) this.keptIgnoredDirectories.delete(filter);
|
|
403
|
+
if (this.wtWatchers.get(repoPath) === entry) onIgnoreRulesChanged(true);
|
|
404
|
+
});
|
|
381
405
|
}
|
|
382
406
|
stopWorkingTreeWatcher(repoPath) {
|
|
383
407
|
const entry = this.wtWatchers.get(repoPath);
|
|
@@ -392,15 +416,22 @@ var RepositoryWatchService = class {
|
|
|
392
416
|
}
|
|
393
417
|
this.wtWatchers.delete(repoPath);
|
|
394
418
|
}
|
|
395
|
-
/**
|
|
419
|
+
/**
|
|
420
|
+
* Creates a working-tree watcher, with the reliability it reported before returning, and whether it has yet to call
|
|
421
|
+
* the `onDidStart` its provider promised
|
|
422
|
+
*/
|
|
396
423
|
createWorkingTreeWatcher(repoPath, onEvent, excludes) {
|
|
397
424
|
let watcher;
|
|
398
425
|
let started = false;
|
|
399
426
|
const onDidStart = () => {
|
|
400
|
-
if (started
|
|
427
|
+
if (started) return;
|
|
401
428
|
started = true;
|
|
429
|
+
if (watcher == null) return;
|
|
402
430
|
const entry = this.wtWatchers.get(repoPath);
|
|
403
|
-
if (entry?.watcher === watcher)
|
|
431
|
+
if (entry?.watcher === watcher) {
|
|
432
|
+
entry.starting = false;
|
|
433
|
+
entry.session.pushWorkingTreeChanges([], true);
|
|
434
|
+
}
|
|
404
435
|
};
|
|
405
436
|
if (this.poller == null) {
|
|
406
437
|
watcher = this.provider.createWatcher(repoPath, "**", onEvent, {
|
|
@@ -409,7 +440,8 @@ var RepositoryWatchService = class {
|
|
|
409
440
|
});
|
|
410
441
|
return {
|
|
411
442
|
watcher,
|
|
412
|
-
reliability: "reliable"
|
|
443
|
+
reliability: "reliable",
|
|
444
|
+
starting: this.provider.callsOnDidStart === true && !started
|
|
413
445
|
};
|
|
414
446
|
}
|
|
415
447
|
let reliability = "reliable";
|
|
@@ -428,7 +460,8 @@ var RepositoryWatchService = class {
|
|
|
428
460
|
watcher = this.provider.createWatcher(repoPath, "**", onEvent, options);
|
|
429
461
|
return {
|
|
430
462
|
watcher,
|
|
431
|
-
reliability
|
|
463
|
+
reliability,
|
|
464
|
+
starting: this.provider.callsOnDidStart === true && !started
|
|
432
465
|
};
|
|
433
466
|
}
|
|
434
467
|
setWorkingTreeReliability(entry, state, initial) {
|
|
@@ -437,7 +470,10 @@ var RepositoryWatchService = class {
|
|
|
437
470
|
entry.reliability = state;
|
|
438
471
|
this.updateWorkingTreePollTarget(entry);
|
|
439
472
|
if (state === "unwatchable" === (previous === "unwatchable")) return;
|
|
440
|
-
if (!initial)
|
|
473
|
+
if (!initial) {
|
|
474
|
+
entry.starting = false;
|
|
475
|
+
entry.session.pushWorkingTreeChanges([], true);
|
|
476
|
+
}
|
|
441
477
|
const record = this.sessionMap.get(entry.session.repoPath);
|
|
442
478
|
if (record != null) this.updateWatchMode(record);
|
|
443
479
|
}
|
|
@@ -501,6 +537,7 @@ var RepositoryWatchService = class {
|
|
|
501
537
|
}
|
|
502
538
|
/** Re-reads the git-ignored directories once ignore rules stop changing (trailing debounce, one timer per repo) */
|
|
503
539
|
scheduleIgnoredDirectoriesRefresh(entry) {
|
|
540
|
+
if (entry.filter != null) this.keptIgnoredDirectories.delete(entry.filter);
|
|
504
541
|
if (!this.canPruneIgnoredDirectories()) return;
|
|
505
542
|
entry.generation++;
|
|
506
543
|
entry.unprunable = void 0;
|
|
@@ -533,7 +570,10 @@ var RepositoryWatchService = class {
|
|
|
533
570
|
directories = void 0;
|
|
534
571
|
}
|
|
535
572
|
if (entry.generation === generation) {
|
|
536
|
-
if (directories != null)
|
|
573
|
+
if (directories != null) {
|
|
574
|
+
this.swapWorkingTreeWatcher(entry, repoPath, directories);
|
|
575
|
+
if (entry.filter != null) this.keptIgnoredDirectories.set(entry.filter, directories);
|
|
576
|
+
}
|
|
537
577
|
if (unpruned != null) this.settleUnprunedDirectories(entry, unpruned, directories);
|
|
538
578
|
}
|
|
539
579
|
} finally {
|
|
@@ -567,6 +607,7 @@ var RepositoryWatchService = class {
|
|
|
567
607
|
}
|
|
568
608
|
const previousWatcher = entry.watcher;
|
|
569
609
|
entry.watcher = created.watcher;
|
|
610
|
+
entry.starting = created.starting;
|
|
570
611
|
entry.ignoredDirectories = directories;
|
|
571
612
|
previousWatcher.dispose();
|
|
572
613
|
this.setWorkingTreeReliability(entry, created.reliability);
|