@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.
Files changed (71) hide show
  1. package/CHANGELOG.md +35 -1
  2. package/dist/git/cache.d.ts +15 -2
  3. package/dist/git/cache.js +36 -9
  4. package/dist/git/cache.js.map +1 -1
  5. package/dist/git/models/repository.d.ts +4 -2
  6. package/dist/git/models/repository.js +1 -0
  7. package/dist/git/models/repository.js.map +1 -1
  8. package/dist/git/models/worktree.d.ts +5 -1
  9. package/dist/git/models/worktree.js +5 -3
  10. package/dist/git/models/worktree.js.map +1 -1
  11. package/dist/git/providers/provider.d.ts +17 -0
  12. package/dist/git/providers/worktrees.d.ts +17 -2
  13. package/dist/git/run.types.d.ts +7 -0
  14. package/dist/git/service.d.ts +5 -2
  15. package/dist/git/service.js +11 -5
  16. package/dist/git/service.js.map +1 -1
  17. package/dist/git/watching/classifyChange.d.ts +4 -3
  18. package/dist/git/watching/classifyChange.js +7 -4
  19. package/dist/git/watching/classifyChange.js.map +1 -1
  20. package/dist/git/watching/gitIgnoreFilter.d.ts +21 -3
  21. package/dist/git/watching/gitIgnoreFilter.js +74 -14
  22. package/dist/git/watching/gitIgnoreFilter.js.map +1 -1
  23. package/dist/git/watching/provider.d.ts +14 -0
  24. package/dist/git/watching/watchGroup.d.ts +142 -3
  25. package/dist/git/watching/watchGroup.js +424 -19
  26. package/dist/git/watching/watchGroup.js.map +1 -1
  27. package/dist/git/watching/watchService.d.ts +39 -1
  28. package/dist/git/watching/watchService.js +81 -20
  29. package/dist/git/watching/watchService.js.map +1 -1
  30. package/dist/git/watching/watchSession.d.ts +13 -8
  31. package/dist/git/watching/watchSession.js +50 -10
  32. package/dist/git/watching/watchSession.js.map +1 -1
  33. package/dist/git/watching/watcherPatterns.d.ts +14 -0
  34. package/dist/git/watching/watcherPatterns.js +38 -1
  35. package/dist/git/watching/watcherPatterns.js.map +1 -1
  36. package/dist/git/watching/worktreeSummary.d.ts +97 -7
  37. package/dist/git/watching/worktreeSummary.js +454 -27
  38. package/dist/git/watching/worktreeSummary.js.map +1 -1
  39. package/dist/git-cli/cliGitProvider.d.ts +26 -0
  40. package/dist/git-cli/cliGitProvider.js +70 -9
  41. package/dist/git-cli/cliGitProvider.js.map +1 -1
  42. package/dist/git-cli/exec/commandClassifier.d.ts +9 -3
  43. package/dist/git-cli/exec/commandClassifier.js +98 -17
  44. package/dist/git-cli/exec/commandClassifier.js.map +1 -1
  45. package/dist/git-cli/exec/git.d.ts +20 -0
  46. package/dist/git-cli/exec/git.js +69 -7
  47. package/dist/git-cli/exec/git.js.map +1 -1
  48. package/dist/git-cli/exec/headState.d.ts +5 -0
  49. package/dist/git-cli/exec/headState.js +14 -1
  50. package/dist/git-cli/exec/headState.js.map +1 -1
  51. package/dist/git-cli/parsers/worktreeParser.js +2 -2
  52. package/dist/git-cli/parsers/worktreeParser.js.map +1 -1
  53. package/dist/git-cli/providers/branches.js +9 -36
  54. package/dist/git-cli/providers/branches.js.map +1 -1
  55. package/dist/git-cli/providers/config.d.ts +21 -0
  56. package/dist/git-cli/providers/config.js +72 -1
  57. package/dist/git-cli/providers/config.js.map +1 -1
  58. package/dist/git-cli/providers/graph.js +1 -1
  59. package/dist/git-cli/providers/graph.js.map +1 -1
  60. package/dist/git-cli/providers/operations.d.ts +3 -0
  61. package/dist/git-cli/providers/operations.js +14 -5
  62. package/dist/git-cli/providers/operations.js.map +1 -1
  63. package/dist/git-cli/providers/worktrees.d.ts +25 -3
  64. package/dist/git-cli/providers/worktrees.js +125 -32
  65. package/dist/git-cli/providers/worktrees.js.map +1 -1
  66. package/dist/git-cli/service.d.ts +2 -0
  67. package/dist/git-cli/service.js +4 -2
  68. package/dist/git-cli/service.js.map +1 -1
  69. package/dist/plus/integrations/providers/issueSorts.d.ts +0 -1
  70. package/dist/plus/integrations/reads/drains.d.ts +0 -1
  71. 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 readInfoExclude;
120
+ private resolveInfoExcludePath;
103
121
  private readIgnoreCase;
104
- private readGlobalExcludes;
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.readGlobalExcludes(),
157
- this.readInfoExclude(),
158
- this.readFile(`${this.repoPath}/.gitignore`),
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.readFile(`${this.repoPath}/${dir}${suffix}`);
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 readInfoExclude() {
222
- if (this.getInfoExcludePath == null) return this.readFile(`${this.gitDirPath}/info/exclude`);
283
+ async resolveInfoExcludePath() {
284
+ if (this.getInfoExcludePath == null) return `${this.gitDirPath}/info/exclude`;
223
285
  try {
224
- const path = await this.getInfoExcludePath();
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 readGlobalExcludes() {
299
+ async resolveGlobalExcludesPath() {
239
300
  if (this.getGlobalExcludesPath == null) return void 0;
240
301
  try {
241
- const path = await this.getGlobalExcludesPath();
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
- /** When given, reports of the shared `.git/config` are classified by what changed in it (see above) */
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
- /** When given, reports of the shared `.git/config` are classified by what changed in it (see above) */
87
- readFile?: ((path: string) => Promise<Uint8Array>) | undefined);
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