@gitkraken/core-gitlens 0.15.3 → 0.15.5

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 (91) hide show
  1. package/CHANGELOG.md +45 -1
  2. package/dist/git/cache.d.ts +28 -18
  3. package/dist/git/cache.js +71 -25
  4. package/dist/git/cache.js.map +1 -1
  5. package/dist/git/context.d.ts +19 -2
  6. package/dist/git/context.js.map +1 -1
  7. package/dist/git/models/pullRequest.d.ts +7 -5
  8. package/dist/git/models/pullRequest.js.map +1 -1
  9. package/dist/git/models/reference.d.ts +8 -0
  10. package/dist/git/providers/branches.d.ts +43 -0
  11. package/dist/git/providers/provider.d.ts +7 -0
  12. package/dist/git/service.d.ts +5 -1
  13. package/dist/git/service.js +8 -4
  14. package/dist/git/service.js.map +1 -1
  15. package/dist/git/watching/changeEvent.d.ts +29 -3
  16. package/dist/git/watching/changeEvent.js +39 -3
  17. package/dist/git/watching/changeEvent.js.map +1 -1
  18. package/dist/git/watching/configSections.d.ts +35 -0
  19. package/dist/git/watching/configSections.js +210 -0
  20. package/dist/git/watching/configSections.js.map +1 -0
  21. package/dist/git/watching/watchGroup.d.ts +37 -4
  22. package/dist/git/watching/watchGroup.js +108 -2
  23. package/dist/git/watching/watchGroup.js.map +1 -1
  24. package/dist/git/watching/watchService.d.ts +14 -3
  25. package/dist/git/watching/watchService.js +10 -7
  26. package/dist/git/watching/watchService.js.map +1 -1
  27. package/dist/git/watching/watchSession.d.ts +52 -20
  28. package/dist/git/watching/watchSession.js +166 -43
  29. package/dist/git/watching/watchSession.js.map +1 -1
  30. package/dist/git/watching/watcherPatterns.js +1 -0
  31. package/dist/git/watching/watcherPatterns.js.map +1 -1
  32. package/dist/git/watching/worktreeSummary.d.ts +21 -3
  33. package/dist/git/watching/worktreeSummary.js +118 -17
  34. package/dist/git/watching/worktreeSummary.js.map +1 -1
  35. package/dist/git-cli/cliGitProvider.d.ts +6 -1
  36. package/dist/git-cli/cliGitProvider.js +45 -5
  37. package/dist/git-cli/cliGitProvider.js.map +1 -1
  38. package/dist/git-cli/parsers/refParser.js +2 -1
  39. package/dist/git-cli/parsers/refParser.js.map +1 -1
  40. package/dist/git-cli/providers/branches.d.ts +10 -3
  41. package/dist/git-cli/providers/branches.js +99 -25
  42. package/dist/git-cli/providers/branches.js.map +1 -1
  43. package/dist/git-cli/providers/config.d.ts +45 -9
  44. package/dist/git-cli/providers/config.js +150 -24
  45. package/dist/git-cli/providers/config.js.map +1 -1
  46. package/dist/git-cli/providers/operations.d.ts +35 -1
  47. package/dist/git-cli/providers/operations.js +175 -16
  48. package/dist/git-cli/providers/operations.js.map +1 -1
  49. package/dist/git-cli/providers/stash.d.ts +1 -0
  50. package/dist/git-cli/providers/stash.js +9 -3
  51. package/dist/git-cli/providers/stash.js.map +1 -1
  52. package/dist/git-cli/providers/worktrees.js +11 -4
  53. package/dist/git-cli/providers/worktrees.js.map +1 -1
  54. package/dist/git-cli/service.js +5 -1
  55. package/dist/git-cli/service.js.map +1 -1
  56. package/dist/plus/integrations/models/gitHostIntegration.d.ts +29 -14
  57. package/dist/plus/integrations/models/gitHostIntegration.js +41 -19
  58. package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
  59. package/dist/plus/integrations/providers/azure/azure.d.ts +16 -16
  60. package/dist/plus/integrations/providers/azure/azure.js +45 -25
  61. package/dist/plus/integrations/providers/azure/azure.js.map +1 -1
  62. package/dist/plus/integrations/providers/azure/azure.utils.d.ts +12 -0
  63. package/dist/plus/integrations/providers/azure/azure.utils.js +17 -1
  64. package/dist/plus/integrations/providers/azure/azure.utils.js.map +1 -1
  65. package/dist/plus/integrations/providers/azure/search.d.ts +4 -0
  66. package/dist/plus/integrations/providers/azure/search.js +46 -2
  67. package/dist/plus/integrations/providers/azure/search.js.map +1 -1
  68. package/dist/plus/integrations/providers/azure/workItemSearch.d.ts +11 -0
  69. package/dist/plus/integrations/providers/azure/workItemSearch.js +54 -0
  70. package/dist/plus/integrations/providers/azure/workItemSearch.js.map +1 -0
  71. package/dist/plus/integrations/providers/azure/workItemStateCategories.d.ts +2 -2
  72. package/dist/plus/integrations/providers/azure/workItemStateCategories.js +19 -15
  73. package/dist/plus/integrations/providers/azure/workItemStateCategories.js.map +1 -1
  74. package/dist/plus/integrations/providers/azureDevOps.d.ts +33 -20
  75. package/dist/plus/integrations/providers/azureDevOps.js +109 -48
  76. package/dist/plus/integrations/providers/azureDevOps.js.map +1 -1
  77. package/dist/plus/integrations/providers/azureDevOpsServer.d.ts +16 -47
  78. package/dist/plus/integrations/providers/azureDevOpsServer.js +52 -91
  79. package/dist/plus/integrations/providers/azureDevOpsServer.js.map +1 -1
  80. package/dist/plus/integrations/providers/issueSorts.d.ts +0 -1
  81. package/dist/plus/integrations/providers/models.d.ts +5 -0
  82. package/dist/plus/integrations/providers/models.js +14 -3
  83. package/dist/plus/integrations/providers/models.js.map +1 -1
  84. package/dist/plus/integrations/reads/drains.d.ts +2 -3
  85. package/dist/plus/integrations/reads/drains.js +4 -4
  86. package/dist/plus/integrations/reads/drains.js.map +1 -1
  87. package/dist/plus/integrations/reads/issues.js +7 -4
  88. package/dist/plus/integrations/reads/issues.js.map +1 -1
  89. package/dist/plus/integrations/reads/resolveRepository.js +5 -5
  90. package/dist/plus/integrations/reads/resolveRepository.js.map +1 -1
  91. package/package.json +1 -1
@@ -7,6 +7,21 @@ import { DeprecatedGkConfigKeys, GitConfigKeys, GitConfigSubProvider, GitConfigT
7
7
  import { Git } from "../exec/git.js";
8
8
  import { CliGitProviderInternal } from "../cliGitProvider.js";
9
9
  //#region ../git-cli/src/providers/config.d.ts
10
+ /** The wall clock a read's fingerprint is checked against (see {@link isGkConfigStatRacy}), exported for testing */
11
+ export declare const gkConfigFingerprintClock: {
12
+ now: () => number;
13
+ };
14
+ /**
15
+ * Whether a stat of `.git/gk/config` taken at `statTimeMs` came too soon after the file's last change to vouch for
16
+ * the file staying unchanged while it matches — git's "racily clean" index entries. A later write in the same tick of
17
+ * the filesystem's clock as that change, of the same size and given the inode the stat saw (freed by the write in
18
+ * between and reused), gets the same fingerprint; once the stat is a tick past the change, any later write carries a
19
+ * later time. Times that are whole seconds come from a filesystem that keeps no finer (ext4 with 128-byte inodes,
20
+ * HFS+, FAT's 2-second mtime, some network mounts), so the stat must be 2s past; otherwise 50ms covers the coarsest
21
+ * tick a finer clock still has (a 10ms jiffy, Windows' 15.6ms timer) and the wall clock lagging it by one. A file
22
+ * changed after `statTimeMs` (a clock ahead of ours) is racy too. Exported for testing.
23
+ */
24
+ export declare function isGkConfigStatRacy(mtimeNs: bigint, ctimeNs: bigint, statTimeMs: number): boolean;
10
25
  /**
11
26
  * Canonicalizes a git config key for lookup against `--get-regexp` output, which git emits with the
12
27
  * section and variable-name lowercased and only the subsection case-preserved. Without this a
@@ -31,6 +46,13 @@ export declare function canonicalizeGitConfigKey(key: string): string;
31
46
  export declare function parseConfigRegexOutput(data: string | undefined, options?: {
32
47
  includeValueless?: boolean;
33
48
  }): Map<string, string>;
49
+ /**
50
+ * Parses `git config --null --get-regexp` output into every value of each key, in the order git printed them. Each
51
+ * entry is its key, a newline and its value, ended by a NUL, so a value may hold newlines or spaces unambiguously.
52
+ * A valueless entry (`key`, no `=`), which git prints with no newline, is skipped, as {@link parseConfigRegexOutput}
53
+ * skips it by default.
54
+ */
55
+ export declare function parseConfigRegexNullOutput(data: string | undefined): Map<string, string[]>;
34
56
  export declare function parseGitBoolean(value: string | undefined): boolean;
35
57
  /** Whether a git config value spells boolean-false (`false`/`no`/`off`/`0`/empty), per git's maybe-bool parsing. */
36
58
  export declare function isGitBooleanFalse(value: string): boolean;
@@ -52,8 +74,6 @@ export declare class ConfigGitSubProvider implements GitConfigSubProvider {
52
74
  * skip option joins it; any other call chains after it, so one key's writes land in call order. Cleared on settle.
53
75
  */
54
76
  private readonly _pendingGkConfigWrites;
55
- /** Maps from failed bulk reads, which a reconcile must not diff as the file's content */
56
- private readonly _failedGkConfigMaps;
57
77
  /** Writes each `.git/gk/config` one at a time — concurrent `git config` writes fail on its lock */
58
78
  private readonly _gkConfigWrites;
59
79
  getConfig(repoPath: string | undefined, key: GitConfigKeys, options?: {
@@ -66,6 +86,12 @@ export declare class ConfigGitSubProvider implements GitConfigSubProvider {
66
86
  global?: boolean;
67
87
  runGitLocally?: boolean;
68
88
  }): Promise<Map<string, string>>;
89
+ /**
90
+ * Every value of each key matching `pattern` (`git config --get-regexp`), for a key git lets repeat, like
91
+ * `remote.<name>.fetch`: {@link getConfigRegex} keeps only each key's last value. Cached beside it, so the same
92
+ * writes and config changes clear it. Rejects when git couldn't read the configuration.
93
+ */
94
+ getConfigRegexAll(repoPath: string, pattern: string): Promise<Map<string, string[]>>;
69
95
  private getConfigRegexCore;
70
96
  setConfig(repoPath: string | undefined, key: GitConfigKeys, value: string | undefined, options?: {
71
97
  file?: string;
@@ -89,6 +115,14 @@ export declare class ConfigGitSubProvider implements GitConfigSubProvider {
89
115
  * namespaces costs one `git config` call instead of one per namespace.
90
116
  */
91
117
  private getGkConfigMap;
118
+ /** Records the read that is creating `repoPath`'s cached gk config map (see {@link gkConfigMapReads}) */
119
+ private trackGkConfigMapRead;
120
+ /**
121
+ * Hard-evicts the cached gk config map, unless `.git/gk/config` is unchanged since the read behind it stat'd the
122
+ * file, or that read is yet to spawn. The stat is taken before the read, so a write landing between the two
123
+ * only costs a re-read on its report, never a missed change. Returns the read it kept, if any.
124
+ */
125
+ private evictGkConfigMapIfChanged;
92
126
  private getGkConfigRegexCore;
93
127
  setGkConfig(repoPath: string, key: GkConfigKeys | DeprecatedGkConfigKeys, value: string | undefined, options?: {
94
128
  skipInvalidation?: readonly GkConfigInvalidationTarget[];
@@ -111,10 +145,12 @@ export declare class ConfigGitSubProvider implements GitConfigSubProvider {
111
145
  private runGkConfigSectionCommand;
112
146
  /**
113
147
  * Reconciles a watcher-observed `'gkConfig'` change — registered with the shared `Cache` as its
114
- * gk-config reconciler (see the constructor). Re-reads the bulk map (hard-evicted by
115
- * `Cache.handleGkConfigChanged`, so this hits disk) and diffs it against `priorSnapshot` to cascade
148
+ * gk-config reconciler (see the constructor). Hard-evicts the bulk map as the change is reported, unless the file
149
+ * is unchanged since the map was read (see `evictGkConfigMapIfChanged`), then reads it (from disk, or the map
150
+ * kept) and diffs it against `priorSnapshot` to cascade
116
151
  * `baseBranchName`/`branchOverviews` only for refs whose merge-relevant keys actually changed, and to evict the
117
- * branches only when a branch metadata key (another process's activity stamp) changed.
152
+ * branches only when a branch metadata key (another process's activity stamp) changed. The diff runs on a kept
153
+ * map too: a read can land between an external write and its report, and already hold the change.
118
154
  *
119
155
  * Serialized per commonPath: a reconcile is a read-then-diff against a single shared baseline, so
120
156
  * two overlapping passes would both diff against the same snapshot and the later change could fall
@@ -122,10 +158,10 @@ export declare class ConfigGitSubProvider implements GitConfigSubProvider {
122
158
  * diffs against the baseline the previous pass just established.
123
159
  *
124
160
  * `.git/gk/config` lives in the common dir, so the watcher delivers one event per worktree sharing it
125
- * (see `WatchGroup.onCommonEvent`). Siblings never run concurrently — the later ones fold into the
126
- * in-flight pass — but folding in arms the trailing pass, so a fan-out costs one extra read that
127
- * re-reads and diffs an unchanged file. However many siblings land while a pass is running, they
128
- * coalesce into that single trailing pass; the cost is the price of never dropping a real change.
161
+ * (see `WatchGroup.onCommonEvent`), and core's own write is reported too. Siblings never run concurrently — the
162
+ * later ones fold into the in-flight pass — but folding in arms the trailing pass, which re-diffs the map unless
163
+ * the file changed again. However many siblings land while a pass is running, they coalesce into that single
164
+ * trailing pass; the cost is the price of never dropping a real change.
129
165
  * Every reporting path is retained because `Repository` matches the follow-up change event on exact
130
166
  * path, and a sibling worktree that never receives one would sit on stale derived values.
131
167
  */
@@ -10,7 +10,7 @@ import { WorkspaceUntrustedError } from "../../git/errors.js";
10
10
  import { fsExists } from "../../utils/env/node/exec.js";
11
11
  import { KeyedSequencer } from "../exec/indexLock.js";
12
12
  import { gate } from "../../utils/decorators/gate.js";
13
- import { promises } from "fs";
13
+ import { promises, statSync } from "fs";
14
14
  import { env } from "process";
15
15
  import { hostname, userInfo } from "os";
16
16
  //#region ../git-cli/src/providers/config.ts
@@ -19,6 +19,58 @@ const emptyArray = Object.freeze([]);
19
19
  /** Catch-all pattern matching every key in `.git/gk/config` for the one-shot bulk read. */
20
20
  const gkConfigAllPattern = ".";
21
21
  /**
22
+ * Maps from failed bulk reads, which a reconcile must not diff as the file's content, each to the read that produced
23
+ * it. Module-wide, like {@link gkConfigMapReads}: a reconcile can join a read another provider sharing its `Cache`
24
+ * started.
25
+ */
26
+ const failedGkConfigMaps = /* @__PURE__ */ new WeakMap();
27
+ /**
28
+ * The read behind each commonPath's cached gk config map, per `Cache`. Recorded as the read starts, which is when
29
+ * the cache creates the entry for it, so it always describes the entry cached now. Kept per `Cache` rather than per
30
+ * provider, since providers sharing a `Cache` serve each other's reads.
31
+ */
32
+ const gkConfigMapReads = /* @__PURE__ */ new WeakMap();
33
+ /** The wall clock a read's fingerprint is checked against (see {@link isGkConfigStatRacy}), exported for testing */
34
+ const gkConfigFingerprintClock = { now: () => Date.now() };
35
+ const nsPerMs = 1000000n;
36
+ const nsPerSecond = 1000000000n;
37
+ /**
38
+ * Whether a stat of `.git/gk/config` taken at `statTimeMs` came too soon after the file's last change to vouch for
39
+ * the file staying unchanged while it matches — git's "racily clean" index entries. A later write in the same tick of
40
+ * the filesystem's clock as that change, of the same size and given the inode the stat saw (freed by the write in
41
+ * between and reused), gets the same fingerprint; once the stat is a tick past the change, any later write carries a
42
+ * later time. Times that are whole seconds come from a filesystem that keeps no finer (ext4 with 128-byte inodes,
43
+ * HFS+, FAT's 2-second mtime, some network mounts), so the stat must be 2s past; otherwise 50ms covers the coarsest
44
+ * tick a finer clock still has (a 10ms jiffy, Windows' 15.6ms timer) and the wall clock lagging it by one. A file
45
+ * changed after `statTimeMs` (a clock ahead of ours) is racy too. Exported for testing.
46
+ */
47
+ function isGkConfigStatRacy(mtimeNs, ctimeNs, statTimeMs) {
48
+ const changedNs = mtimeNs > ctimeNs ? mtimeNs : ctimeNs;
49
+ const marginNs = mtimeNs % nsPerSecond === 0n && ctimeNs % nsPerSecond === 0n ? 2n * nsPerSecond : 50n * nsPerMs;
50
+ return BigInt(Math.floor(statTimeMs)) * nsPerMs - changedNs < marginNs;
51
+ }
52
+ /**
53
+ * A stat of `.git/gk/config` that two stats share only if no write landed between them: `git config` writes a lock
54
+ * file and renames it over the original, so every write gives the file a new inode, and its size and times tell
55
+ * apart a later write that lands on a freed inode — unless that write fell in the same clock tick as the change the
56
+ * first stat saw, which `statTimeMs` rules out (see {@link isGkConfigStatRacy}): given the time the stat is taken,
57
+ * a stat that racy is `undefined`, never matching. `'absent'` when there is no file, `undefined` when it can't be
58
+ * stat'd.
59
+ */
60
+ function getGkConfigFingerprint(path, statTimeMs) {
61
+ try {
62
+ const stat = statSync(path, {
63
+ bigint: true,
64
+ throwIfNoEntry: false
65
+ });
66
+ if (stat == null) return "absent";
67
+ if (statTimeMs != null && isGkConfigStatRacy(stat.mtimeNs, stat.ctimeNs, statTimeMs)) return void 0;
68
+ return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`;
69
+ } catch {
70
+ return;
71
+ }
72
+ }
73
+ /**
22
74
  * Canonicalizes a git config key for lookup against `--get-regexp` output, which git emits with the
23
75
  * section and variable-name lowercased and only the subsection case-preserved. Without this a
24
76
  * subsection-less key like `gk.defaultRemote` would miss the lowercased `gk.defaultremote` in the map.
@@ -59,6 +111,26 @@ function parseConfigRegexOutput(data, options) {
59
111
  }
60
112
  return configMap;
61
113
  }
114
+ /**
115
+ * Parses `git config --null --get-regexp` output into every value of each key, in the order git printed them. Each
116
+ * entry is its key, a newline and its value, ended by a NUL, so a value may hold newlines or spaces unambiguously.
117
+ * A valueless entry (`key`, no `=`), which git prints with no newline, is skipped, as {@link parseConfigRegexOutput}
118
+ * skips it by default.
119
+ */
120
+ function parseConfigRegexNullOutput(data) {
121
+ const configMap = /* @__PURE__ */ new Map();
122
+ if (!data) return configMap;
123
+ for (const entry of data.split("\0")) {
124
+ const newlineIndex = entry.indexOf("\n");
125
+ if (newlineIndex === -1) continue;
126
+ const key = entry.substring(0, newlineIndex);
127
+ const value = entry.substring(newlineIndex + 1);
128
+ const values = configMap.get(key);
129
+ if (values != null) values.push(value);
130
+ else configMap.set(key, [value]);
131
+ }
132
+ return configMap;
133
+ }
62
134
  function parseGitBoolean(value) {
63
135
  if (value == null) return false;
64
136
  const normalized = value.toLowerCase().trim();
@@ -93,8 +165,6 @@ var ConfigGitSubProvider = class {
93
165
  * skip option joins it; any other call chains after it, so one key's writes land in call order. Cleared on settle.
94
166
  */
95
167
  _pendingGkConfigWrites = /* @__PURE__ */ new Map();
96
- /** Maps from failed bulk reads, which a reconcile must not diff as the file's content */
97
- _failedGkConfigMaps = /* @__PURE__ */ new WeakSet();
98
168
  /** Writes each `.git/gk/config` one at a time — concurrent `git config` writes fail on its lock */
99
169
  _gkConfigWrites = new KeyedSequencer();
100
170
  getConfig(repoPath, key, options) {
@@ -117,6 +187,22 @@ var ConfigGitSubProvider = class {
117
187
  const global = options?.global || repoPath == null;
118
188
  return this.cache.getConfigRegex(global ? void 0 : repoPath, pattern, () => this.getConfigRegexCore(repoPath, pattern, options));
119
189
  }
190
+ /**
191
+ * Every value of each key matching `pattern` (`git config --get-regexp`), for a key git lets repeat, like
192
+ * `remote.<name>.fetch`: {@link getConfigRegex} keeps only each key's last value. Cached beside it, so the same
193
+ * writes and config changes clear it. Rejects when git couldn't read the configuration.
194
+ */
195
+ async getConfigRegexAll(repoPath, pattern) {
196
+ const joined = await this.cache.getConfigRegex(repoPath, `\0all\0${pattern}`, async () => {
197
+ const result = await this.git.run({
198
+ cwd: this.cache.getCommonPath(repoPath),
199
+ errors: "ignore"
200
+ }, "config", "--null", "--get-regexp", pattern);
201
+ if (result.completion.status !== "exited" || result.exitCode !== 0 && result.exitCode !== 1) throw new Error(`Unable to read the config keys matching '${pattern}'`);
202
+ return new Map(Array.from(parseConfigRegexNullOutput(result.stdout), ([key, values]) => [key, values.join("\0")]));
203
+ });
204
+ return new Map(Array.from(joined, ([key, value]) => [key, value.split("\0")]));
205
+ }
120
206
  async getConfigRegexCore(repoPath, pattern, options) {
121
207
  const args = ["config", "--get-regex"];
122
208
  if (options?.file) args.push("-f", options.file);
@@ -249,22 +335,54 @@ var ConfigGitSubProvider = class {
249
335
  * namespaces costs one `git config` call instead of one per namespace.
250
336
  */
251
337
  async getGkConfigMap(repoPath) {
252
- await this.migrateGkConfigFromGitConfig(this.cache.getCommonPath(repoPath));
338
+ const commonPath = this.cache.getCommonPath(repoPath);
339
+ if (!this._migratedRepos.has(commonPath)) await this.migrateGkConfigFromGitConfig(commonPath);
253
340
  return this.cache.getGkConfigMap(repoPath, async (cacheable) => {
254
- const { map, failed } = await this.getGkConfigRegexCore(repoPath, gkConfigAllPattern);
341
+ const read = this.trackGkConfigMapRead(repoPath);
342
+ const { map, failed } = await this.getGkConfigRegexCore(repoPath, gkConfigAllPattern, read);
255
343
  if (failed) {
256
344
  cacheable.invalidate();
257
- this._failedGkConfigMaps.add(map);
345
+ failedGkConfigMaps.set(map, read);
258
346
  }
259
347
  return map;
260
348
  });
261
349
  }
262
- async getGkConfigRegexCore(repoPath, pattern) {
350
+ /** Records the read that is creating `repoPath`'s cached gk config map (see {@link gkConfigMapReads}) */
351
+ trackGkConfigMapRead(repoPath) {
352
+ let reads = gkConfigMapReads.get(this.cache);
353
+ if (reads == null) {
354
+ reads = /* @__PURE__ */ new Map();
355
+ gkConfigMapReads.set(this.cache, reads);
356
+ }
357
+ const read = { spawned: false };
358
+ reads.set(this.cache.getCommonPath(repoPath), read);
359
+ return read;
360
+ }
361
+ /**
362
+ * Hard-evicts the cached gk config map, unless `.git/gk/config` is unchanged since the read behind it stat'd the
363
+ * file, or that read is yet to spawn. The stat is taken before the read, so a write landing between the two
364
+ * only costs a re-read on its report, never a missed change. Returns the read it kept, if any.
365
+ */
366
+ evictGkConfigMapIfChanged(repoPath) {
367
+ const read = gkConfigMapReads.get(this.cache)?.get(this.cache.getCommonPath(repoPath));
368
+ if (read != null) {
369
+ if (!read.spawned) return read;
370
+ if (read.path != null && read.fingerprint != null && getGkConfigFingerprint(read.path) === read.fingerprint) return read;
371
+ }
372
+ this.cache.deleteGkConfigMap(repoPath);
373
+ }
374
+ async getGkConfigRegexCore(repoPath, pattern, read) {
263
375
  const gkConfigPath = await this.getGkConfigPath(repoPath);
264
- if (!gkConfigPath) return {
265
- map: /* @__PURE__ */ new Map(),
266
- failed: false
267
- };
376
+ if (!gkConfigPath) {
377
+ read.spawned = true;
378
+ return {
379
+ map: /* @__PURE__ */ new Map(),
380
+ failed: false
381
+ };
382
+ }
383
+ read.path = gkConfigPath;
384
+ read.fingerprint = getGkConfigFingerprint(gkConfigPath, gkConfigFingerprintClock.now());
385
+ read.spawned = true;
268
386
  const result = await this.git.run({
269
387
  cwd: repoPath,
270
388
  errors: "ignore",
@@ -355,10 +473,12 @@ var ConfigGitSubProvider = class {
355
473
  }
356
474
  /**
357
475
  * Reconciles a watcher-observed `'gkConfig'` change — registered with the shared `Cache` as its
358
- * gk-config reconciler (see the constructor). Re-reads the bulk map (hard-evicted by
359
- * `Cache.handleGkConfigChanged`, so this hits disk) and diffs it against `priorSnapshot` to cascade
476
+ * gk-config reconciler (see the constructor). Hard-evicts the bulk map as the change is reported, unless the file
477
+ * is unchanged since the map was read (see `evictGkConfigMapIfChanged`), then reads it (from disk, or the map
478
+ * kept) and diffs it against `priorSnapshot` to cascade
360
479
  * `baseBranchName`/`branchOverviews` only for refs whose merge-relevant keys actually changed, and to evict the
361
- * branches only when a branch metadata key (another process's activity stamp) changed.
480
+ * branches only when a branch metadata key (another process's activity stamp) changed. The diff runs on a kept
481
+ * map too: a read can land between an external write and its report, and already hold the change.
362
482
  *
363
483
  * Serialized per commonPath: a reconcile is a read-then-diff against a single shared baseline, so
364
484
  * two overlapping passes would both diff against the same snapshot and the later change could fall
@@ -366,10 +486,10 @@ var ConfigGitSubProvider = class {
366
486
  * diffs against the baseline the previous pass just established.
367
487
  *
368
488
  * `.git/gk/config` lives in the common dir, so the watcher delivers one event per worktree sharing it
369
- * (see `WatchGroup.onCommonEvent`). Siblings never run concurrently — the later ones fold into the
370
- * in-flight pass — but folding in arms the trailing pass, so a fan-out costs one extra read that
371
- * re-reads and diffs an unchanged file. However many siblings land while a pass is running, they
372
- * coalesce into that single trailing pass; the cost is the price of never dropping a real change.
489
+ * (see `WatchGroup.onCommonEvent`), and core's own write is reported too. Siblings never run concurrently — the
490
+ * later ones fold into the in-flight pass — but folding in arms the trailing pass, which re-diffs the map unless
491
+ * the file changed again. However many siblings land while a pass is running, they coalesce into that single
492
+ * trailing pass; the cost is the price of never dropping a real change.
373
493
  * Every reporting path is retained because `Repository` matches the follow-up change event on exact
374
494
  * path, and a sibling worktree that never receives one would sit on stale derived values.
375
495
  */
@@ -377,6 +497,7 @@ var ConfigGitSubProvider = class {
377
497
  const commonPath = this.cache.getCommonPath(repoPath);
378
498
  const inflight = this._gkConfigReconciles.get(commonPath);
379
499
  if (inflight != null) {
500
+ this.evictGkConfigMapIfChanged(repoPath);
380
501
  inflight.paths.set(repoPath, this.cache.getCloseGeneration(repoPath));
381
502
  inflight.pending = true;
382
503
  return "deferred";
@@ -401,9 +522,13 @@ var ConfigGitSubProvider = class {
401
522
  entry.pending = false;
402
523
  const readPath = first(this.pruneClosedReconcilePaths(entry.paths).keys());
403
524
  if (readPath == null) return "reconciled";
404
- this.cache.deleteGkConfigMap(readPath);
405
- const freshMap = await this.getGkConfigMap(readPath);
406
- if (this._failedGkConfigMaps.has(freshMap)) throw new Error(`Failed to read the gk config through '${readPath}'`);
525
+ const kept = this.evictGkConfigMapIfChanged(readPath);
526
+ let freshMap = await this.getGkConfigMap(readPath);
527
+ if (kept != null && failedGkConfigMaps.get(freshMap) === kept) {
528
+ this.cache.deleteGkConfigMap(readPath);
529
+ freshMap = await this.getGkConfigMap(readPath);
530
+ }
531
+ if (failedGkConfigMaps.has(freshMap)) throw new Error(`Failed to read the gk config through '${readPath}'`);
407
532
  if (!this.pruneClosedReconcilePaths(entry.paths).size) return "reconciled";
408
533
  if (this.cache.reconcileGkConfigMap(readPath, snapshot, freshMap)) for (const path of entry.paths.keys()) this.provider.notifyHost(path, ["gkConfig"]);
409
534
  snapshot = this.cache.getGkConfigSnapshot(readPath);
@@ -644,12 +769,13 @@ var ConfigGitSubProvider = class {
644
769
  };
645
770
  __decorate([trace()], ConfigGitSubProvider.prototype, "getConfig", null);
646
771
  __decorate([trace()], ConfigGitSubProvider.prototype, "getConfigRegex", null);
772
+ __decorate([trace()], ConfigGitSubProvider.prototype, "getConfigRegexAll", null);
647
773
  __decorate([debug()], ConfigGitSubProvider.prototype, "setConfig", null);
648
774
  __decorate([gate(), debug()], ConfigGitSubProvider.prototype, "getCurrentUser", null);
649
775
  __decorate([gate(), trace({ exit: (r) => `returned ${r}` })], ConfigGitSubProvider.prototype, "getDefaultWorktreePath", null);
650
776
  __decorate([gate(), trace({ exit: (r) => `returned ${r.uri.toString(true)}, commonUri=${r.commonUri?.toString(true)}, parentUri=${r.parentUri?.toString(true)}` })], ConfigGitSubProvider.prototype, "getGitDir", null);
651
- __decorate([debug()], ConfigGitSubProvider.prototype, "getGkConfig", null);
652
- __decorate([debug()], ConfigGitSubProvider.prototype, "getGkConfigRegex", null);
777
+ __decorate([trace()], ConfigGitSubProvider.prototype, "getGkConfig", null);
778
+ __decorate([trace()], ConfigGitSubProvider.prototype, "getGkConfigRegex", null);
653
779
  __decorate([debug()], ConfigGitSubProvider.prototype, "setGkConfig", null);
654
780
  __decorate([debug()], ConfigGitSubProvider.prototype, "removeGkConfigBranchSection", null);
655
781
  __decorate([debug()], ConfigGitSubProvider.prototype, "renameGkConfigBranchSection", null);
@@ -658,6 +784,6 @@ __decorate([debug()], ConfigGitSubProvider.prototype, "validateSigningSetup", nu
658
784
  __decorate([debug()], ConfigGitSubProvider.prototype, "setSigningConfig", null);
659
785
  __decorate([gate(), debug()], ConfigGitSubProvider.prototype, "migrateGkConfigFromGitConfig", null);
660
786
  //#endregion
661
- export { ConfigGitSubProvider, canonicalizeGitConfigKey, isGitBooleanFalse, parseConfigRegexOutput, parseGitBoolean };
787
+ export { ConfigGitSubProvider, canonicalizeGitConfigKey, gkConfigFingerprintClock, isGitBooleanFalse, isGkConfigStatRacy, parseConfigRegexNullOutput, parseConfigRegexOutput, parseGitBoolean };
662
788
 
663
789
  //# sourceMappingURL=config.js.map