@gitkraken/core-gitlens 0.4.101 → 0.5.100

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 (66) hide show
  1. package/CHANGELOG.md +11 -6
  2. package/dist/git/models/graph.d.ts +63 -33
  3. package/dist/git/models/graph.d.ts.map +1 -1
  4. package/dist/git/models/graph.js.map +1 -1
  5. package/dist/git/models/graphSession.d.ts +3 -84
  6. package/dist/git/models/graphSession.d.ts.map +1 -1
  7. package/dist/git/models/graphSession.js +1 -6
  8. package/dist/git/models/graphSession.js.map +1 -1
  9. package/dist/git/providers/graph.d.ts +1 -15
  10. package/dist/git/providers/graph.d.ts.map +1 -1
  11. package/dist/git/providers/pausedOperations.d.ts +2 -0
  12. package/dist/git/providers/pausedOperations.d.ts.map +1 -1
  13. package/dist/git/utils/reachability.utils.d.ts +1 -1
  14. package/dist/git/utils/reachability.utils.js +1 -1
  15. package/dist/git-cli/providers/graph.d.ts +1 -3
  16. package/dist/git-cli/providers/graph.d.ts.map +1 -1
  17. package/dist/git-cli/providers/graph.js +37 -214
  18. package/dist/git-cli/providers/graph.js.map +1 -1
  19. package/dist/git-cli/providers/pausedOperations.d.ts +1 -0
  20. package/dist/git-cli/providers/pausedOperations.d.ts.map +1 -1
  21. package/dist/git-cli/providers/pausedOperations.js +21 -1
  22. package/dist/git-cli/providers/pausedOperations.js.map +1 -1
  23. package/dist/git-cli/providers/staging.d.ts.map +1 -1
  24. package/dist/git-cli/providers/staging.js +10 -1
  25. package/dist/git-cli/providers/staging.js.map +1 -1
  26. package/dist/plus/git-github/providers/github/graph.d.ts +1 -3
  27. package/dist/plus/git-github/providers/github/graph.d.ts.map +1 -1
  28. package/dist/plus/git-github/providers/github/graph.js +8 -10
  29. package/dist/plus/git-github/providers/github/graph.js.map +1 -1
  30. package/dist/plus/integrations/authentication/integrationAuthenticationProvider.d.ts +38 -48
  31. package/dist/plus/integrations/authentication/integrationAuthenticationProvider.d.ts.map +1 -1
  32. package/dist/plus/integrations/authentication/integrationAuthenticationProvider.js +22 -5
  33. package/dist/plus/integrations/authentication/integrationAuthenticationProvider.js.map +1 -1
  34. package/dist/plus/integrations/authentication/rejectedTokenTracker.d.ts +39 -0
  35. package/dist/plus/integrations/authentication/rejectedTokenTracker.d.ts.map +1 -0
  36. package/dist/plus/integrations/authentication/rejectedTokenTracker.js +53 -0
  37. package/dist/plus/integrations/authentication/rejectedTokenTracker.js.map +1 -0
  38. package/dist/plus/integrations/models/gitHostIntegration.d.ts.map +1 -1
  39. package/dist/plus/integrations/models/gitHostIntegration.js +19 -6
  40. package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
  41. package/dist/plus/integrations/models/integration.d.ts +20 -0
  42. package/dist/plus/integrations/models/integration.d.ts.map +1 -1
  43. package/dist/plus/integrations/models/integration.js +61 -4
  44. package/dist/plus/integrations/models/integration.js.map +1 -1
  45. package/dist/plus/integrations/models/issuesIntegration.js +5 -5
  46. package/dist/plus/integrations/models/issuesIntegration.js.map +1 -1
  47. package/dist/utils/date.d.ts +1 -1
  48. package/dist/utils/date.d.ts.map +1 -1
  49. package/dist/utils/date.js +5 -3
  50. package/dist/utils/date.js.map +1 -1
  51. package/package.json +1 -1
  52. package/src/git/models/graph.ts +59 -41
  53. package/src/git/models/graphSession.ts +3 -106
  54. package/src/git/providers/graph.ts +1 -15
  55. package/src/git/providers/pausedOperations.ts +8 -1
  56. package/src/git/utils/reachability.utils.ts +1 -1
  57. package/src/git-cli/providers/graph.ts +37 -250
  58. package/src/git-cli/providers/pausedOperations.ts +29 -2
  59. package/src/git-cli/providers/staging.ts +8 -1
  60. package/src/plus/git-github/providers/github/graph.ts +8 -15
  61. package/src/plus/integrations/authentication/integrationAuthenticationProvider.ts +62 -40
  62. package/src/plus/integrations/authentication/rejectedTokenTracker.ts +56 -0
  63. package/src/plus/integrations/models/gitHostIntegration.ts +19 -6
  64. package/src/plus/integrations/models/integration.ts +68 -5
  65. package/src/plus/integrations/models/issuesIntegration.ts +5 -5
  66. package/src/utils/date.ts +6 -3
@@ -25,15 +25,11 @@ import type {
25
25
  GitGraphSearchResultData,
26
26
  GitGraphSearchResults,
27
27
  } from '../../git/models/graphSearch.js';
28
- import { graphSessionSnapshotVersion } from '../../git/models/graphSession.js';
29
28
  import type {
30
29
  GitGraphSession,
31
30
  GitGraphSessionChangedChannels,
32
31
  GitGraphSessionRefreshOptions,
33
32
  GitGraphSessionRefreshResult,
34
- GitGraphSessionSnapshot,
35
- GraphSessionRestoreDiscardReason,
36
- GraphSessionRestoreResult,
37
33
  } from '../../git/models/graphSession.js';
38
34
  import type { GitRemote } from '../../git/models/remote.js';
39
35
  import type { SearchQuery } from '../../git/models/search.js';
@@ -195,130 +191,12 @@ function downstreamsChanged(prior: ReadonlyMap<string, string[]>, next: Readonly
195
191
  return false;
196
192
  }
197
193
 
198
- /**
199
- * Cap on the number of window rows a {@link GraphSession.serialize} persists. A longer window persists only
200
- * its TOP slice with `hasMore` forced true (restore then behaves as a shorter loaded window — the bottom
201
- * re-pages on demand). Pragmatic: generous enough that a typical session restores its whole window, bounded
202
- * enough that the JSON file stays sub-MB-class (rows are a few hundred bytes each).
203
- */
204
- const maxPersistedGraphRows = 2000;
205
-
206
194
  /** The walk-shape key (`ordering|onlyFollowFirstParent`) an incremental seed is gated on — one builder so the
207
- * restore/refresh/rebuild sites can't drift in how they stringify it. */
195
+ * refresh/rebuild sites can't drift in how they stringify it. */
208
196
  function buildShapeKey(shape: { ordering: string; onlyFollowFirstParent: boolean }): string {
209
197
  return `${shape.ordering}|${shape.onlyFollowFirstParent}`;
210
198
  }
211
199
 
212
- /**
213
- * Validates a persisted {@link GitGraphSessionSnapshot} structurally before it's trusted to seed a restore.
214
- * Returns the discard reason on any failure (→ the caller ignores the snapshot and does a normal initial
215
- * walk), or `undefined` when it's coherent. Never throws — a corrupt/adversarial cache must degrade to a
216
- * full walk, never crash the open. Deep validation (does the cache match current git?) is NOT done here:
217
- * that's the restore's immediate refresh, which reconciles a stale-but-valid snapshot against ground truth.
218
- */
219
- function validateGraphSnapshot(
220
- snapshot: GitGraphSessionSnapshot,
221
- repoPath: string,
222
- currentBuildShape: string,
223
- ): GraphSessionRestoreDiscardReason | undefined {
224
- if (snapshot.v !== graphSessionSnapshotVersion) return 'schema';
225
- if (snapshot.repoPath !== repoPath) return 'repo-path';
226
- // A shape change reshapes every row's parents/order — the cached rows can't be reused.
227
- if (snapshot.buildShape !== currentBuildShape) return 'shape';
228
- if (!Array.isArray(snapshot.rows) || snapshot.rows.length === 0) return 'empty';
229
- if (
230
- !Array.isArray(snapshot.refTips) ||
231
- snapshot.refTips.some(
232
- t => !Array.isArray(t) || t.length !== 2 || typeof t[0] !== 'string' || typeof t[1] !== 'string',
233
- )
234
- ) {
235
- return 'tips';
236
- }
237
-
238
- // Each row must be structurally sane — a corrupt row poisons the seed / stitch math. Duplicate shas break
239
- // the ids set / stitch dedup (they'd re-appear as "already seen"), so reject them too.
240
- const seenShas = new Set<string>();
241
- for (const row of snapshot.rows) {
242
- if (row == null || typeof row.sha !== 'string' || !Array.isArray(row.parents) || typeof row.date !== 'number') {
243
- return 'rows';
244
- }
245
- if (seenShas.has(row.sha)) return 'rows';
246
-
247
- seenShas.add(row.sha);
248
- }
249
-
250
- // Every row's `reachabilityIndex` must be a valid index into the table's `sets` (the decode reads it
251
- // directly). Guards against a truncated table / mismatched pairing.
252
- const reachability = snapshot.reachability;
253
- if (reachability != null && (!Array.isArray(reachability.sets) || !Array.isArray(reachability.dictionary))) {
254
- return 'reachability';
255
- }
256
-
257
- const setCount = reachability?.sets?.length ?? 0;
258
- for (const row of snapshot.rows) {
259
- const index = row.contexts?.reachabilityIndex;
260
- if (index != null && (!Number.isInteger(index) || index < 0 || index >= setCount)) return 'reachability';
261
- }
262
-
263
- // Downstreams must be `[upstream name, tracking branch names]` entries (reconstructed into a Map on restore).
264
- if (
265
- !Array.isArray(snapshot.downstreams) ||
266
- snapshot.downstreams.some(
267
- d => !Array.isArray(d) || d.length !== 2 || typeof d[0] !== 'string' || !Array.isArray(d[1]),
268
- )
269
- ) {
270
- return 'downstreams';
271
- }
272
-
273
- // Per-sha stats, when present, must be `[sha, stats-object]` entries (reconstructed into a Map on restore).
274
- if (
275
- snapshot.rowsStats != null &&
276
- (!Array.isArray(snapshot.rowsStats) ||
277
- snapshot.rowsStats.some(
278
- s =>
279
- !Array.isArray(s) ||
280
- s.length !== 2 ||
281
- typeof s[0] !== 'string' ||
282
- typeof s[1] !== 'object' ||
283
- s[1] == null,
284
- ))
285
- ) {
286
- return 'rowsStats';
287
- }
288
-
289
- return undefined;
290
- }
291
-
292
- /**
293
- * Reconstruct a persisted snapshot into the synthetic `GitGraph` a {@link GraphSession.restore} hands to its
294
- * immediate refresh as the PRIOR generation — carrying exactly the fields the refresh's seed construction +
295
- * change-diffing read (`refTips`, `reachability`, `rowsStats`, `downstreams`, `paging.hasMore`). Avatars are
296
- * deliberately empty: resolved avatar URLs are webview-instance-bound and don't survive a restart, so the
297
- * refresh re-derives them (and the write-once merge over an empty prior map is a no-op). The side-queried
298
- * collections (`branches`/`remotes`/`stashes`/`worktrees`) aren't read by the refresh, so they're left empty.
299
- */
300
- function buildRestoredGraph(snapshot: GitGraphSessionSnapshot): GitGraph {
301
- return {
302
- repoPath: snapshot.repoPath,
303
- avatars: new Map<string, string>(),
304
- ids: new Set<string>(snapshot.rows.map(r => r.sha)),
305
- includes: snapshot.includesStats ? { stats: true } : undefined,
306
- branches: new Map<string, GitBranch>(),
307
- remotes: new Map<string, GitRemote>(),
308
- downstreams: new Map(snapshot.downstreams),
309
- stashes: undefined,
310
- worktrees: undefined,
311
- worktreesByBranch: undefined,
312
- reachability: snapshot.reachability,
313
- refTips: new Map(snapshot.refTips),
314
- decorationFingerprint: snapshot.decorationFingerprint,
315
- shallow: snapshot.shallow,
316
- rows: snapshot.rows,
317
- rowsStats: snapshot.rowsStats != null ? new Map(snapshot.rowsStats) : undefined,
318
- paging: { limit: snapshot.rows.length, startingCursor: undefined, hasMore: snapshot.hasMore },
319
- };
320
- }
321
-
322
200
  /** The graph log parser's per-record shape (see `graphMapping`), narrowed to the fields the row builder reads. */
323
201
  type GraphCommitRecord = {
324
202
  sha: string;
@@ -405,8 +283,6 @@ export class GraphGitSubProvider implements GitGraphSubProvider {
405
283
  rev?: string;
406
284
  limit?: number;
407
285
  include?: { stats?: boolean };
408
- restore?: GitGraphSessionSnapshot;
409
- onRestore?: (result: GraphSessionRestoreResult) => void;
410
286
  },
411
287
  cancellation?: AbortSignal,
412
288
  ): Promise<GitGraphSession> {
@@ -422,30 +298,6 @@ export class GraphGitSubProvider implements GitGraphSubProvider {
422
298
  };
423
299
  const session = new GraphSession(this, repoPath, options?.rowProcessor, getWalkShape);
424
300
 
425
- // R7c restart persistence: seed from a persisted snapshot when it's structurally valid for THIS repo +
426
- // walk shape, then immediately refresh (the snapshot is exactly an R6 seed — enumeration + the tip/FF/
427
- // stash gates reconcile a stale one). Any validation failure discards it and falls through to a normal
428
- // initial walk. Never trusted over git.
429
- const restore = options?.restore;
430
- if (restore != null) {
431
- const shape = getWalkShape();
432
- const reason = validateGraphSnapshot(restore, repoPath, buildShapeKey(shape));
433
- if (reason == null) {
434
- try {
435
- const refresh = await session.restore(restore, options, cancellation);
436
- options?.onRestore?.({ restored: true, rows: restore.rows.length, refresh: refresh });
437
- return session;
438
- } catch {
439
- // Belt-and-suspenders: structural validation can't catch a snapshot that reconstructs but
440
- // throws in the restore's refresh (e.g. a garbage rev its enumerate chokes on). A corrupt
441
- // cache must degrade to a full walk, never crash the open — report a miss and fall through.
442
- options?.onRestore?.({ restored: false, reason: 'corrupt' });
443
- }
444
- } else {
445
- options?.onRestore?.({ restored: false, reason: reason });
446
- }
447
- }
448
-
449
301
  await session.initialize(options, cancellation);
450
302
  return session;
451
303
  }
@@ -810,6 +662,9 @@ export class GraphGitSubProvider implements GitGraphSubProvider {
810
662
  // `defaultLocalName!` is non-null here because `tip === defaultBranchName` short-circuits when `defaultBranchName` is null.
811
663
  isDefault: tip === defaultBranchName && !branchMap.has(defaultLocalName!),
812
664
  hostingServiceType: remote.provider?.gkProviderId,
665
+ // `branchMap` is keyed by full name, so the remote ref resolves here — the same
666
+ // lookup the host does today to bake `+starred` into the context string.
667
+ starred: branchMap.get(tip)?.starred || undefined,
813
668
  });
814
669
  continue;
815
670
  }
@@ -823,7 +678,11 @@ export class GraphGitSubProvider implements GitGraphSubProvider {
823
678
  (branchId === branchIdOfMainWorktree ? mainWorktree : undefined);
824
679
  const worktreeRef =
825
680
  worktree != null
826
- ? { id: getWorktreeId(repoPath, worktree.name), path: worktree.path }
681
+ ? {
682
+ id: getWorktreeId(repoPath, worktree.name),
683
+ path: worktree.path,
684
+ isDefault: worktree.isDefault === true,
685
+ }
827
686
  : undefined;
828
687
  refHeads.push({
829
688
  id: branchId,
@@ -831,11 +690,19 @@ export class GraphGitSubProvider implements GitGraphSubProvider {
831
690
  isCurrentHead: head,
832
691
  upstream:
833
692
  branch?.upstream != null
834
- ? { name: branch.upstream.name, id: getBranchId(repoPath, true, branch.upstream.name) }
693
+ ? {
694
+ name: branch.upstream.name,
695
+ id: getBranchId(repoPath, true, branch.upstream.name),
696
+ missing: branch.upstream.missing,
697
+ state: {
698
+ ahead: branch.upstream.state.ahead,
699
+ behind: branch.upstream.state.behind,
700
+ },
701
+ }
835
702
  : undefined,
836
703
  worktree: worktreeRef,
837
- worktreeId: worktree?.isDefault === false ? worktreeRef?.id : undefined,
838
704
  isDefault: defaultLocalName != null && tip === defaultLocalName,
705
+ starred: branch?.starred || undefined,
839
706
  });
840
707
  }
841
708
  }
@@ -853,10 +720,11 @@ export class GraphGitSubProvider implements GitGraphSubProvider {
853
720
  date: Number(ordering === 'author-date' ? commit.authorDate : commit.committerDate) * 1000,
854
721
  commitDate: Number(commit.committerDate) * 1000,
855
722
  message: (stash.message ?? commit.message).trim(),
856
- type: 'stash-node',
857
- heads: refHeads,
858
- remotes: refRemoteHeads,
859
- tags: refTags,
723
+ kind: 'stash',
724
+ stashNumber: stash.stashNumber,
725
+ heads: refHeads.length ? refHeads : undefined,
726
+ remotes: refRemoteHeads.length ? refRemoteHeads : undefined,
727
+ tags: refTags.length ? refTags : undefined,
860
728
  isCurrentUser: true,
861
729
  };
862
730
  // Don't seed `rowStats` from `stash.stats` here: the graph's stash list is fetched with
@@ -873,10 +741,10 @@ export class GraphGitSubProvider implements GitGraphSubProvider {
873
741
  date: Number(ordering === 'author-date' ? commit.authorDate : commit.committerDate) * 1000,
874
742
  commitDate: Number(commit.committerDate) * 1000,
875
743
  message: commit.message.trim(),
876
- type: parents.length > 1 ? 'merge-node' : 'commit-node',
877
- heads: refHeads,
878
- remotes: refRemoteHeads,
879
- tags: refTags,
744
+ kind: parents.length > 1 ? 'merge' : 'commit',
745
+ heads: refHeads.length ? refHeads : undefined,
746
+ remotes: refRemoteHeads.length ? refRemoteHeads : undefined,
747
+ tags: refTags.length ? refTags : undefined,
880
748
  isCurrentUser: isCurrentUser || undefined,
881
749
  };
882
750
  if (commit.stats != null) {
@@ -1035,7 +903,7 @@ export class GraphGitSubProvider implements GitGraphSubProvider {
1035
903
  continue;
1036
904
  }
1037
905
 
1038
- if (row.type === 'stash-node') {
906
+ if (row.kind === 'stash') {
1039
907
  missingStashStdin += `${row.sha}\n`;
1040
908
  } else {
1041
909
  missingStdin += `${row.sha}\n`;
@@ -1215,7 +1083,7 @@ export class GraphGitSubProvider implements GitGraphSubProvider {
1215
1083
  refRemoteHeads,
1216
1084
  refTags,
1217
1085
  head,
1218
- row.type === 'stash-node',
1086
+ row.kind === 'stash',
1219
1087
  );
1220
1088
  // Transient: the row processor's `+unique` decision reads this; stripped by finalize.
1221
1089
  // Unsorted (the consumer re-sorts after decoding) — order doesn't affect interning.
@@ -1446,7 +1314,7 @@ export class GraphGitSubProvider implements GitGraphSubProvider {
1446
1314
  // Stash-set gate: any push/pop/drop changes the injected rows.
1447
1315
  const currentStashShas =
1448
1316
  gitStash?.stashes != null ? new Set<string>(gitStash.stashes.keys()) : new Set<string>();
1449
- const priorStashShas = seed.rows.filter(r => r.type === 'stash-node').map(r => r.sha);
1317
+ const priorStashShas = seed.rows.filter(r => r.kind === 'stash').map(r => r.sha);
1450
1318
  if (priorStashShas.length !== currentStashShas.size || priorStashShas.some(s => !currentStashShas.has(s))) {
1451
1319
  fallback('stash-changed');
1452
1320
  return undefined;
@@ -1456,9 +1324,9 @@ export class GraphGitSubProvider implements GitGraphSubProvider {
1456
1324
  // upstreams, worktree assignments, remote urls/providers, current user) that can change without
1457
1325
  // moving any ref tip — e.g. `git remote set-head`, `branch --set-upstream-to`, `worktree add`.
1458
1326
  // Reused rows keep their prior decorations (only flags/reachability are re-derived below), so ANY
1459
- // change here must take the full walk that rebuilds them. An absent seed fingerprint (old persisted
1460
- // snapshot) never matches — safe full fallback. Deliberately LAST of the gates so the structural
1461
- // gates above keep their precise reasons (a deleted tracking branch reports `ref-deleted`, not this).
1327
+ // change here must take the full walk that rebuilds them. An absent seed fingerprint never matches —
1328
+ // safe full fallback. Deliberately LAST of the gates so the structural gates above keep their precise
1329
+ // reasons (a deleted tracking branch reports `ref-deleted`, not this).
1462
1330
  if (seed.decorationFingerprint !== decorationFingerprint) {
1463
1331
  fallback('metadata-changed');
1464
1332
  return undefined;
@@ -1617,7 +1485,7 @@ export class GraphGitSubProvider implements GitGraphSubProvider {
1617
1485
  row.remotes ?? [],
1618
1486
  row.tags ?? [],
1619
1487
  row.sha === currentHeadSha,
1620
- row.type === 'stash-node',
1488
+ row.kind === 'stash',
1621
1489
  );
1622
1490
  if (freshShas.has(row.sha)) {
1623
1491
  // Fresh rows are raw — run the full processor (contexts, emojify, avatar). Reused rows keep
@@ -1627,7 +1495,7 @@ export class GraphGitSubProvider implements GitGraphSubProvider {
1627
1495
  }
1628
1496
  // Authoritative flags for every commit row (membership can shift on reused rows too — e.g. a new
1629
1497
  // interior branch flips `+unique`). Stash rows carry none.
1630
- if (row.type !== 'stash-node') {
1498
+ if (row.kind !== 'stash') {
1631
1499
  (row.contexts ??= {}).flags = computeGraphRowContextFlags(row.sha, refs?.values(), graphCtx);
1632
1500
  }
1633
1501
  finalizeRowReachability(row, row.sha, refs);
@@ -2112,39 +1980,6 @@ class GraphSession implements GitGraphSession {
2112
1980
  this.applyRebuild(graph, shape);
2113
1981
  }
2114
1982
 
2115
- /**
2116
- * R7c restart persistence. Reconstruct the prior generation from a persisted snapshot as an R6 seed
2117
- * WITHOUT any git (its window/tips/reachability/stats become this session's prior `current`/`window`/
2118
- * `buildShape`), then IMMEDIATELY {@link refresh} to current truth — the enumeration + tip/FF/stash gates
2119
- * reconcile a stale snapshot (fast when unchanged, a full walk on any structural change). The session
2120
- * handed back is ALWAYS post-refresh; the raw snapshot is never surfaced. The caller has already validated
2121
- * the snapshot structurally. Returns the refresh outcome so the caller can report the restore result.
2122
- */
2123
- async restore(
2124
- snapshot: GitGraphSessionSnapshot,
2125
- options?: { rev?: string; limit?: number; include?: { stats?: boolean } },
2126
- cancellation?: AbortSignal,
2127
- ): Promise<GitGraphSessionRefreshResult> {
2128
- this._window = snapshot.rows;
2129
- this._buildShape = snapshot.buildShape;
2130
- this._current = buildRestoredGraph(snapshot);
2131
-
2132
- // Anchor the re-walk on the window's BOTTOM commit so it spans the whole restored window (the host's
2133
- // rebuild-anchor discipline), matching to at least the current limit so a since-grown repo still fills a
2134
- // full page. Stash rows aren't real revs, so skip them when picking the anchor.
2135
- let anchor: string | undefined;
2136
- for (let i = snapshot.rows.length - 1; i >= 0; i--) {
2137
- const type = snapshot.rows[i].type;
2138
- if (type === 'commit-node' || type === 'merge-node') {
2139
- anchor = snapshot.rows[i].sha;
2140
- break;
2141
- }
2142
- }
2143
- const limit = Math.max(snapshot.rows.length, options?.limit ?? 0);
2144
-
2145
- return this.refresh({ rev: anchor, limit: limit, include: options?.include }, cancellation);
2146
- }
2147
-
2148
1983
  async refresh(
2149
1984
  options?: GitGraphSessionRefreshOptions,
2150
1985
  cancellation?: AbortSignal,
@@ -2266,56 +2101,8 @@ class GraphSession implements GitGraphSession {
2266
2101
  return true;
2267
2102
  }
2268
2103
 
2269
- serialize(): GitGraphSessionSnapshot | undefined {
2270
- const current = this._current;
2271
- // Nothing worth persisting: no window yet, no build shape recorded, or no ref tips (the restore's
2272
- // tip-diff gate has nothing to diff — never true for the CLI provider, but guards a defensive future).
2273
- if (this._window.length === 0 || this._buildShape == null || current?.refTips == null) return undefined;
2274
-
2275
- const [ordering, onlyFollowFirstParent] = this._buildShape.split('|');
2276
-
2277
- // Cap the persisted window to a bounded top slice; a longer window persists its top with `hasMore`
2278
- // forced true (restore behaves as a shorter loaded window — the bottom re-pages on demand). Slicing
2279
- // makes a new array of the SAME row objects — no mutation of the live window.
2280
- const capped = this._window.length > maxPersistedGraphRows;
2281
- const rows = capped ? this._window.slice(0, maxPersistedGraphRows) : (this._window as GitGraphRow[]);
2282
- const hasMore = capped || (current.paging?.hasMore ?? false);
2283
-
2284
- // Stats are immutable per sha; when capped, trim to the persisted shas (entries for dropped rows are
2285
- // dead weight). The reachability table is kept whole even when capped — it's proportional to distinct
2286
- // ref-sets (small), the persisted rows' indices stay valid, and extra sets are harmless on decode.
2287
- let rowsStats: [string, GitGraphRowStats][] | undefined;
2288
- if (current.includes?.stats === true && current.rowsStats != null && current.rowsStats.size > 0) {
2289
- const shas = capped ? new Set(rows.map(r => r.sha)) : undefined;
2290
- rowsStats = [];
2291
- for (const [sha, stats] of current.rowsStats) {
2292
- if (shas != null && !shas.has(sha)) continue;
2293
-
2294
- rowsStats.push([sha, stats]);
2295
- }
2296
- }
2297
-
2298
- return {
2299
- v: graphSessionSnapshotVersion,
2300
- repoPath: this.repoPath,
2301
- buildShape: this._buildShape,
2302
- ordering: ordering as 'date' | 'author-date' | 'topo',
2303
- onlyFollowFirstParent: onlyFollowFirstParent === 'true',
2304
- rows: rows,
2305
- refTips: [...current.refTips],
2306
- decorationFingerprint: current.decorationFingerprint,
2307
- reachability: current.reachability,
2308
- rowsStats: rowsStats,
2309
- downstreams: Array.from(current.downstreams, ([k, v]): [string, string[]] => [k, [...v]]),
2310
- hasMore: hasMore,
2311
- includesStats: current.includes?.stats === true,
2312
- shallow: current.shallow ?? false,
2313
- };
2314
- }
2315
-
2316
2104
  dispose(): void {
2317
- // No-op — the window lives entirely in memory. R7c restart persistence is host-driven: the host
2318
- // serializes this session (debounced + on its own dispose) and owns the storage IO.
2105
+ // No-op — the window lives entirely in memory and dies with the session.
2319
2106
  }
2320
2107
 
2321
2108
  /** A cursor-less rebuild (full walk / fast path) IS the full window. */
@@ -509,7 +509,10 @@ export class PausedOperationsGitSubProvider implements GitPausedOperationsSubPro
509
509
  }
510
510
 
511
511
  @debug()
512
- async continuePausedOperation(repoPath: string, options?: { skip?: boolean }): Promise<void> {
512
+ async continuePausedOperation(
513
+ repoPath: string,
514
+ options?: { skip?: boolean; messageEditor?: string },
515
+ ): Promise<void> {
513
516
  const scope = getScopedLogger();
514
517
 
515
518
  const status = await this.getPausedOperationStatus(repoPath);
@@ -521,8 +524,32 @@ export class PausedOperationsGitSubProvider implements GitPausedOperationsSubPro
521
524
 
522
525
  const args = [status.type, options?.skip ? '--skip' : '--continue'];
523
526
 
527
+ // Continuing after a conflicted step can open the commit-message editor (merge-backend rebases,
528
+ // cherry-picks, and reverts). `GIT_EDITOR` is what git honors for that step, so set it on the
529
+ // environment; also set `core.editor` config as a fallback.
530
+ const configs: string[] = [];
531
+ let env: Record<string, string | undefined> | undefined;
532
+ if (options?.messageEditor) {
533
+ configs.push('-c', `core.editor=${options.messageEditor}`);
534
+ env = { GIT_EDITOR: options.messageEditor };
535
+ }
536
+
524
537
  try {
525
- await this.git.run({ cwd: repoPath, errors: 'throw' }, ...args);
538
+ await this.git.run(
539
+ {
540
+ cwd: repoPath,
541
+ errors: 'throw',
542
+ configs: configs,
543
+ env: env,
544
+ // The runner dedups in-flight commands by args only — it ignores per-call `configs`/`env`.
545
+ // A headless continue (with `GIT_EDITOR`) and a plain `--continue` would therefore collapse
546
+ // into one execution, so the plain one could silently run headless (auto-accepting a commit
547
+ // message) or the headless one could ride a plain continue and open an editor that hangs the
548
+ // loop. Tag the headless variant so it can't share a pending command with the plain one.
549
+ correlationKey: options?.messageEditor ? 'paused-op-continue-headless' : undefined,
550
+ },
551
+ ...args,
552
+ );
526
553
  this.context.hooks?.cache?.onReset?.(repoPath, 'branches', 'status');
527
554
  this.context.hooks?.repository?.onChanged?.(repoPath, ['head', 'heads', 'index']);
528
555
  } catch (ex) {
@@ -58,7 +58,14 @@ export class StagingGitSubProvider implements GitStagingSubProvider {
58
58
  if (gitDir == null) throw new Error(`Unable to determine git directory for ${repoPath}`);
59
59
 
60
60
  const currentIndex = joinPaths(gitDir.uri.fsPath, 'index');
61
- await fs.copyFile(currentIndex, tempIndex);
61
+ try {
62
+ await fs.copyFile(currentIndex, tempIndex);
63
+ } catch (ex) {
64
+ // A repo that has never staged anything has no index file yet; git reads a missing
65
+ // `GIT_INDEX_FILE` as an empty index, which is what 'current' means there. Any other
66
+ // failure is real — don't silently hand back an index claiming everything is deleted.
67
+ if ((ex as NodeJS.ErrnoException)?.code !== 'ENOENT') throw ex;
68
+ }
62
69
  break;
63
70
  }
64
71
  case 'ref': {
@@ -21,8 +21,6 @@ import type {
21
21
  GitGraphSessionChangedChannels,
22
22
  GitGraphSessionRefreshOptions,
23
23
  GitGraphSessionRefreshResult,
24
- GitGraphSessionSnapshot,
25
- GraphSessionRestoreResult,
26
24
  } from '../../../../git/models/graphSession.js';
27
25
  import type { GitLog } from '../../../../git/models/log.js';
28
26
  import type { GitRemote } from '../../../../git/models/remote.js';
@@ -52,14 +50,12 @@ export class GraphGitSubProvider implements GitGraphSubProvider {
52
50
  rev?: string;
53
51
  limit?: number;
54
52
  include?: { stats?: boolean };
55
- restore?: GitGraphSessionSnapshot;
56
- onRestore?: (result: GraphSessionRestoreResult) => void;
57
53
  },
58
54
  cancellation?: AbortSignal,
59
55
  ): Promise<GitGraphSession> {
60
56
  // GitHub-backed graphs have no incremental machinery (and `getGraph` takes no row processor); the
61
- // session is a thin window accumulator over full fetches. `rowProcessor` and `restore`/`onRestore`
62
- // (there's no incremental restore path) are accepted for interface parity and ignored — always a full walk.
57
+ // session is a thin window accumulator over full fetches. `rowProcessor` is accepted for interface
58
+ // parity and ignored — always a full walk.
63
59
  const session = new GraphSession(this, repoPath);
64
60
  await session.initialize(options, cancellation);
65
61
  return session;
@@ -295,10 +291,12 @@ export class GraphGitSubProvider implements GitGraphSubProvider {
295
291
  date: commit.committer.date.getTime(),
296
292
  commitDate: commit.committer.date.getTime(),
297
293
  message: commit.message?.length ? commit.message : commit.summary,
298
- type: commit.parents.length > 1 ? 'merge-node' : 'commit-node',
299
- heads: refHeads,
300
- remotes: refRemoteHeads,
301
- tags: refTags,
294
+ kind: commit.parents.length > 1 ? 'merge' : 'commit',
295
+ // Omitted rather than empty: an empty array still counts as "present" to the splice
296
+ // fingerprint, which defeats its bare-row skip and makes every ref-less row look changeable.
297
+ heads: refHeads.length ? refHeads : undefined,
298
+ remotes: refRemoteHeads.length ? refRemoteHeads : undefined,
299
+ tags: refTags.length ? refTags : undefined,
302
300
  });
303
301
 
304
302
  if (commit.stats != null) {
@@ -623,11 +621,6 @@ class GraphSession implements GitGraphSession {
623
621
  return true;
624
622
  }
625
623
 
626
- serialize(): GitGraphSessionSnapshot | undefined {
627
- // No incremental restore path (no ref-tip gate / reachability table), so nothing worth persisting.
628
- return undefined;
629
- }
630
-
631
624
  dispose(): void {
632
625
  // No-op — the window lives entirely in memory.
633
626
  }