@gitkraken/core-gitlens 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -25,6 +25,7 @@ const allFields = 7;
25
25
  const maxConcurrentReadsPerRepository = 4;
26
26
  const maxConcurrentReads = 8;
27
27
  const maxUnwatchedReads = 256;
28
+ const maxDeferredDirtyMs = 1e4;
28
29
  const emptyRefs = Object.freeze([]);
29
30
  const noBaseKeys = [
30
31
  void 0,
@@ -66,6 +67,9 @@ var SummarySubscription = class {
66
67
  get current() {
67
68
  return this.disposed ? void 0 : this.entry.value;
68
69
  }
70
+ get dirtyReadAt() {
71
+ return this.disposed || this.entry.value == null ? void 0 : this.entry.dirtyReadAt;
72
+ }
69
73
  get onDidChange() {
70
74
  return this.emitter.event;
71
75
  }
@@ -101,7 +105,10 @@ var SummarySubscription = class {
101
105
  * has working changes, and how far it is past its base — kept current by the repository watch layer.
102
106
  *
103
107
  * **Lifecycle.** Entries are per repository key and ref-counted across {@link subscribe} calls. The first subscriber
104
- * 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
105
112
  * watch and drops the value. Each subscriber's timing options join the watch session's cadence (the shortest wins);
106
113
  * the service's own listening subscriptions never constrain it.
107
114
  *
@@ -128,7 +135,8 @@ var SummarySubscription = class {
128
135
  * **Value-change-only notification.** {@link WorktreeSummarySubscription.onDidChange} and {@link onDidChangeSummary}
129
136
  * fire only when some field's value differs from the previous summary — a recompute that reads the same answer is
130
137
  * silent, so a consumer can re-render on every event without diffing. A failed recompute keeps the last values and
131
- * 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).
132
140
  *
133
141
  * **Worktrees without a subscription.** {@link getSummary} and {@link getSummarySnapshot} answer one from its last read
134
142
  * (kept for the {@link maxUnwatchedReads} most recently asked), re-reading only the groups whose inputs moved since:
@@ -140,11 +148,14 @@ var SummarySubscription = class {
140
148
  * asks about this way for others' changes to show. Its working tree is unwatched, so an edit that leaves the index
141
149
  * alone goes unseen until `dirty` is read again: the snapshot says when that was, and `probeDirty` reads it anyway.
142
150
  * Concurrent reads of a worktree share one, and take no read slot: each is a caller's foreground read, which never
143
- * waits behind subscribed worktrees' runs.
151
+ * waits behind subscribed worktrees' runs. A first subscriber takes over the last read.
144
152
  */
145
153
  var WorktreeSummaryService = class {
146
154
  _onDidChangeSummary = new Emitter();
147
- /** 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
+ */
148
159
  get onDidChangeSummary() {
149
160
  return this._onDidChangeSummary.event;
150
161
  }
@@ -192,7 +203,11 @@ var WorktreeSummaryService = class {
192
203
  this.entries.clear();
193
204
  this._onDidChangeSummary.dispose();
194
205
  }
195
- /** 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
+ */
196
211
  subscribe(repoPath, options) {
197
212
  const key = getRepositoryKey(repoPath);
198
213
  let entry = this._disposed ? void 0 : this.entries.get(key);
@@ -207,16 +222,11 @@ var WorktreeSummaryService = class {
207
222
  target.subscribers.add(subscription);
208
223
  if (isNew) {
209
224
  if (!target.disposed) {
210
- this.unwatchedReads.delete(key);
211
225
  this.unwatchedFlights.delete(key);
212
- target.pending = 23;
213
226
  target.running = this.start(target);
214
227
  }
215
228
  } else if (target.handle != null) subscription.attach(target.handle.session);
216
- else if (target.running == null && !target.disposed) {
217
- target.pending = 23;
218
- target.running = this.start(target);
219
- }
229
+ else if (target.running == null && !target.disposed) target.running = this.start(target);
220
230
  return subscription;
221
231
  }
222
232
  /**
@@ -232,7 +242,8 @@ var WorktreeSummaryService = class {
232
242
  }
233
243
  /**
234
244
  * Returns a worktree's summary, with when its dirty flag was read. A subscribed worktree answers as
235
- * {@link getSummary} does, with `dirtyReadAt` `undefined`, since its summary follows its working tree. Any other
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
236
247
  * answers from its last read, re-reading only the groups whose inputs moved since (no git when none did), and in
237
248
  * full the first time, or after a failed or stale read. Its working tree is unwatched, so the dirty flag is re-read
238
249
  * only when the index file moved, or with `probeDirty`; a rejection drops the last read. Its inputs are core's
@@ -242,9 +253,17 @@ var WorktreeSummaryService = class {
242
253
  const key = getRepositoryKey(repoPath);
243
254
  const entry = this.entries.get(key);
244
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);
263
+ }
245
264
  return {
246
- summary: await this.getSubscribedSummary(entry, options?.cancellation),
247
- dirtyReadAt: void 0
265
+ summary,
266
+ dirtyReadAt: entry.dirtyReadAt
248
267
  };
249
268
  }
250
269
  async getSubscribedSummary(entry, cancellation) {
@@ -269,10 +288,7 @@ var WorktreeSummaryService = class {
269
288
  if (flight === waited) flight = void 0;
270
289
  }
271
290
  const entry = this.entries.get(key);
272
- if (entry != null) return {
273
- summary: await this.getSubscribedSummary(entry, cancellation),
274
- dirtyReadAt: void 0
275
- };
291
+ if (entry != null) return this.getSubscribedSnapshot(entry, true, cancellation);
276
292
  }
277
293
  if (flight == null) {
278
294
  flight = new UnwatchedFlight(key, probeDirty, this._disposed, (f) => this.readUnwatched(f));
@@ -313,33 +329,14 @@ var WorktreeSummaryService = class {
313
329
  const repo = this.getRepository(key);
314
330
  if (repo == null) throw new Error(`No repository found for '${key}'`);
315
331
  const headRemotes = last != null ? getHeadRemotes(last.result.summary.baseDependsOn) : void 0;
316
- const probe = await this.probeUnwatched(repo, headRemotes, signal);
332
+ const probe = await this.probeUnwatched(repo, headRemotes, true, signal);
317
333
  if (flight.disposed) throw new CancellationError();
318
334
  let fields;
319
335
  let baseMoves;
320
336
  if (last == null) fields = 23;
321
337
  else {
322
- fields = last.due;
323
- const { inputs } = last;
324
- const previous = last.result.summary;
325
- if (probe.branch !== inputs.branch || probe.detached !== inputs.detached || probe.sha !== inputs.sha) fields |= allFields;
326
- else {
327
- if (probe.upstream !== inputs.upstream) fields |= 18;
328
- else if (getUpstreamTips(probe.tips, probe.upstream) !== inputs.upstreamTips) fields |= branchField;
329
- for (const ref of previous.baseDependsOn) {
330
- const value = getBaseInput(probe, ref);
331
- if (value !== null && value === inputs.baseTips.get(ref)) continue;
332
- baseMoves ??= /* @__PURE__ */ new Set();
333
- baseMoves.add(ref);
334
- }
335
- if (baseMoves != null) fields |= baseMoveField;
336
- if ((fields & baseConfigField) === 0 && last.result.baseConfigKey != null && repo.branches.getAheadOfBase != null && (probe.detached || probe.branch != null)) {
337
- const configKey = getBaseConfigKey(await readBaseConfig(repo, signal), previous);
338
- if (flight.disposed) throw new CancellationError();
339
- if (configKey != null && configKey !== last.result.baseConfigKey) fields |= 18;
340
- }
341
- }
342
- if (flight.probeDirty || probe.index == null || probe.index !== inputs.index) fields |= dirtyField;
338
+ ({fields, baseMoves} = await getMovedFields(repo, last, probe, flight.probeDirty, signal));
339
+ if (flight.disposed) throw new CancellationError();
343
340
  if (fields === 0) {
344
341
  this.unwatchedReads.touch(key);
345
342
  return last;
@@ -370,18 +367,18 @@ var WorktreeSummaryService = class {
370
367
  }
371
368
  /**
372
369
  * Reads an unwatched read's inputs: cached answers but for the index file's stat (see {@link UnwatchedInputs}), with
373
- * the `HEAD` symrefs of `remotes` (every remote's when `undefined`)
370
+ * the `HEAD` symrefs of `remotes` (every remote's when `undefined`); without `statIndex`, the index is unknown
374
371
  */
375
- async probeUnwatched(repo, remotes, signal) {
372
+ async probeUnwatched(repo, remotes, statIndex, signal) {
376
373
  const remoteHeads = /* @__PURE__ */ new Map();
374
+ const stat = statIndex ? this.stat : void 0;
377
375
  const [current, tips, gitDir] = await Promise.all([
378
376
  repo.branches.getBranch(void 0, void 0, signal),
379
377
  repo.refs.getRefTips(void 0, signal),
380
- getGitDir(repo),
378
+ stat != null ? getGitDir(repo) : void 0,
381
379
  readRemoteHeads(repo, remoteHeads, remotes, signal)
382
380
  ]);
383
381
  let index;
384
- const stat = this.stat;
385
382
  if (stat != null && gitDir != null) try {
386
383
  const stats = await stat(joinPaths(gitDir.uri.fsPath, "index"));
387
384
  index = stats != null ? `${stats.size}:${stats.mtime}` : "";
@@ -419,6 +416,15 @@ var WorktreeSummaryService = class {
419
416
  this.fail(entry, /* @__PURE__ */ new Error(`Unable to watch '${entry.key}': no repository or git directory`));
420
417
  return;
421
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;
422
428
  entry.handle = handle;
423
429
  entry.slotsKey = getRepositoryKey(gitDir.commonUri ?? gitDir.uri);
424
430
  const session = handle.session;
@@ -437,10 +443,56 @@ var WorktreeSummaryService = class {
437
443
  workingTreeSub
438
444
  ];
439
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
+ }
440
450
  await this.drain(entry);
441
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
+ }
442
494
  onRepoChanged(entry, e) {
443
- const value = entry.value;
495
+ const value = entry.value ?? entry.seed;
444
496
  if (value == null || e.changed("unknown", "head")) {
445
497
  this.queue(entry, allFields | (e.changed("config") ? baseConfigField : 0));
446
498
  return;
@@ -497,7 +549,8 @@ var WorktreeSummaryService = class {
497
549
  }
498
550
  queue(entry, fields, baseMoves) {
499
551
  if (entry.disposed || fields === 0) return;
500
- 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;
501
554
  if (baseMoves != null) {
502
555
  entry.baseMoves ??= /* @__PURE__ */ new Set();
503
556
  for (const ref of baseMoves) entry.baseMoves.add(ref);
@@ -559,7 +612,8 @@ var WorktreeSummaryService = class {
559
612
  return true;
560
613
  }
561
614
  async run(entry, fields, baseMoves) {
562
- const previous = entry.value;
615
+ const { value, seed } = entry;
616
+ const previous = value ?? seed;
563
617
  let result;
564
618
  try {
565
619
  const repo = this.getRepository(entry.key);
@@ -573,16 +627,27 @@ var WorktreeSummaryService = class {
573
627
  if (entry.disposed) return;
574
628
  if (result.summary.stale && (previous == null || !previous.stale)) _logger.warn(`WorktreeSummaryService: reading the base of '${entry.key}' failed: ${String(result.baseError)}`);
575
629
  entry.error = void 0;
630
+ entry.seed = void 0;
576
631
  entry.baseKeys = result.baseKeys;
577
632
  entry.baseConfigKey = entry.pending & baseConfigField ? void 0 : result.baseConfigKey;
578
633
  entry.baseDefaultBranch = result.baseDefaultBranch;
579
- if (previous != null && summariesEqual(previous, result.summary)) return;
580
- entry.value = result.summary;
581
- 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);
582
643
  }
583
644
  fail(entry, ex) {
584
645
  const previous = entry.value;
585
646
  if (previous == null) {
647
+ if (entry.seed != null) {
648
+ dropSeed(entry);
649
+ if (entry.pending !== 0) entry.pending |= 23;
650
+ }
586
651
  if (entry.error == null) _logger.warn(`WorktreeSummaryService: reading '${entry.key}' failed: ${String(ex)}`);
587
652
  entry.error = ex instanceof Error ? ex : new Error(String(ex));
588
653
  return;
@@ -617,6 +682,8 @@ var WorktreeSummaryService = class {
617
682
  entry.handle?.dispose();
618
683
  entry.handle = void 0;
619
684
  entry.value = void 0;
685
+ entry.seed = void 0;
686
+ clearDeferredDirty(entry);
620
687
  entry.baseKeys = void 0;
621
688
  entry.baseConfigKey = void 0;
622
689
  entry.baseDefaultBranch = void 0;
@@ -635,6 +702,19 @@ function* getConfigRelevantRefs(value) {
635
702
  if (value.upstream != null) yield `refs/heads/${value.upstream.name}`;
636
703
  yield* value.baseDependsOn;
637
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
+ }
638
718
  function createEntry(key) {
639
719
  return {
640
720
  key,
@@ -644,6 +724,9 @@ function createEntry(key) {
644
724
  handle: void 0,
645
725
  listeners: void 0,
646
726
  value: void 0,
727
+ seed: void 0,
728
+ dirtyReadAt: void 0,
729
+ dirtyReadTimer: void 0,
647
730
  baseKeys: void 0,
648
731
  baseConfigKey: void 0,
649
732
  baseDefaultBranch: void 0,
@@ -807,6 +890,37 @@ function getTipsByName(tips) {
807
890
  }
808
891
  return byName;
809
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
+ }
810
924
  /** The upstream's tips as a remote-tracking and as a local branch, since its short name doesn't say which */
811
925
  function getUpstreamTips(tips, upstream) {
812
926
  if (upstream == null) return void 0;