@gitkraken/core-gitlens 0.16.5 → 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 +35 -1
- package/dist/git/cache.d.ts +15 -2
- package/dist/git/cache.js +36 -9
- package/dist/git/cache.js.map +1 -1
- package/dist/git/models/repository.d.ts +4 -2
- package/dist/git/models/repository.js +1 -0
- package/dist/git/models/repository.js.map +1 -1
- package/dist/git/models/worktree.d.ts +5 -1
- package/dist/git/models/worktree.js +5 -3
- package/dist/git/models/worktree.js.map +1 -1
- package/dist/git/providers/provider.d.ts +17 -0
- package/dist/git/providers/worktrees.d.ts +17 -2
- package/dist/git/run.types.d.ts +7 -0
- package/dist/git/service.d.ts +5 -2
- package/dist/git/service.js +11 -5
- package/dist/git/service.js.map +1 -1
- package/dist/git/watching/classifyChange.d.ts +4 -3
- package/dist/git/watching/classifyChange.js +7 -4
- package/dist/git/watching/classifyChange.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 +14 -0
- package/dist/git/watching/watchGroup.d.ts +142 -3
- package/dist/git/watching/watchGroup.js +424 -19
- package/dist/git/watching/watchGroup.js.map +1 -1
- package/dist/git/watching/watchService.d.ts +39 -1
- package/dist/git/watching/watchService.js +81 -20
- package/dist/git/watching/watchService.js.map +1 -1
- package/dist/git/watching/watchSession.d.ts +13 -8
- package/dist/git/watching/watchSession.js +50 -10
- package/dist/git/watching/watchSession.js.map +1 -1
- package/dist/git/watching/watcherPatterns.d.ts +14 -0
- package/dist/git/watching/watcherPatterns.js +38 -1
- package/dist/git/watching/watcherPatterns.js.map +1 -1
- package/dist/git/watching/worktreeSummary.d.ts +97 -7
- package/dist/git/watching/worktreeSummary.js +454 -27
- package/dist/git/watching/worktreeSummary.js.map +1 -1
- package/dist/git-cli/cliGitProvider.d.ts +26 -0
- package/dist/git-cli/cliGitProvider.js +70 -9
- package/dist/git-cli/cliGitProvider.js.map +1 -1
- package/dist/git-cli/exec/commandClassifier.d.ts +9 -3
- package/dist/git-cli/exec/commandClassifier.js +98 -17
- package/dist/git-cli/exec/commandClassifier.js.map +1 -1
- package/dist/git-cli/exec/git.d.ts +20 -0
- package/dist/git-cli/exec/git.js +69 -7
- package/dist/git-cli/exec/git.js.map +1 -1
- package/dist/git-cli/exec/headState.d.ts +5 -0
- package/dist/git-cli/exec/headState.js +14 -1
- package/dist/git-cli/exec/headState.js.map +1 -1
- package/dist/git-cli/parsers/worktreeParser.js +2 -2
- package/dist/git-cli/parsers/worktreeParser.js.map +1 -1
- package/dist/git-cli/providers/branches.js +9 -36
- 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/git-cli/providers/graph.js +1 -1
- package/dist/git-cli/providers/graph.js.map +1 -1
- package/dist/git-cli/providers/operations.d.ts +3 -0
- package/dist/git-cli/providers/operations.js +14 -5
- package/dist/git-cli/providers/operations.js.map +1 -1
- package/dist/git-cli/providers/worktrees.d.ts +25 -3
- package/dist/git-cli/providers/worktrees.js +125 -32
- package/dist/git-cli/providers/worktrees.js.map +1 -1
- package/dist/git-cli/service.d.ts +2 -0
- package/dist/git-cli/service.js +4 -2
- package/dist/git-cli/service.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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"}
|
|
@@ -42,6 +42,13 @@ export interface FileWatcherOptions {
|
|
|
42
42
|
* already down reports `unwatchable` itself. Going from `unwatchable` to anything else makes core rescan.
|
|
43
43
|
*/
|
|
44
44
|
readonly onDidChangeReliability?: (state: FileWatcherReliability) => void;
|
|
45
|
+
/**
|
|
46
|
+
* Called once, when a watch the provider places asynchronously goes live: whatever changed under `basePath` between
|
|
47
|
+
* `createWatcher` and then went unseen, so core rescans. A provider whose watch is live when `createWatcher` returns
|
|
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.
|
|
50
|
+
*/
|
|
51
|
+
readonly onDidStart?: () => void;
|
|
45
52
|
}
|
|
46
53
|
/**
|
|
47
54
|
* Factory for creating filesystem watchers. The library calls this
|
|
@@ -60,6 +67,13 @@ export interface FileWatchingProvider {
|
|
|
60
67
|
* set for a provider that can use it.
|
|
61
68
|
*/
|
|
62
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;
|
|
63
77
|
}
|
|
64
78
|
//#endregion
|
|
65
79
|
//# sourceMappingURL=provider.d.ts.map
|
|
@@ -61,14 +61,51 @@ interface SessionEntry {
|
|
|
61
61
|
* starts, is the baseline and delivers nothing; a report arriving before it completes may describe a change it
|
|
62
62
|
* already holds, so it is delivered unconfined. A read that fails or doesn't settle within a few seconds, or a file
|
|
63
63
|
* git couldn't parse, is unconfined too.
|
|
64
|
+
*
|
|
65
|
+
* Given a `readFile`, a `readDirectory` (a directory's entries with their types, only the directories counting as
|
|
66
|
+
* admin entries) and the folders whose worktrees the host has core ignore (a tool's throwaway worktrees, which no
|
|
67
|
+
* host shows), the group reports no change of an ignored worktree's admin entry
|
|
68
|
+
* (`<common>/worktrees/<id>`) to any session. Only registered folders count, since a user may keep real worktrees
|
|
69
|
+
* anywhere, `.git` included. It lists the entries and reads each one's `gitdir` once as it starts, and again after a
|
|
70
|
+
* watcher dropped events; a report arriving meanwhile is told at once, and decided once the listing settles, so what
|
|
71
|
+
* is known of its entry catches up. From then on it reads an entry only when its `gitdir` may be new (the entry was
|
|
72
|
+
* created, or its `gitdir` written, or it was removed before), so a real worktree's HEAD moves are told at once,
|
|
73
|
+
* unread. Of two reads of one entry, the later decides both reports. An entry it doesn't know whose `gitdir` can't be
|
|
74
|
+
* read (a watcher can report one before git writes it, and miss the write) is read once more a moment later, its
|
|
75
|
+
* reports held until then, and told if it still can't be. Only the entry's directory or its `gitdir` going is a
|
|
76
|
+
* removal; a deleted `HEAD` (a watcher may report git's lock file renamed over it as a delete) or `locked` is told as
|
|
77
|
+
* the entry is known. Removing an entry deletes the `gitdir` a read needs, so a removal is told from what the entry was
|
|
78
|
+
* known to be (before a read that couldn't read it, if one is waiting to read it again), and an entry it never knew is
|
|
79
|
+
* told as real. Any other report of a removed entry but a delete is read, and one that can't be read is the removal's
|
|
80
|
+
* own (a watcher may report the entry's directory changed beside each file it deletes), unless it creates the entry
|
|
81
|
+
* again. A report of the `worktrees` directory itself (a coalesced
|
|
82
|
+
* removal, a parent directory change, a poll, which sees nothing finer) lists it again: it is told to every session
|
|
83
|
+
* when a real entry was added or removed since the last listing, and to none when only ignored ones were. Git removes
|
|
84
|
+
* the directory with its last entry, so a directory that isn't there is an empty listing, as is a removal of it that
|
|
85
|
+
* can't be listed. Since a listing (or a worktree moved into or out of a folder) changes what every session shows, it
|
|
86
|
+
* is told to all of them; the other watcher's report of the same change then finds nothing left to tell. Where only a
|
|
87
|
+
* poll watches the common dir, it sees just the `worktrees` directory, so an entry removed and added again under the
|
|
88
|
+
* same id between two polls, or an entry's HEAD or lock changed in place, goes untold.
|
|
89
|
+
*
|
|
90
|
+
* Given the folders, the watchers over the common dir leave each one unwatched, since a tool's checkout there would
|
|
91
|
+
* otherwise have every directory of it watched; one that shares a top-level directory with what the `.git` watch reads
|
|
92
|
+
* stays watched.
|
|
64
93
|
*/
|
|
65
94
|
export declare class WatchGroup implements UnifiedDisposable {
|
|
66
95
|
readonly commonGitDir: string;
|
|
67
96
|
private readonly provider;
|
|
68
97
|
/** When given, a watcher that reports it can't be trusted is polled; otherwise none is asked */
|
|
69
98
|
private readonly poller?;
|
|
70
|
-
/**
|
|
99
|
+
/**
|
|
100
|
+
* When given, reports of the shared `.git/config` are classified by what changed in it, and admin entries are
|
|
101
|
+
* read to tell an ignored worktree's (see above)
|
|
102
|
+
*/
|
|
71
103
|
private readonly readFile?;
|
|
104
|
+
/**
|
|
105
|
+
* Returns a directory's entries with their types; with `readFile`, the admin entries are listed, only the
|
|
106
|
+
* directories counting (see above)
|
|
107
|
+
*/
|
|
108
|
+
private readonly readDirectory?;
|
|
72
109
|
private readonly _sessions;
|
|
73
110
|
private _commonWatcher;
|
|
74
111
|
private _commonReliability;
|
|
@@ -80,11 +117,52 @@ export declare class WatchGroup implements UnifiedDisposable {
|
|
|
80
117
|
private _configBaselining;
|
|
81
118
|
private _configReading;
|
|
82
119
|
private _configRereadQueued;
|
|
120
|
+
/**
|
|
121
|
+
* Settles once the latest baseline of the admin entries was taken; `undefined` when the group can't tell an
|
|
122
|
+
* ignored worktree's, so every report is told at once
|
|
123
|
+
*/
|
|
124
|
+
private _worktreesBaseline;
|
|
125
|
+
/** A baseline is pending, so a report waits for it */
|
|
126
|
+
private _worktreesBaselining;
|
|
127
|
+
private _worktreesRebaselineQueued;
|
|
128
|
+
/** Set by the first baseline; while empty, nothing is ignored and every report is told at once */
|
|
129
|
+
private _ignoredWorktreeRoots;
|
|
130
|
+
/** Listings run one at a time, so each compares with the one before it */
|
|
131
|
+
private _worktreesQueue;
|
|
132
|
+
/** The `worktrees` directory's reports waiting on the queued listing; `undefined` while none is queued */
|
|
133
|
+
private _worktreesRescanQueued;
|
|
134
|
+
/** The admin entries the last listing found */
|
|
135
|
+
private _listedWorktreeIds;
|
|
136
|
+
/** Admin entries known to exist, by id: whether each is an ignored worktree's */
|
|
137
|
+
private readonly _worktreeIds;
|
|
138
|
+
/** What removed admin entries were, by id, oldest first */
|
|
139
|
+
private readonly _removedWorktreeIds;
|
|
140
|
+
/**
|
|
141
|
+
* Each id's latest `gitdir` read, settling with what it found and what the id was known to be before it; only it
|
|
142
|
+
* may update what is known of the id. A removal of the entry drops it
|
|
143
|
+
*/
|
|
144
|
+
private readonly _worktreeReads;
|
|
145
|
+
/** Admin entries whose `gitdir` couldn't be read, by id, waiting to be read once more */
|
|
146
|
+
private readonly _worktreeRetries;
|
|
147
|
+
/** The `.git` excludes of a watcher over the common git dir, with the ignored worktrees' folders it may leave out */
|
|
148
|
+
private readonly _commonGitDirExcludes;
|
|
83
149
|
constructor(commonGitDir: string, provider: FileWatchingProvider,
|
|
84
150
|
/** When given, a watcher that reports it can't be trusted is polled; otherwise none is asked */
|
|
85
151
|
poller?: GitDirPoller | undefined,
|
|
86
|
-
/**
|
|
87
|
-
|
|
152
|
+
/**
|
|
153
|
+
* When given, reports of the shared `.git/config` are classified by what changed in it, and admin entries are
|
|
154
|
+
* read to tell an ignored worktree's (see above)
|
|
155
|
+
*/
|
|
156
|
+
readFile?: ((path: string) => Promise<Uint8Array>) | undefined,
|
|
157
|
+
/**
|
|
158
|
+
* Returns a directory's entries with their types; with `readFile`, the admin entries are listed, only the
|
|
159
|
+
* directories counting (see above)
|
|
160
|
+
*/
|
|
161
|
+
readDirectory?: ((path: string) => Promise<readonly (readonly [string, number])[]>) | undefined,
|
|
162
|
+
/** The absolute, normalized folders whose worktrees are ignored; none reads no admin entry */
|
|
163
|
+
ignoredWorktreeRoots?: Promise<readonly string[]>,
|
|
164
|
+
/** The folders whose worktrees are ignored, relative to the common git dir */
|
|
165
|
+
ignoredWorktreeFolders?: readonly string[]);
|
|
88
166
|
[Symbol.dispose](): void;
|
|
89
167
|
/** Dispose the entire group (common watcher + all root watchers). */
|
|
90
168
|
dispose(): void;
|
|
@@ -100,6 +178,12 @@ export declare class WatchGroup implements UnifiedDisposable {
|
|
|
100
178
|
private disposeCommonWatcher;
|
|
101
179
|
/** Hosts that didn't opt into polling get exactly the options they always had */
|
|
102
180
|
private getWatcherOptions;
|
|
181
|
+
/**
|
|
182
|
+
* The ignored worktrees' folders join the `.git` excludes of a watcher over the common git dir: a tool that keeps its
|
|
183
|
+
* throwaway checkouts inside the git dir would otherwise have each one's every directory watched, and every file it
|
|
184
|
+
* writes reported. Every other watcher sits on a linked worktree's admin entry, which holds none of them
|
|
185
|
+
*/
|
|
186
|
+
private getExcludes;
|
|
103
187
|
private setRootReliability;
|
|
104
188
|
private setCommonReliability;
|
|
105
189
|
private stopPolling;
|
|
@@ -119,12 +203,67 @@ export declare class WatchGroup implements UnifiedDisposable {
|
|
|
119
203
|
private onCommonEvent;
|
|
120
204
|
private deliverOverflow;
|
|
121
205
|
private onRootEvent;
|
|
206
|
+
/** A report of the `worktrees` directory, an admin entry or a file in it, once the group may tell them apart */
|
|
207
|
+
private isWorktreesReport;
|
|
208
|
+
/** `deliver` tells the sessions the report reaches of it, should the report concern them (see above) */
|
|
209
|
+
private onWorktreesReported;
|
|
210
|
+
private decideWorktreesReport;
|
|
211
|
+
/**
|
|
212
|
+
* Reads the entry's `gitdir`: whether it names an ignored worktree (`undefined` when it can't be read), and what the
|
|
213
|
+
* id was known to be when this read replaced it. Reads of one id can settle out of order (a slow one from before the
|
|
214
|
+
* worktree moved out of the folder), so only the latest updates what is known of it; an earlier one settles with
|
|
215
|
+
* what the latest finds, without what the id was, since the latest tells a move itself, or once the entry was removed
|
|
216
|
+
* since, with what the removal found it was. One that can't read the entry forgets what it was known to be, since git
|
|
217
|
+
* may be writing its `gitdir` again, and reads it once more a moment later, unless `retry` is false (it then leaves
|
|
218
|
+
* what is known as it was); one that can settles that read, if pending.
|
|
219
|
+
*/
|
|
220
|
+
private readWorktreeId;
|
|
221
|
+
/** Reads the entry once more a moment later, unless that is already pending; `previous` is what it was known to be */
|
|
222
|
+
private scheduleWorktreeRetry;
|
|
223
|
+
/**
|
|
224
|
+
* A report whose read couldn't read the entry waits on the entry's next read, and `deliver` is told then, unless that
|
|
225
|
+
* read finds an ignored worktree's; one still unreadable is told. Both watchers report each change of an entry, so
|
|
226
|
+
* the reports of one id wait on one read. Resolves with what it found, as {@link settleWorktreeRetry} does
|
|
227
|
+
*/
|
|
228
|
+
private waitForWorktreeRetry;
|
|
229
|
+
/**
|
|
230
|
+
* Tells the reports waiting on the id's retry what the entry turned out to be: nothing when an ignored worktree's,
|
|
231
|
+
* every session when it moved into or out of an ignored folder, and the reports' own sessions otherwise, `ignored`
|
|
232
|
+
* being `undefined` when it still couldn't be read
|
|
233
|
+
*/
|
|
234
|
+
private settleWorktreeRetry;
|
|
235
|
+
/**
|
|
236
|
+
* Takes `id` as removed, remembering what it was for the removal's other reports; returns that, or `undefined` when
|
|
237
|
+
* the id was never known
|
|
238
|
+
*/
|
|
239
|
+
private forgetWorktreeId;
|
|
240
|
+
/** Whether the entry's `gitdir` names an ignored worktree, or `undefined` when it can't be read. Never rejects */
|
|
241
|
+
private readIgnoredWorktree;
|
|
242
|
+
private queueWorktreesBaseline;
|
|
243
|
+
/** A watcher of the `worktrees` directory dropped events: whatever changed meanwhile went unseen */
|
|
244
|
+
private rebaselineWorktrees;
|
|
245
|
+
/** The watchers' reports of the directory pile onto one queued listing, which tells them apart */
|
|
246
|
+
private queueWorktreesRescan;
|
|
247
|
+
/**
|
|
248
|
+
* Lists the admin entries and compares them with the last listing. A baseline (or a rebaseline, after a watcher
|
|
249
|
+
* dropped events) reads every entry; a rescan only those it doesn't know, reading one it can't read once more a
|
|
250
|
+
* moment later. Git removes the directory with its last entry, so a directory that isn't there is an empty listing,
|
|
251
|
+
* whatever the reports said of it (a poll sees its removal as a change). A listing that fails otherwise is a
|
|
252
|
+
* baseline's empty directory, and a rescan's too when one of its reports (`delivers`) removed the directory. For any
|
|
253
|
+
* other rescan, it is a change it can't rule out, told to the reports' own sessions
|
|
254
|
+
*/
|
|
255
|
+
private listWorktrees;
|
|
256
|
+
private deliverWorktreesChange;
|
|
122
257
|
private onConfigReported;
|
|
123
258
|
private readConfigBaseline;
|
|
124
259
|
/** Reads and compares until no report came in during the last read: it may describe a change the read missed */
|
|
125
260
|
private readConfigChanges;
|
|
126
261
|
/** `undefined` when the file couldn't be read or parsed, or the read didn't settle in time */
|
|
127
262
|
private readConfig;
|
|
263
|
+
/** `undefined` when the file couldn't be read, or the read didn't settle in time */
|
|
264
|
+
private readWithTimeout;
|
|
265
|
+
/** `undefined` when `read` rejects, or doesn't settle in time */
|
|
266
|
+
private withTimeout;
|
|
128
267
|
/**
|
|
129
268
|
* Tells every session of a change to the shared `.git/config`: confined to some branches' sections, or (`undefined`,
|
|
130
269
|
* or a change outside them) anywhere, which may have changed a remote's settings too. Either way no ref moved
|