@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
@@ -3,7 +3,9 @@ import { CancellationError, isCancellationError, raceWithSignal } from "../../ut
3
3
  import { Logger as _logger } from "../../utils/logger.js";
4
4
  import { Emitter } from "../../utils/event.js";
5
5
  import { getRepositoryKey } from "../../utils/uri.js";
6
+ import { joinPaths } from "../../utils/path.js";
6
7
  import { getRefChangeKind } from "./changeEvent.js";
8
+ import { LruMap } from "../../utils/lruMap.js";
7
9
  //#region ../git/src/watching/worktreeSummary.ts
8
10
  /** Read groups, as bit flags so a pending set is a number rather than an allocation per event */
9
11
  const dirtyField = 1;
@@ -22,12 +24,33 @@ const baseMoveField = 32;
22
24
  const allFields = 7;
23
25
  const maxConcurrentReadsPerRepository = 4;
24
26
  const maxConcurrentReads = 8;
27
+ const maxUnwatchedReads = 256;
28
+ const maxDeferredDirtyMs = 1e4;
25
29
  const emptyRefs = Object.freeze([]);
26
30
  const noBaseKeys = [
27
31
  void 0,
28
32
  void 0,
29
33
  void 0
30
34
  ];
35
+ /** A read of a worktree without a subscription, which concurrent callers share */
36
+ var UnwatchedFlight = class {
37
+ key;
38
+ probeDirty;
39
+ disposed;
40
+ abort = new AbortController();
41
+ /** Callers waiting for it; the read is aborted once none is */
42
+ callers = 0;
43
+ /** Read groups made due while it ran, carried into what it stores */
44
+ due = 0;
45
+ promise;
46
+ constructor(key, probeDirty, disposed, read) {
47
+ this.key = key;
48
+ this.probeDirty = probeDirty;
49
+ this.disposed = disposed;
50
+ this.promise = Promise.resolve().then(() => read(this));
51
+ this.promise.catch(noop);
52
+ }
53
+ };
31
54
  var SummarySubscription = class {
32
55
  entry;
33
56
  options;
@@ -44,6 +67,9 @@ var SummarySubscription = class {
44
67
  get current() {
45
68
  return this.disposed ? void 0 : this.entry.value;
46
69
  }
70
+ get dirtyReadAt() {
71
+ return this.disposed || this.entry.value == null ? void 0 : this.entry.dirtyReadAt;
72
+ }
47
73
  get onDidChange() {
48
74
  return this.emitter.event;
49
75
  }
@@ -79,7 +105,10 @@ var SummarySubscription = class {
79
105
  * has working changes, and how far it is past its base — kept current by the repository watch layer.
80
106
  *
81
107
  * **Lifecycle.** Entries are per repository key and ref-counted across {@link subscribe} calls. The first subscriber
82
- * starts watching the worktree and reads everything; the last one to leave aborts any in-flight read, releases the
108
+ * starts watching the worktree and reads everything, or, for a worktree last read without a subscription, starts from
109
+ * that read: it re-reads the groups whose inputs moved since, by the same rules as such a read, and always `dirty` —
110
+ * unless its working-tree watch is still starting, when the rescan that watch owes reads it, and the summary keeps the
111
+ * saved read's flag, with its `dirtyReadAt`, until then. The last one to leave aborts any in-flight read, releases the
83
112
  * watch and drops the value. Each subscriber's timing options join the watch session's cadence (the shortest wins);
84
113
  * the service's own listening subscriptions never constrain it.
85
114
  *
@@ -106,11 +135,27 @@ var SummarySubscription = class {
106
135
  * **Value-change-only notification.** {@link WorktreeSummarySubscription.onDidChange} and {@link onDidChangeSummary}
107
136
  * fire only when some field's value differs from the previous summary — a recompute that reads the same answer is
108
137
  * silent, so a consumer can re-render on every event without diffing. A failed recompute keeps the last values and
109
- * sets `stale` (firing once); the next successful one clears it.
138
+ * sets `stale` (firing once); the next successful one clears it. A summary still showing the dirty flag of a read
139
+ * without a subscription fires again, unchanged, once that flag is read again (its `dirtyReadAt` cleared).
140
+ *
141
+ * **Worktrees without a subscription.** {@link getSummary} and {@link getSummarySnapshot} answer one from its last read
142
+ * (kept for the {@link maxUnwatchedReads} most recently asked), re-reading only the groups whose inputs moved since:
143
+ * HEAD's branch or commit reads all of them; the upstream's tip, or its name, reads `branch`; a ref the base depends on
144
+ * (where a remote's `HEAD` symref points, for one) reads `base`, re-measured from the previous one where that can
145
+ * answer; the config inputs its base read consulted read `branch` and the base when they changed; and the worktree's
146
+ * index file reads `dirty`. The inputs come from core's caches, so nothing moved means no git at all; only a `.git`
147
+ * watch of the worktree (or core's own writes) keeps them current, so a host must watch the `.git` of a worktree it
148
+ * asks about this way for others' changes to show. Its working tree is unwatched, so an edit that leaves the index
149
+ * alone goes unseen until `dirty` is read again: the snapshot says when that was, and `probeDirty` reads it anyway.
150
+ * Concurrent reads of a worktree share one, and take no read slot: each is a caller's foreground read, which never
151
+ * waits behind subscribed worktrees' runs. A first subscriber takes over the last read.
110
152
  */
111
153
  var WorktreeSummaryService = class {
112
154
  _onDidChangeSummary = new Emitter();
113
- /** Fires when the summary of any subscribed worktree changes value */
155
+ /**
156
+ * Fires when the summary of any subscribed worktree changes value, and with the same summary when a dirty flag kept
157
+ * from a read without a subscription is read again (see {@link WorktreeSummarySubscription.dirtyReadAt})
158
+ */
114
159
  get onDidChangeSummary() {
115
160
  return this._onDidChangeSummary.event;
116
161
  }
@@ -120,9 +165,14 @@ var WorktreeSummaryService = class {
120
165
  activeReads = 0;
121
166
  /** Entries waiting for a read slot, oldest first */
122
167
  readWaiters = /* @__PURE__ */ new Map();
168
+ /** The last read of each worktree without a subscription, by repository key */
169
+ unwatchedReads = new LruMap(maxUnwatchedReads);
170
+ /** The read in flight for each worktree without a subscription, by repository key */
171
+ unwatchedFlights = /* @__PURE__ */ new Map();
123
172
  watchService;
124
173
  getRepository;
125
174
  getCommonPath;
175
+ stat;
126
176
  verifyBaseRemeasure;
127
177
  mergeTargetsListener;
128
178
  _disposed = false;
@@ -130,6 +180,7 @@ var WorktreeSummaryService = class {
130
180
  this.watchService = options.watchService;
131
181
  this.getRepository = options.getRepository;
132
182
  this.getCommonPath = options.getCommonPath;
183
+ this.stat = options.stat;
133
184
  this.verifyBaseRemeasure = options.verifyBaseRemeasure;
134
185
  this.mergeTargetsListener = options.onDidChangeMergeTargets?.((e) => this.onMergeTargetsChanged(e));
135
186
  }
@@ -141,12 +192,22 @@ var WorktreeSummaryService = class {
141
192
  this._disposed = true;
142
193
  for (const resolve of this.readWaiters.values()) resolve(false);
143
194
  this.readWaiters.clear();
195
+ for (const flight of this.unwatchedFlights.values()) {
196
+ flight.disposed = true;
197
+ flight.abort.abort();
198
+ }
199
+ this.unwatchedFlights.clear();
200
+ this.unwatchedReads.clear();
144
201
  this.mergeTargetsListener?.dispose();
145
202
  for (const entry of [...this.entries.values()]) for (const subscription of [...entry.subscribers]) subscription.dispose();
146
203
  this.entries.clear();
147
204
  this._onDidChangeSummary.dispose();
148
205
  }
149
- /** Subscribes to a worktree's summary, starting to watch and read it if this is its first subscriber */
206
+ /**
207
+ * Subscribes to a worktree's summary, starting to watch and read it if this is its first subscriber: from its last
208
+ * read without a subscription, if any, re-reading the groups whose inputs moved since and the dirty flag — at once,
209
+ * or, while its working-tree watch is still starting, as that watch goes live
210
+ */
150
211
  subscribe(repoPath, options) {
151
212
  const key = getRepositoryKey(repoPath);
152
213
  let entry = this._disposed ? void 0 : this.entries.get(key);
@@ -161,33 +222,179 @@ var WorktreeSummaryService = class {
161
222
  target.subscribers.add(subscription);
162
223
  if (isNew) {
163
224
  if (!target.disposed) {
164
- target.pending = 23;
225
+ this.unwatchedFlights.delete(key);
165
226
  target.running = this.start(target);
166
227
  }
167
228
  } else if (target.handle != null) subscription.attach(target.handle.session);
168
- else if (target.running == null && !target.disposed) {
169
- target.pending = 23;
170
- target.running = this.start(target);
171
- }
229
+ else if (target.running == null && !target.disposed) target.running = this.start(target);
172
230
  return subscription;
173
231
  }
174
232
  /**
175
233
  * Returns a worktree's summary. For a subscribed worktree, the cached value — after waiting for any run in flight
176
- * or pending; it rejects with the read's error when there is still no value. Otherwise a one-off full read that
177
- * is neither cached nor watched.
234
+ * or pending; it rejects with the read's error when there is still no value. Otherwise the last read of it,
235
+ * re-reading what moved since (see {@link getSummarySnapshot}).
178
236
  */
179
237
  async getSummary(repoPath, cancellation) {
180
238
  const key = getRepositoryKey(repoPath);
181
239
  const entry = this.entries.get(key);
182
- if (entry == null) {
183
- const repo = this.getRepository(key);
184
- if (repo == null) throw new Error(`No repository found for '${key}'`);
185
- return (await readSummary(repo, key, allFields, void 0, void 0, void 0, void 0, void 0, cancellation, this.verifyBaseRemeasure)).summary;
240
+ if (entry == null) return (await this.getUnwatchedSnapshot(key, false, cancellation)).summary;
241
+ return this.getSubscribedSummary(entry, cancellation);
242
+ }
243
+ /**
244
+ * Returns a worktree's summary, with when its dirty flag was read. A subscribed worktree answers as
245
+ * {@link getSummary} does, with `dirtyReadAt` `undefined` once its summary follows its working tree, and until then
246
+ * the saved read's (see {@link WorktreeSummarySubscription.dirtyReadAt}), which `probeDirty` reads again. Any other
247
+ * answers from its last read, re-reading only the groups whose inputs moved since (no git when none did), and in
248
+ * full the first time, or after a failed or stale read. Its working tree is unwatched, so the dirty flag is re-read
249
+ * only when the index file moved, or with `probeDirty`; a rejection drops the last read. Its inputs are core's
250
+ * cached answers, which only a `.git` watch of the worktree (or core's own writes) keeps current.
251
+ */
252
+ async getSummarySnapshot(repoPath, options) {
253
+ const key = getRepositoryKey(repoPath);
254
+ const entry = this.entries.get(key);
255
+ if (entry == null) return this.getUnwatchedSnapshot(key, options?.probeDirty ?? false, options?.cancellation);
256
+ return this.getSubscribedSnapshot(entry, options?.probeDirty ?? false, options?.cancellation);
257
+ }
258
+ async getSubscribedSnapshot(entry, probeDirty, cancellation) {
259
+ let summary = await this.getSubscribedSummary(entry, cancellation);
260
+ if (probeDirty && entry.dirtyReadAt != null) {
261
+ this.queue(entry, dirtyField);
262
+ summary = await this.getSubscribedSummary(entry, cancellation);
186
263
  }
264
+ return {
265
+ summary,
266
+ dirtyReadAt: entry.dirtyReadAt
267
+ };
268
+ }
269
+ async getSubscribedSummary(entry, cancellation) {
187
270
  if (entry.running != null) await (cancellation != null ? raceWithSignal(entry.running, cancellation) : entry.running);
188
271
  if (entry.disposed) throw new CancellationError();
189
272
  if (entry.value != null) return entry.value;
190
- throw entry.error ?? /* @__PURE__ */ new Error(`No summary available for '${key}'`);
273
+ throw entry.error ?? /* @__PURE__ */ new Error(`No summary available for '${entry.key}'`);
274
+ }
275
+ /** Joins the read in flight for `key`, or starts one; the read is aborted only once no caller waits for it */
276
+ async getUnwatchedSnapshot(key, probeDirty, cancellation) {
277
+ if (cancellation?.aborted) throw new CancellationError();
278
+ let flight = this.getJoinableFlight(key);
279
+ if (probeDirty) {
280
+ while (flight != null && !flight.probeDirty) {
281
+ const waited = flight;
282
+ try {
283
+ await (cancellation != null ? raceWithSignal(waited.promise, cancellation) : waited.promise);
284
+ } catch (ex) {
285
+ if (cancellation?.aborted) throw ex;
286
+ }
287
+ flight = this.getJoinableFlight(key);
288
+ if (flight === waited) flight = void 0;
289
+ }
290
+ const entry = this.entries.get(key);
291
+ if (entry != null) return this.getSubscribedSnapshot(entry, true, cancellation);
292
+ }
293
+ if (flight == null) {
294
+ flight = new UnwatchedFlight(key, probeDirty, this._disposed, (f) => this.readUnwatched(f));
295
+ if (!this._disposed) this.unwatchedFlights.set(key, flight);
296
+ }
297
+ const current = flight;
298
+ current.callers++;
299
+ try {
300
+ const read = await (cancellation != null ? raceWithSignal(current.promise, cancellation) : current.promise);
301
+ return {
302
+ summary: read.result.summary,
303
+ dirtyReadAt: read.dirtyReadAt
304
+ };
305
+ } finally {
306
+ current.callers--;
307
+ if (current.callers === 0) {
308
+ if (this.unwatchedFlights.get(key) === current) this.unwatchedFlights.delete(key);
309
+ current.disposed = true;
310
+ current.abort.abort();
311
+ }
312
+ }
313
+ }
314
+ /**
315
+ * The read in flight for `key`, unless core wrote a merge target or base while it ran: it may have read the stored
316
+ * values before the write, and a caller after an awaited write sees them, as one of a subscribed worktree does
317
+ */
318
+ getJoinableFlight(key) {
319
+ const flight = this.unwatchedFlights.get(key);
320
+ return flight?.due === 0 ? flight : void 0;
321
+ }
322
+ async readUnwatched(flight) {
323
+ const { key } = flight;
324
+ const signal = flight.abort.signal;
325
+ if (flight.disposed) throw new CancellationError();
326
+ let last = this.unwatchedReads.get(key);
327
+ if (last?.result.summary.stale) last = void 0;
328
+ try {
329
+ const repo = this.getRepository(key);
330
+ if (repo == null) throw new Error(`No repository found for '${key}'`);
331
+ const headRemotes = last != null ? getHeadRemotes(last.result.summary.baseDependsOn) : void 0;
332
+ const probe = await this.probeUnwatched(repo, headRemotes, true, signal);
333
+ if (flight.disposed) throw new CancellationError();
334
+ let fields;
335
+ let baseMoves;
336
+ if (last == null) fields = 23;
337
+ else {
338
+ ({fields, baseMoves} = await getMovedFields(repo, last, probe, flight.probeDirty, signal));
339
+ if (flight.disposed) throw new CancellationError();
340
+ if (fields === 0) {
341
+ this.unwatchedReads.touch(key);
342
+ return last;
343
+ }
344
+ }
345
+ if (headRemotes != null && (fields & 60) !== 0) {
346
+ await readRemoteHeads(repo, probe.remoteHeads, void 0, signal);
347
+ if (flight.disposed) throw new CancellationError();
348
+ }
349
+ let result = await readSummary(repo, key, fields, last?.result.summary, last?.result.baseKeys, last?.result.baseConfigKey, last?.result.baseDefaultBranch, baseMoves, signal, this.verifyBaseRemeasure);
350
+ const kept = last?.result.summary;
351
+ if (kept != null && summariesEqual(kept, result.summary)) result = {
352
+ ...result,
353
+ summary: kept
354
+ };
355
+ const read = {
356
+ result,
357
+ dirtyReadAt: result.dirtyReadAt ?? last?.dirtyReadAt ?? Date.now(),
358
+ inputs: toUnwatchedInputs(probe, result.summary),
359
+ due: flight.due
360
+ };
361
+ if (this.unwatchedFlights.get(key) === flight && !this.entries.has(key)) this.unwatchedReads.set(key, read);
362
+ return read;
363
+ } catch (ex) {
364
+ if (!isCancellationError(ex) && this.unwatchedFlights.get(key) === flight) this.unwatchedReads.delete(key);
365
+ throw ex;
366
+ }
367
+ }
368
+ /**
369
+ * Reads an unwatched read's inputs: cached answers but for the index file's stat (see {@link UnwatchedInputs}), with
370
+ * the `HEAD` symrefs of `remotes` (every remote's when `undefined`); without `statIndex`, the index is unknown
371
+ */
372
+ async probeUnwatched(repo, remotes, statIndex, signal) {
373
+ const remoteHeads = /* @__PURE__ */ new Map();
374
+ const stat = statIndex ? this.stat : void 0;
375
+ const [current, tips, gitDir] = await Promise.all([
376
+ repo.branches.getBranch(void 0, void 0, signal),
377
+ repo.refs.getRefTips(void 0, signal),
378
+ stat != null ? getGitDir(repo) : void 0,
379
+ readRemoteHeads(repo, remoteHeads, remotes, signal)
380
+ ]);
381
+ let index;
382
+ if (stat != null && gitDir != null) try {
383
+ const stats = await stat(joinPaths(gitDir.uri.fsPath, "index"));
384
+ index = stats != null ? `${stats.size}:${stats.mtime}` : "";
385
+ } catch {
386
+ index = void 0;
387
+ }
388
+ const detached = current?.detached ?? false;
389
+ return {
390
+ branch: detached ? void 0 : current?.name || void 0,
391
+ detached,
392
+ sha: current?.sha,
393
+ upstream: current?.upstream?.name,
394
+ tips: getTipsByName(tips),
395
+ remoteHeads,
396
+ index
397
+ };
191
398
  }
192
399
  getResourceUsage() {
193
400
  return { "worktreeSummary.entries.count": this.entries.size };
@@ -209,6 +416,15 @@ var WorktreeSummaryService = class {
209
416
  this.fail(entry, /* @__PURE__ */ new Error(`Unable to watch '${entry.key}': no repository or git directory`));
210
417
  return;
211
418
  }
419
+ let last = this.unwatchedReads.get(entry.key);
420
+ this.unwatchedReads.delete(entry.key);
421
+ if (last?.result.summary.stale) last = void 0;
422
+ if (last != null) {
423
+ entry.seed = last.result.summary;
424
+ entry.baseKeys = last.result.baseKeys;
425
+ entry.baseConfigKey = last.result.baseConfigKey;
426
+ entry.baseDefaultBranch = last.result.baseDefaultBranch;
427
+ } else entry.pending |= 23;
212
428
  entry.handle = handle;
213
429
  entry.slotsKey = getRepositoryKey(gitDir.commonUri ?? gitDir.uri);
214
430
  const session = handle.session;
@@ -227,10 +443,56 @@ var WorktreeSummaryService = class {
227
443
  workingTreeSub
228
444
  ];
229
445
  for (const subscription of entry.subscribers) subscription.attach(session);
446
+ if (last != null) {
447
+ await this.queueSeedStart(entry, last, this.watchService.isWorkingTreeStarting(entry.key));
448
+ if (entry.disposed) return;
449
+ }
230
450
  await this.drain(entry);
231
451
  }
452
+ /**
453
+ * Queues the groups of the saved read `last` whose inputs moved since, as an unwatched read finds them, and the
454
+ * dirty flag, which no input vouches for; everything when its inputs can't be read. With `deferDirty`, the dirty
455
+ * flag is left to the rescan of a working-tree watch still starting, and the value keeps `last`'s until then
456
+ */
457
+ async queueSeedStart(entry, last, deferDirty) {
458
+ const signal = entry.abort.signal;
459
+ let moved;
460
+ try {
461
+ const repo = this.getRepository(entry.key);
462
+ if (repo != null) {
463
+ const probe = await this.probeUnwatched(repo, getHeadRemotes(last.result.summary.baseDependsOn), false, signal);
464
+ if (entry.disposed) return;
465
+ moved = await getMovedFields(repo, last, probe, true, signal);
466
+ }
467
+ } catch {}
468
+ if (entry.disposed) return;
469
+ if (moved == null) {
470
+ dropSeed(entry);
471
+ entry.pending |= 23;
472
+ return;
473
+ }
474
+ if (deferDirty) {
475
+ entry.pending |= moved.fields & -2;
476
+ entry.dirtyReadAt = last.dirtyReadAt;
477
+ entry.dirtyReadTimer = setTimeout(() => {
478
+ entry.dirtyReadTimer = void 0;
479
+ this.queue(entry, dirtyField);
480
+ }, maxDeferredDirtyMs);
481
+ if (entry.pending === 0 && entry.seed != null) {
482
+ const seed = entry.seed;
483
+ entry.error = void 0;
484
+ entry.seed = void 0;
485
+ entry.value = seed;
486
+ this.fireChange(entry, seed);
487
+ }
488
+ } else entry.pending |= moved.fields;
489
+ if (moved.baseMoves != null) {
490
+ entry.baseMoves ??= /* @__PURE__ */ new Set();
491
+ for (const ref of moved.baseMoves) entry.baseMoves.add(ref);
492
+ }
493
+ }
232
494
  onRepoChanged(entry, e) {
233
- const value = entry.value;
495
+ const value = entry.value ?? entry.seed;
234
496
  if (value == null || e.changed("unknown", "head")) {
235
497
  this.queue(entry, allFields | (e.changed("config") ? baseConfigField : 0));
236
498
  return;
@@ -267,14 +529,28 @@ var WorktreeSummaryService = class {
267
529
  if (entry.handle == null) continue;
268
530
  const value = entry.value;
269
531
  if (value != null && (value.branch == null || e.refs != null && !e.refs.has(value.branch))) continue;
270
- const commonPath = this.getCommonPath?.(entry.key);
271
- if (commonPath != null && commonPath !== e.repoPath) continue;
532
+ if (!this.isInRepository(entry.key, e.repoPath)) continue;
272
533
  this.queue(entry, baseKeysField);
273
534
  }
535
+ for (const read of this.unwatchedReads.values()) {
536
+ const value = read.result.summary;
537
+ if (value.branch == null || e.refs != null && !e.refs.has(value.branch)) continue;
538
+ if (!this.isInRepository(value.repoPath, e.repoPath)) continue;
539
+ read.due |= baseKeysField;
540
+ }
541
+ for (const flight of this.unwatchedFlights.values()) {
542
+ if (!this.isInRepository(flight.key, e.repoPath)) continue;
543
+ flight.due |= baseKeysField;
544
+ }
545
+ }
546
+ isInRepository(key, commonPath) {
547
+ const path = this.getCommonPath?.(key);
548
+ return path == null || path === commonPath;
274
549
  }
275
550
  queue(entry, fields, baseMoves) {
276
551
  if (entry.disposed || fields === 0) return;
277
- entry.pending |= entry.value == null || entry.value.stale ? fields | 23 : fields;
552
+ const value = entry.value ?? entry.seed;
553
+ entry.pending |= value == null || value.stale ? fields | 23 : fields;
278
554
  if (baseMoves != null) {
279
555
  entry.baseMoves ??= /* @__PURE__ */ new Set();
280
556
  for (const ref of baseMoves) entry.baseMoves.add(ref);
@@ -336,7 +612,8 @@ var WorktreeSummaryService = class {
336
612
  return true;
337
613
  }
338
614
  async run(entry, fields, baseMoves) {
339
- const previous = entry.value;
615
+ const { value, seed } = entry;
616
+ const previous = value ?? seed;
340
617
  let result;
341
618
  try {
342
619
  const repo = this.getRepository(entry.key);
@@ -350,16 +627,27 @@ var WorktreeSummaryService = class {
350
627
  if (entry.disposed) return;
351
628
  if (result.summary.stale && (previous == null || !previous.stale)) _logger.warn(`WorktreeSummaryService: reading the base of '${entry.key}' failed: ${String(result.baseError)}`);
352
629
  entry.error = void 0;
630
+ entry.seed = void 0;
353
631
  entry.baseKeys = result.baseKeys;
354
632
  entry.baseConfigKey = entry.pending & baseConfigField ? void 0 : result.baseConfigKey;
355
633
  entry.baseDefaultBranch = result.baseDefaultBranch;
356
- if (previous != null && summariesEqual(previous, result.summary)) return;
357
- entry.value = result.summary;
358
- this.fireChange(entry, result.summary);
634
+ const dirtyConfirmed = result.dirtyReadAt != null && entry.dirtyReadAt != null;
635
+ if (dirtyConfirmed) clearDeferredDirty(entry);
636
+ if (value != null && summariesEqual(value, result.summary)) {
637
+ if (dirtyConfirmed) this.fireChange(entry, value);
638
+ return;
639
+ }
640
+ const summary = value == null && seed != null && summariesEqual(seed, result.summary) ? seed : result.summary;
641
+ entry.value = summary;
642
+ this.fireChange(entry, summary);
359
643
  }
360
644
  fail(entry, ex) {
361
645
  const previous = entry.value;
362
646
  if (previous == null) {
647
+ if (entry.seed != null) {
648
+ dropSeed(entry);
649
+ if (entry.pending !== 0) entry.pending |= 23;
650
+ }
363
651
  if (entry.error == null) _logger.warn(`WorktreeSummaryService: reading '${entry.key}' failed: ${String(ex)}`);
364
652
  entry.error = ex instanceof Error ? ex : new Error(String(ex));
365
653
  return;
@@ -394,6 +682,8 @@ var WorktreeSummaryService = class {
394
682
  entry.handle?.dispose();
395
683
  entry.handle = void 0;
396
684
  entry.value = void 0;
685
+ entry.seed = void 0;
686
+ clearDeferredDirty(entry);
397
687
  entry.baseKeys = void 0;
398
688
  entry.baseConfigKey = void 0;
399
689
  entry.baseDefaultBranch = void 0;
@@ -412,6 +702,19 @@ function* getConfigRelevantRefs(value) {
412
702
  if (value.upstream != null) yield `refs/heads/${value.upstream.name}`;
413
703
  yield* value.baseDependsOn;
414
704
  }
705
+ /** Drops the saved read a first run was to start from, with the values its base read consulted and its dirty flag */
706
+ function dropSeed(entry) {
707
+ entry.seed = void 0;
708
+ clearDeferredDirty(entry);
709
+ entry.baseKeys = void 0;
710
+ entry.baseConfigKey = void 0;
711
+ entry.baseDefaultBranch = void 0;
712
+ }
713
+ function clearDeferredDirty(entry) {
714
+ entry.dirtyReadAt = void 0;
715
+ clearTimeout(entry.dirtyReadTimer);
716
+ entry.dirtyReadTimer = void 0;
717
+ }
415
718
  function createEntry(key) {
416
719
  return {
417
720
  key,
@@ -421,6 +724,9 @@ function createEntry(key) {
421
724
  handle: void 0,
422
725
  listeners: void 0,
423
726
  value: void 0,
727
+ seed: void 0,
728
+ dirtyReadAt: void 0,
729
+ dirtyReadTimer: void 0,
424
730
  baseKeys: void 0,
425
731
  baseConfigKey: void 0,
426
732
  baseDefaultBranch: void 0,
@@ -443,7 +749,12 @@ function createEntry(key) {
443
749
  */
444
750
  async function readSummary(repo, key, fields, previous, previousBaseKeys, previousBaseConfigKey, previousBaseDefaultBranch, baseMoves, signal, verifyBaseRemeasure) {
445
751
  if (previous == null) fields |= allFields;
446
- let dirtyRead = fields & dirtyField ? readDirty(repo, signal) : void 0;
752
+ let dirtyReadAt;
753
+ const startDirtyRead = () => {
754
+ dirtyReadAt = Date.now();
755
+ return readDirty(repo, signal);
756
+ };
757
+ let dirtyRead = fields & dirtyField ? startDirtyRead() : void 0;
447
758
  let branch = previous?.branch;
448
759
  let detached = previous?.detached ?? false;
449
760
  let sha = previous?.sha;
@@ -460,7 +771,7 @@ async function readSummary(repo, key, fields, previous, previousBaseKeys, previo
460
771
  behind = current?.upstream?.state.behind ?? 0;
461
772
  if (previous != null && (branch !== previous.branch || detached !== previous.detached)) {
462
773
  fields |= baseField;
463
- dirtyRead ??= readDirty(repo, signal);
774
+ dirtyRead ??= startDirtyRead();
464
775
  }
465
776
  }
466
777
  let base = previous?.base;
@@ -555,7 +866,123 @@ async function readSummary(repo, key, fields, previous, previousBaseKeys, previo
555
866
  baseKeys,
556
867
  baseConfigKey,
557
868
  baseDefaultBranch,
558
- baseError
869
+ baseError,
870
+ dirtyReadAt
871
+ };
872
+ }
873
+ async function getGitDir(repo) {
874
+ try {
875
+ return await repo.config.getGitDir?.();
876
+ } catch (ex) {
877
+ if (isCancellationError(ex)) throw ex;
878
+ return;
879
+ }
880
+ }
881
+ /** Each ref list's tips by full name, built once per list, which core's cache shares across a repository's worktrees */
882
+ const tipsByNameCache = /* @__PURE__ */ new WeakMap();
883
+ function getTipsByName(tips) {
884
+ let byName = tipsByNameCache.get(tips);
885
+ if (byName == null) {
886
+ const map = /* @__PURE__ */ new Map();
887
+ for (const tip of tips) map.set(tip.fullName, tip.sha);
888
+ byName = map;
889
+ tipsByNameCache.set(tips, byName);
890
+ }
891
+ return byName;
892
+ }
893
+ /**
894
+ * The read groups due on a read from `last`, given what `probe` found of its inputs since (with the refs the base
895
+ * depends on that moved): those `last` made due, and those whose inputs moved
896
+ */
897
+ async function getMovedFields(repo, last, probe, probeDirty, signal) {
898
+ let fields = last.due;
899
+ let baseMoves;
900
+ const { inputs } = last;
901
+ const previous = last.result.summary;
902
+ if (probe.branch !== inputs.branch || probe.detached !== inputs.detached || probe.sha !== inputs.sha) fields |= allFields;
903
+ else {
904
+ if (probe.upstream !== inputs.upstream) fields |= 18;
905
+ else if (getUpstreamTips(probe.tips, probe.upstream) !== inputs.upstreamTips) fields |= branchField;
906
+ for (const ref of previous.baseDependsOn) {
907
+ const value = getBaseInput(probe, ref);
908
+ if (value !== null && value === inputs.baseTips.get(ref)) continue;
909
+ baseMoves ??= /* @__PURE__ */ new Set();
910
+ baseMoves.add(ref);
911
+ }
912
+ if (baseMoves != null) fields |= baseMoveField;
913
+ if ((fields & baseConfigField) === 0 && last.result.baseConfigKey != null && repo.branches.getAheadOfBase != null && (probe.detached || probe.branch != null)) {
914
+ const configKey = getBaseConfigKey(await readBaseConfig(repo, signal), previous);
915
+ if (configKey != null && configKey !== last.result.baseConfigKey) fields |= 18;
916
+ }
917
+ }
918
+ if (probeDirty || probe.index == null || probe.index !== inputs.index) fields |= dirtyField;
919
+ return {
920
+ fields,
921
+ baseMoves
922
+ };
923
+ }
924
+ /** The upstream's tips as a remote-tracking and as a local branch, since its short name doesn't say which */
925
+ function getUpstreamTips(tips, upstream) {
926
+ if (upstream == null) return void 0;
927
+ return `${tips.get(`refs/remotes/${upstream}`) ?? ""}\0${tips.get(`refs/heads/${upstream}`) ?? ""}`;
928
+ }
929
+ /**
930
+ * Reads where the `HEAD` symref of each of `remotes` (every remote's when `undefined`) points (`origin/main`) into
931
+ * `heads`, as core's cached local reads answer it, skipping those already there: `undefined` when it has none (a
932
+ * remote since removed included), `null` when the read failed
933
+ */
934
+ async function readRemoteHeads(repo, heads, remotes, signal) {
935
+ if (remotes?.size === 0) return;
936
+ const names = remotes ?? (await repo.remotes.getRemotes(void 0, signal)).map((r) => r.name);
937
+ await Promise.all(Array.from(names, async (name) => {
938
+ if (heads.has(name)) return;
939
+ let target;
940
+ try {
941
+ target = await repo.branches.getDefaultBranchName(name, { local: true }, signal);
942
+ } catch (ex) {
943
+ if (isCancellationError(ex)) throw ex;
944
+ target = null;
945
+ }
946
+ heads.set(name, target);
947
+ }));
948
+ }
949
+ /** The remote whose `HEAD` symref `ref` is; `undefined` for any other ref */
950
+ function getHeadRemote(ref) {
951
+ if (!ref.startsWith("refs/remotes/") || !ref.endsWith("/HEAD")) return void 0;
952
+ return ref.slice(13, -5);
953
+ }
954
+ /** The remotes whose `HEAD` symref is among `refs` */
955
+ function getHeadRemotes(refs) {
956
+ const remotes = /* @__PURE__ */ new Set();
957
+ for (const ref of refs) {
958
+ const remote = getHeadRemote(ref);
959
+ if (remote != null) remotes.add(remote);
960
+ }
961
+ return remotes;
962
+ }
963
+ /**
964
+ * What `probe` found of `ref`, a ref the base depends on: for a remote's `HEAD` symref, which the ref tips leave out,
965
+ * the ref it points at, since its tip follows that ref's, itself a dependency; else its tip. `null` for a symref of a
966
+ * remote it didn't read: only when the read found HEAD elsewhere than the probe did and read the base for that, which
967
+ * the next probe sees as HEAD moving anyway
968
+ */
969
+ function getBaseInput(probe, ref) {
970
+ const remote = getHeadRemote(ref);
971
+ if (remote == null) return probe.tips.get(ref);
972
+ return probe.remoteHeads.has(remote) ? probe.remoteHeads.get(remote) : null;
973
+ }
974
+ /** The inputs `summary` was read from, as the probe before its read found them */
975
+ function toUnwatchedInputs(probe, summary) {
976
+ const baseTips = /* @__PURE__ */ new Map();
977
+ for (const ref of summary.baseDependsOn) baseTips.set(ref, getBaseInput(probe, ref));
978
+ return {
979
+ branch: probe.branch,
980
+ detached: probe.detached,
981
+ sha: probe.sha,
982
+ upstream: probe.upstream,
983
+ upstreamTips: getUpstreamTips(probe.tips, probe.upstream),
984
+ baseTips,
985
+ index: probe.index
559
986
  };
560
987
  }
561
988
  async function readBaseConfig(repo, signal) {