@gitkraken/core-gitlens 0.1.0 → 0.2.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 (223) hide show
  1. package/CHANGELOG.md +22 -1
  2. package/dist/git/cache.d.ts +21 -5
  3. package/dist/git/cache.d.ts.map +1 -1
  4. package/dist/git/cache.js +102 -6
  5. package/dist/git/cache.js.map +1 -1
  6. package/dist/git/context.d.ts +11 -2
  7. package/dist/git/context.d.ts.map +1 -1
  8. package/dist/git/context.js.map +1 -1
  9. package/dist/git/models/branch.d.ts +1 -0
  10. package/dist/git/models/branch.d.ts.map +1 -1
  11. package/dist/git/models/branch.js.map +1 -1
  12. package/dist/git/models/commit.d.ts +1 -9
  13. package/dist/git/models/commit.d.ts.map +1 -1
  14. package/dist/git/models/commit.js +4 -9
  15. package/dist/git/models/commit.js.map +1 -1
  16. package/dist/git/models/graph.d.ts +2 -4
  17. package/dist/git/models/graph.d.ts.map +1 -1
  18. package/dist/git/models/repository.d.ts +12 -3
  19. package/dist/git/models/repository.d.ts.map +1 -1
  20. package/dist/git/models/repository.js +25 -4
  21. package/dist/git/models/repository.js.map +1 -1
  22. package/dist/git/models/worktree.d.ts +1 -1
  23. package/dist/git/models/worktree.d.ts.map +1 -1
  24. package/dist/git/models/worktree.js +2 -2
  25. package/dist/git/models/worktree.js.map +1 -1
  26. package/dist/git/parsers/diffParser.d.ts +0 -1
  27. package/dist/git/parsers/diffParser.d.ts.map +1 -1
  28. package/dist/git/parsers/diffParser.js +22 -61
  29. package/dist/git/parsers/diffParser.js.map +1 -1
  30. package/dist/git/providers/branches.d.ts +3 -0
  31. package/dist/git/providers/branches.d.ts.map +1 -1
  32. package/dist/git/providers/commits.d.ts +6 -0
  33. package/dist/git/providers/commits.d.ts.map +1 -1
  34. package/dist/git/providers/config.d.ts +2 -0
  35. package/dist/git/providers/config.d.ts.map +1 -1
  36. package/dist/git/providers/diff.d.ts +2 -0
  37. package/dist/git/providers/diff.d.ts.map +1 -1
  38. package/dist/git/providers/operations.d.ts +6 -0
  39. package/dist/git/providers/operations.d.ts.map +1 -1
  40. package/dist/git/providers/revision.d.ts +1 -1
  41. package/dist/git/providers/revision.d.ts.map +1 -1
  42. package/dist/git/providers/staging.d.ts +2 -0
  43. package/dist/git/providers/staging.d.ts.map +1 -1
  44. package/dist/git/utils/sorting.d.ts +6 -0
  45. package/dist/git/utils/sorting.d.ts.map +1 -1
  46. package/dist/git/utils/sorting.js +19 -0
  47. package/dist/git/utils/sorting.js.map +1 -1
  48. package/dist/git-cli/cliGitProvider.js +1 -1
  49. package/dist/git-cli/cliGitProvider.js.map +1 -1
  50. package/dist/git-cli/exec/git.d.ts +29 -4
  51. package/dist/git-cli/exec/git.d.ts.map +1 -1
  52. package/dist/git-cli/exec/git.js +70 -9
  53. package/dist/git-cli/exec/git.js.map +1 -1
  54. package/dist/git-cli/exec/gitQueue.d.ts +12 -4
  55. package/dist/git-cli/exec/gitQueue.d.ts.map +1 -1
  56. package/dist/git-cli/exec/gitQueue.js +49 -12
  57. package/dist/git-cli/exec/gitQueue.js.map +1 -1
  58. package/dist/git-cli/parsers/logParser.d.ts +1 -0
  59. package/dist/git-cli/parsers/logParser.d.ts.map +1 -1
  60. package/dist/git-cli/parsers/logParser.js +4 -0
  61. package/dist/git-cli/parsers/logParser.js.map +1 -1
  62. package/dist/git-cli/providers/blame.js +1 -1
  63. package/dist/git-cli/providers/blame.js.map +1 -1
  64. package/dist/git-cli/providers/branches.d.ts +2 -0
  65. package/dist/git-cli/providers/branches.d.ts.map +1 -1
  66. package/dist/git-cli/providers/branches.js +176 -123
  67. package/dist/git-cli/providers/branches.js.map +1 -1
  68. package/dist/git-cli/providers/commits.d.ts +4 -0
  69. package/dist/git-cli/providers/commits.d.ts.map +1 -1
  70. package/dist/git-cli/providers/commits.js +58 -17
  71. package/dist/git-cli/providers/commits.js.map +1 -1
  72. package/dist/git-cli/providers/config.d.ts.map +1 -1
  73. package/dist/git-cli/providers/config.js +9 -9
  74. package/dist/git-cli/providers/config.js.map +1 -1
  75. package/dist/git-cli/providers/contributors.d.ts.map +1 -1
  76. package/dist/git-cli/providers/contributors.js +7 -4
  77. package/dist/git-cli/providers/contributors.js.map +1 -1
  78. package/dist/git-cli/providers/diff.d.ts +2 -0
  79. package/dist/git-cli/providers/diff.d.ts.map +1 -1
  80. package/dist/git-cli/providers/diff.js +89 -19
  81. package/dist/git-cli/providers/diff.js.map +1 -1
  82. package/dist/git-cli/providers/graph.d.ts.map +1 -1
  83. package/dist/git-cli/providers/graph.js +43 -27
  84. package/dist/git-cli/providers/graph.js.map +1 -1
  85. package/dist/git-cli/providers/operations.d.ts +17 -0
  86. package/dist/git-cli/providers/operations.d.ts.map +1 -1
  87. package/dist/git-cli/providers/operations.js +47 -14
  88. package/dist/git-cli/providers/operations.js.map +1 -1
  89. package/dist/git-cli/providers/patch.d.ts.map +1 -1
  90. package/dist/git-cli/providers/patch.js +15 -30
  91. package/dist/git-cli/providers/patch.js.map +1 -1
  92. package/dist/git-cli/providers/pausedOperations.js +6 -6
  93. package/dist/git-cli/providers/pausedOperations.js.map +1 -1
  94. package/dist/git-cli/providers/refs.d.ts.map +1 -1
  95. package/dist/git-cli/providers/refs.js +26 -7
  96. package/dist/git-cli/providers/refs.js.map +1 -1
  97. package/dist/git-cli/providers/remotes.js +4 -4
  98. package/dist/git-cli/providers/remotes.js.map +1 -1
  99. package/dist/git-cli/providers/revision.d.ts +1 -1
  100. package/dist/git-cli/providers/revision.d.ts.map +1 -1
  101. package/dist/git-cli/providers/revision.js +15 -11
  102. package/dist/git-cli/providers/revision.js.map +1 -1
  103. package/dist/git-cli/providers/staging.d.ts +2 -0
  104. package/dist/git-cli/providers/staging.d.ts.map +1 -1
  105. package/dist/git-cli/providers/staging.js +20 -8
  106. package/dist/git-cli/providers/staging.js.map +1 -1
  107. package/dist/git-cli/providers/stash.d.ts +0 -8
  108. package/dist/git-cli/providers/stash.d.ts.map +1 -1
  109. package/dist/git-cli/providers/stash.js +63 -119
  110. package/dist/git-cli/providers/stash.js.map +1 -1
  111. package/dist/git-cli/providers/status.js +6 -6
  112. package/dist/git-cli/providers/status.js.map +1 -1
  113. package/dist/git-cli/providers/tags.js +4 -4
  114. package/dist/git-cli/providers/tags.js.map +1 -1
  115. package/dist/git-cli/providers/worktrees.js +3 -3
  116. package/dist/git-cli/providers/worktrees.js.map +1 -1
  117. package/dist/plus/ai/constants.d.ts +2 -1
  118. package/dist/plus/ai/constants.d.ts.map +1 -1
  119. package/dist/plus/ai/constants.js +7 -0
  120. package/dist/plus/ai/constants.js.map +1 -1
  121. package/dist/plus/ai/models/model.d.ts +1 -1
  122. package/dist/plus/ai/models/model.d.ts.map +1 -1
  123. package/dist/plus/ai/models/model.js.map +1 -1
  124. package/dist/plus/ai/models/promptTemplates.d.ts +19 -2
  125. package/dist/plus/ai/models/promptTemplates.d.ts.map +1 -1
  126. package/dist/plus/ai/models/results.d.ts +28 -0
  127. package/dist/plus/ai/models/results.d.ts.map +1 -1
  128. package/dist/plus/ai/prompts.d.ts +3 -0
  129. package/dist/plus/ai/prompts.d.ts.map +1 -1
  130. package/dist/plus/ai/prompts.js +126 -0
  131. package/dist/plus/ai/prompts.js.map +1 -1
  132. package/dist/plus/ai/utils/ai.utils.d.ts.map +1 -1
  133. package/dist/plus/ai/utils/ai.utils.js +2 -0
  134. package/dist/plus/ai/utils/ai.utils.js.map +1 -1
  135. package/dist/plus/ai/utils/results.utils.d.ts +3 -0
  136. package/dist/plus/ai/utils/results.utils.d.ts.map +1 -1
  137. package/dist/plus/ai/utils/results.utils.js +75 -0
  138. package/dist/plus/ai/utils/results.utils.js.map +1 -1
  139. package/dist/plus/ai/utils/truncation.utils.d.ts +1 -1
  140. package/dist/plus/ai/utils/truncation.utils.d.ts.map +1 -1
  141. package/dist/plus/ai/utils/truncation.utils.js.map +1 -1
  142. package/dist/plus/git-github/providers/github/branches.d.ts.map +1 -1
  143. package/dist/plus/git-github/providers/github/branches.js +16 -1
  144. package/dist/plus/git-github/providers/github/branches.js.map +1 -1
  145. package/dist/plus/git-github/providers/github/diff.d.ts +2 -0
  146. package/dist/plus/git-github/providers/github/diff.d.ts.map +1 -1
  147. package/dist/plus/git-github/providers/github/diff.js.map +1 -1
  148. package/dist/utils/decorators/log.js +1 -1
  149. package/dist/utils/decorators/log.js.map +1 -1
  150. package/dist/utils/decorators/sequentialize.d.ts.map +1 -1
  151. package/dist/utils/decorators/sequentialize.js +4 -2
  152. package/dist/utils/decorators/sequentialize.js.map +1 -1
  153. package/dist/utils/env/node/base64.js.map +1 -1
  154. package/dist/utils/env/node/exec.js.map +1 -1
  155. package/dist/utils/formatter.d.ts.map +1 -1
  156. package/dist/utils/formatter.js +4 -10
  157. package/dist/utils/formatter.js.map +1 -1
  158. package/dist/utils/iterable.js.map +1 -1
  159. package/dist/utils/lruMap.d.ts +40 -0
  160. package/dist/utils/lruMap.d.ts.map +1 -0
  161. package/dist/utils/lruMap.js +81 -0
  162. package/dist/utils/lruMap.js.map +1 -0
  163. package/dist/utils/version.d.ts +14 -0
  164. package/dist/utils/version.d.ts.map +1 -1
  165. package/dist/utils/version.js +48 -0
  166. package/dist/utils/version.js.map +1 -1
  167. package/package.json +1 -1
  168. package/src/git/cache.ts +179 -13
  169. package/src/git/context.ts +11 -2
  170. package/src/git/models/branch.ts +1 -0
  171. package/src/git/models/commit.ts +1 -17
  172. package/src/git/models/graph.ts +2 -4
  173. package/src/git/models/repository.ts +26 -6
  174. package/src/git/models/worktree.ts +2 -2
  175. package/src/git/parsers/diffParser.ts +7 -44
  176. package/src/git/providers/branches.ts +3 -0
  177. package/src/git/providers/commits.ts +7 -0
  178. package/src/git/providers/config.ts +2 -0
  179. package/src/git/providers/diff.ts +8 -2
  180. package/src/git/providers/operations.ts +10 -3
  181. package/src/git/providers/revision.ts +5 -1
  182. package/src/git/providers/staging.ts +2 -0
  183. package/src/git/utils/sorting.ts +21 -0
  184. package/src/git-cli/cliGitProvider.ts +1 -1
  185. package/src/git-cli/exec/git.ts +70 -18
  186. package/src/git-cli/exec/gitQueue.ts +57 -13
  187. package/src/git-cli/parsers/logParser.ts +5 -1
  188. package/src/git-cli/providers/blame.ts +1 -1
  189. package/src/git-cli/providers/branches.ts +207 -133
  190. package/src/git-cli/providers/commits.ts +71 -18
  191. package/src/git-cli/providers/config.ts +9 -14
  192. package/src/git-cli/providers/contributors.ts +7 -12
  193. package/src/git-cli/providers/diff.ts +100 -19
  194. package/src/git-cli/providers/graph.ts +47 -27
  195. package/src/git-cli/providers/operations.ts +77 -18
  196. package/src/git-cli/providers/patch.ts +15 -37
  197. package/src/git-cli/providers/pausedOperations.ts +6 -6
  198. package/src/git-cli/providers/refs.ts +27 -8
  199. package/src/git-cli/providers/remotes.ts +4 -4
  200. package/src/git-cli/providers/revision.ts +20 -13
  201. package/src/git-cli/providers/staging.ts +18 -8
  202. package/src/git-cli/providers/stash.ts +67 -134
  203. package/src/git-cli/providers/status.ts +6 -6
  204. package/src/git-cli/providers/tags.ts +4 -4
  205. package/src/git-cli/providers/worktrees.ts +3 -3
  206. package/src/plus/ai/constants.ts +8 -0
  207. package/src/plus/ai/models/model.ts +1 -0
  208. package/src/plus/ai/models/promptTemplates.ts +29 -0
  209. package/src/plus/ai/models/results.ts +30 -0
  210. package/src/plus/ai/prompts.ts +129 -0
  211. package/src/plus/ai/utils/ai.utils.ts +2 -0
  212. package/src/plus/ai/utils/results.utils.ts +99 -0
  213. package/src/plus/ai/utils/truncation.utils.ts +3 -1
  214. package/src/plus/git-github/providers/github/branches.ts +17 -5
  215. package/src/plus/git-github/providers/github/diff.ts +8 -2
  216. package/src/utils/decorators/log.ts +1 -1
  217. package/src/utils/decorators/sequentialize.ts +8 -6
  218. package/src/utils/env/node/base64.ts +1 -1
  219. package/src/utils/env/node/exec.ts +2 -2
  220. package/src/utils/formatter.ts +4 -11
  221. package/src/utils/iterable.ts +1 -1
  222. package/src/utils/lruMap.ts +87 -0
  223. package/src/utils/version.ts +48 -0
@@ -99,7 +99,7 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
99
99
  ? this.provider.pausedOps?.getPausedOperationStatus(repoPath, cancellation)
100
100
  : undefined,
101
101
  this.git
102
- .exec(
102
+ .run(
103
103
  {
104
104
  cwd: repoPath,
105
105
  cancellation: cancellation,
@@ -172,7 +172,7 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
172
172
  const parser = getBranchParser(supported);
173
173
 
174
174
  const [gitResult, defaultWorktreePathResult, metadataMapResult] = await Promise.allSettled([
175
- this.git.exec(
175
+ this.git.run(
176
176
  { cwd: commonPath, cancellation: signal },
177
177
  'for-each-ref',
178
178
  ...parser.arguments,
@@ -365,50 +365,60 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
365
365
  ): Promise<BranchContributionsOverview | undefined> {
366
366
  const scope = getScopedLogger();
367
367
 
368
- try {
369
- const branch = await this.getBranch(repoPath, ref, cancellation);
370
- if (branch == null) return undefined;
371
-
368
+ const getCore = async (
369
+ _commonPath: string,
370
+ _cacheable: CacheController | undefined,
371
+ signal: AbortSignal | undefined,
372
+ storedTarget: string | undefined,
373
+ ): Promise<BranchContributionsOverview | undefined> => {
372
374
  let mergeTarget: string | undefined;
373
375
 
374
- // Tier 1: Check stored merge target (user-set or previously detected from PR)
375
- const storedTarget = await this.getStoredMergeTargetBranchName(repoPath, ref);
376
+ // Tier 1: Stored merge target (user-set or previously detected from PR). Caller has
377
+ // already read this once for the cache decision; reuse rather than re-fetching.
376
378
  if (storedTarget) {
377
379
  const validated = await this.provider.refs.getSymbolicReferenceName(repoPath, storedTarget);
378
380
  mergeTarget = validated || storedTarget;
379
381
  }
380
382
 
381
- // Tier 2: PR-based lookup (caller provides the PR promise)
383
+ // Tier 2: PR-based lookup. The branch object is only needed here (for `remoteName`),
384
+ // so lazy-load it on the cold path instead of paying for it on every call.
382
385
  if (mergeTarget == null && options?.associatedPullRequest != null) {
383
386
  const pr = await options.associatedPullRequest;
384
387
  if (pr?.refs?.base != null) {
388
+ const branch = await this.getBranch(repoPath, ref, signal);
389
+ if (branch == null) return undefined;
385
390
  mergeTarget = `${branch.remoteName}/${pr.refs.base.branch}`;
386
- // Store for future reuse
391
+ // Store for future reuse — turns the next call into a Tier 1 cache-eligible path.
387
392
  void this.storeMergeTargetBranchName(repoPath, ref, mergeTarget);
388
393
  }
389
394
  }
390
395
 
391
396
  // Tier 3: Base branch, then default branch
392
- mergeTarget ??= await this.getBaseBranchName(repoPath, ref, cancellation);
393
- mergeTarget ??= await this.getDefaultBranchName(repoPath, undefined, cancellation);
397
+ mergeTarget ??= await this.getBaseBranchName(repoPath, ref, signal);
398
+ mergeTarget ??= await this.getDefaultBranchName(repoPath, undefined, signal);
394
399
 
395
400
  if (mergeTarget == null) return undefined;
396
401
 
397
- const mergeBase = await this.provider.refs.getMergeBase(
398
- repoPath,
399
- ref,
400
- mergeTarget,
401
- undefined,
402
- cancellation,
403
- );
402
+ const mergeBase = await this.provider.refs.getMergeBase(repoPath, ref, mergeTarget, undefined, signal);
404
403
  if (mergeBase == null) return undefined;
405
404
 
406
- const result = await this.provider.contributors.getContributors(
407
- repoPath,
408
- createRevisionRange(mergeBase, ref, '..'),
409
- { stats: true },
410
- cancellation,
411
- );
405
+ // Fetch the merge-base commit's dates in parallel with contributors so consumers
406
+ // (e.g. the graph minimap scope window) don't pay a separate round-trip for them.
407
+ // `allSettled` so a transient failure in one side doesn't drop the overview entirely.
408
+ const [contributorsSettled, mergeBaseDatesSettled] = await Promise.allSettled([
409
+ this.provider.contributors.getContributors(
410
+ repoPath,
411
+ createRevisionRange(mergeBase, ref, '..'),
412
+ { stats: true },
413
+ signal,
414
+ ),
415
+ this.provider.commits.getCommitDates(repoPath, mergeBase, signal),
416
+ ]);
417
+
418
+ if (signal?.aborted) throw new CancellationError();
419
+
420
+ const result = getSettledValue(contributorsSettled) ?? { contributors: [] };
421
+ const mergeBaseDates = getSettledValue(mergeBaseDatesSettled);
412
422
 
413
423
  sortContributors(result.contributors, { orderBy: 'score:desc' });
414
424
 
@@ -446,6 +456,7 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
446
456
  branch: ref,
447
457
  mergeTarget: mergeTarget,
448
458
  mergeBase: mergeBase,
459
+ mergeBaseDate: mergeBaseDates?.committerDate,
449
460
 
450
461
  commits: totalCommits,
451
462
  files: totalFiles,
@@ -457,6 +468,23 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
457
468
 
458
469
  contributors: result.contributors,
459
470
  };
471
+ };
472
+
473
+ try {
474
+ // Pre-check Tier 1: when a stored merge target exists the result is deterministic for
475
+ // `ref` (Tier 2's PR resolution is short-circuited), so it's safe to cache. Otherwise
476
+ // the result depends on PR resolution which the cache key can't capture — bypass.
477
+ const storedTarget = await this.getStoredMergeTargetBranchName(repoPath, ref);
478
+ if (storedTarget != null) {
479
+ return await this.cache.getBranchOverview(
480
+ repoPath,
481
+ ref,
482
+ (commonPath, cacheable, signal) =>
483
+ getCore(commonPath, cacheable, signal ?? cancellation, storedTarget),
484
+ { cancellation: cancellation },
485
+ );
486
+ }
487
+ return await getCore(repoPath, undefined, cancellation, undefined);
460
488
  } catch (ex) {
461
489
  scope?.error(ex);
462
490
  if (isCancellationError(ex)) throw ex;
@@ -520,7 +548,7 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
520
548
  params.push(options.name);
521
549
  }
522
550
 
523
- return this.git.exec(
551
+ return this.git.run(
524
552
  {
525
553
  cwd: repoPath,
526
554
  cancellation: cancellation,
@@ -563,7 +591,7 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
563
591
  let data: [string, string | undefined] | undefined;
564
592
  let result;
565
593
  try {
566
- result = await this.git.exec(
594
+ result = await this.git.run(
567
595
  { cwd: repoPath, cancellation: signal, errors: 'throw' },
568
596
  'rev-parse',
569
597
  '--abbrev-ref',
@@ -582,7 +610,7 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
582
610
  data = [ex.stdout, undefined];
583
611
  } else {
584
612
  try {
585
- result = await this.git.exec(
613
+ result = await this.git.run(
586
614
  { cwd: repoPath, cancellation: signal },
587
615
  'symbolic-ref',
588
616
  '--short',
@@ -629,7 +657,7 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
629
657
  }
630
658
  }
631
659
  } else if (GitWarnings.headNotABranch.test(msg)) {
632
- result = await this.git.exec(
660
+ result = await this.git.run(
633
661
  {
634
662
  cwd: repoPath,
635
663
  cancellation: signal,
@@ -687,7 +715,7 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
687
715
  let retried = false;
688
716
  while (true) {
689
717
  try {
690
- const result = await this.git.exec(
718
+ const result = await this.git.run(
691
719
  { cwd: commonPath, cancellation: cancellation },
692
720
  'symbolic-ref',
693
721
  '--short',
@@ -699,7 +727,7 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
699
727
  try {
700
728
  if (!retried) {
701
729
  retried = true;
702
- await this.git.exec(
730
+ await this.git.run(
703
731
  { cwd: commonPath, cancellation: cancellation },
704
732
  'remote',
705
733
  'set-head',
@@ -709,7 +737,7 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
709
737
  continue;
710
738
  }
711
739
 
712
- const result = await this.git.exec(
740
+ const result = await this.git.run(
713
741
  { cwd: commonPath, cancellation: cancellation },
714
742
  'ls-remote',
715
743
  '--symref',
@@ -741,7 +769,7 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
741
769
  args.push('--no-track');
742
770
  }
743
771
  try {
744
- await this.git.exec({ cwd: repoPath }, ...args);
772
+ await this.git.run({ cwd: repoPath }, ...args);
745
773
  this.context.hooks?.cache?.onReset?.(repoPath, 'branches');
746
774
  this.context.hooks?.repository?.onChanged?.(repoPath, ['heads']);
747
775
  } catch (ex) {
@@ -767,7 +795,7 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
767
795
  const branches = ensureArray(names);
768
796
  const args = ['branch', options?.force ? '-D' : '-d', ...branches];
769
797
  try {
770
- await this.git.exec({ cwd: repoPath }, ...args);
798
+ await this.git.run({ cwd: repoPath }, ...args);
771
799
  this.context.hooks?.cache?.onReset?.(repoPath, 'branches');
772
800
  this.context.hooks?.repository?.onChanged?.(repoPath, ['heads']);
773
801
  } catch (ex) {
@@ -795,7 +823,7 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
795
823
  const branches = ensureArray(names);
796
824
  const args = ['push', '-d', remote, ...branches];
797
825
  try {
798
- await this.git.exec({ cwd: repoPath }, ...args);
826
+ await this.git.run({ cwd: repoPath }, ...args);
799
827
  this.context.hooks?.cache?.onReset?.(repoPath, 'branches');
800
828
  this.context.hooks?.repository?.onChanged?.(repoPath, ['remotes']);
801
829
  } catch (ex) {
@@ -815,42 +843,67 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
815
843
  }
816
844
 
817
845
  @debug()
818
- async getBranchMergedStatus(
846
+ getBranchMergedStatus(
819
847
  repoPath: string,
820
848
  branch: GitBranchReference,
821
849
  into: GitBranchReference,
822
850
  cancellation?: AbortSignal,
823
851
  ): Promise<GitBranchMergedStatus> {
824
852
  if (branch.name === into.name || branch.upstream?.name === into.name) {
825
- return { merged: false };
853
+ return Promise.resolve({ merged: false });
826
854
  }
827
855
 
828
- const result = await this.getBranchMergedStatusCore(repoPath, branch, into, cancellation);
829
- if (result.merged) return result;
856
+ // Cache key omits repoPath so worktrees of the same common repo share the cached answer.
857
+ // Branch refs (`refs/heads/*` / `refs/remotes/*`) are unique within a common repo, so
858
+ // `name` + `remote` flag is the minimal stable identity.
859
+ const cacheKey = `${branch.remote ? 'r' : 'l'}:${branch.name}|${into.remote ? 'r' : 'l'}:${into.name}`;
830
860
 
831
- // If the branch we are checking is a remote branch, check if it has been merged into its local branch (if there is one)
832
- if (into.remote) {
833
- const localIntoBranch = await this.getLocalBranchByUpstream(repoPath, into.name, cancellation);
834
- // If there is a local branch and it is not the branch we are checking, check if it has been merged into it
835
- if (localIntoBranch != null && localIntoBranch.name !== branch.name) {
836
- const result = await this.getBranchMergedStatusCore(repoPath, branch, localIntoBranch, cancellation);
837
- if (result.merged) {
838
- return {
839
- ...result,
840
- localBranchOnly: createReference(localIntoBranch.ref, localIntoBranch.repoPath, {
841
- id: localIntoBranch.id,
842
- refType: 'branch',
843
- name: localIntoBranch.name,
844
- remote: localIntoBranch.remote,
845
- upstream: localIntoBranch.upstream,
846
- sha: localIntoBranch.sha,
847
- }),
848
- };
861
+ return this.cache.getBranchMergedStatus(
862
+ repoPath,
863
+ cacheKey,
864
+ async (commonPath, _cacheable, signal) => {
865
+ const result = await this.getBranchMergedStatusCore(commonPath, branch, into, signal);
866
+ if (result.merged) return result;
867
+
868
+ // If the branch we are checking is a remote branch, check if it has been merged into its local branch (if there is one)
869
+ if (into.remote) {
870
+ const localIntoBranch = await this.getLocalBranchByUpstream(commonPath, into.name, signal);
871
+ // If there is a local branch and it is not the branch we are checking, check if it has been merged into it
872
+ if (localIntoBranch != null && localIntoBranch.name !== branch.name) {
873
+ // Skip the second full merge-check cycle when the local branch points at the same commit as the remote —
874
+ // the merge-base/cherry/diff/apply pipeline would produce the same answer we already got for `into` above
875
+ if (localIntoBranch.sha != null && localIntoBranch.sha === into.sha) {
876
+ return { merged: false };
877
+ }
878
+
879
+ const result = await this.getBranchMergedStatusCore(
880
+ commonPath,
881
+ branch,
882
+ localIntoBranch,
883
+ signal,
884
+ );
885
+ if (result.merged) {
886
+ // `localBranchOnly` is built against `commonPath` here; the cache mapper rewrites
887
+ // `repoPath`/`id` to the requesting worktree on retrieval.
888
+ return {
889
+ ...result,
890
+ localBranchOnly: createReference(localIntoBranch.ref, localIntoBranch.repoPath, {
891
+ id: localIntoBranch.id,
892
+ refType: 'branch',
893
+ name: localIntoBranch.name,
894
+ remote: localIntoBranch.remote,
895
+ upstream: localIntoBranch.upstream,
896
+ sha: localIntoBranch.sha,
897
+ }),
898
+ };
899
+ }
900
+ }
849
901
  }
850
- }
851
- }
852
902
 
853
- return { merged: false };
903
+ return { merged: false };
904
+ },
905
+ cancellation,
906
+ );
854
907
  }
855
908
 
856
909
  private async getBranchMergedStatusCore(
@@ -864,7 +917,7 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
864
917
  try {
865
918
  // Check if branch is direct ancestor (handles FF merges)
866
919
  try {
867
- await this.git.exec(
920
+ await this.git.run(
868
921
  { cwd: repoPath, cancellation: cancellation, errors: 'throw' },
869
922
  'merge-base',
870
923
  '--is-ancestor',
@@ -877,7 +930,7 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
877
930
  }
878
931
 
879
932
  // Cherry-pick detection (handles cherry-picks, rebases, etc)
880
- let result = await this.git.exec(
933
+ let result = await this.git.run(
881
934
  { cwd: repoPath, cancellation: cancellation },
882
935
  'cherry',
883
936
  '--abbrev',
@@ -904,7 +957,7 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
904
957
  undefined,
905
958
  cancellation,
906
959
  );
907
- result = await this.git.exec({ cwd: repoPath, cancellation: cancellation }, 'diff', mergeBase, branch.name);
960
+ result = await this.git.run({ cwd: repoPath, cancellation: cancellation }, 'diff', mergeBase, branch.name);
908
961
  if (result.stdout) {
909
962
  // Create a temporary index file
910
963
  await using disposableIndex = await this.provider.staging.createTemporaryIndex(
@@ -914,7 +967,7 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
914
967
  );
915
968
  const { env } = disposableIndex;
916
969
 
917
- result = await this.git.exec(
970
+ result = await this.git.run(
918
971
  {
919
972
  cwd: repoPath,
920
973
  cancellation: cancellation,
@@ -977,7 +1030,7 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
977
1030
  const parentRefs = shas.map(c => `${c}^`);
978
1031
  let parentShas: string[];
979
1032
  try {
980
- const result = await this.git.exec(
1033
+ const result = await this.git.run(
981
1034
  { cwd: repoPath, cancellation: signal, errors: 'throw' },
982
1035
  'rev-parse',
983
1036
  ...parentRefs,
@@ -1022,7 +1075,7 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
1022
1075
 
1023
1076
  let data;
1024
1077
  try {
1025
- const result = await this.git.exec(
1078
+ const result = await this.git.run(
1026
1079
  { cwd: repoPath, cancellation: signal, errors: 'throw' },
1027
1080
  'merge-tree',
1028
1081
  '-z',
@@ -1095,7 +1148,7 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
1095
1148
  let currentTreeOid: string;
1096
1149
  try {
1097
1150
  // Get the initial target branch tree OID
1098
- const treeResult = await this.git.exec(
1151
+ const treeResult = await this.git.run(
1099
1152
  { cwd: repoPath, cancellation: cancellation, errors: 'throw' },
1100
1153
  'rev-parse',
1101
1154
  `${targetBranch}^{tree}`,
@@ -1121,7 +1174,7 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
1121
1174
  // - base = commit.parent (where the commit started)
1122
1175
  // - ours = currentTreeOid (current state of the target)
1123
1176
  // - theirs = commit.sha (what we're cherry-picking)
1124
- const result = await this.git.exec(
1177
+ const result = await this.git.run(
1125
1178
  { cwd: repoPath, cancellation: cancellation, errors: 'throw' },
1126
1179
  'merge-tree',
1127
1180
  '--write-tree',
@@ -1210,36 +1263,43 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
1210
1263
  }
1211
1264
 
1212
1265
  @debug({ exit: true })
1213
- async getBaseBranchName(repoPath: string, ref: string, cancellation?: AbortSignal): Promise<string | undefined> {
1214
- try {
1215
- // getGkConfig has built-in fallback to regular config for backward compatibility
1216
- let mergeBase = await this.provider.config.getGkConfig(repoPath, `branch.${ref}.gk-merge-base`);
1217
- let update = false;
1218
-
1219
- // Also check vscode-merge-base in regular config (VS Code compatibility)
1220
- if (mergeBase == null) {
1221
- mergeBase = await this.provider.config.getConfig(repoPath, `branch.${ref}.vscode-merge-base`);
1222
- update = mergeBase != null;
1223
- }
1266
+ getBaseBranchName(repoPath: string, ref: string, cancellation?: AbortSignal): Promise<string | undefined> {
1267
+ return this.cache.getBaseBranchName(
1268
+ repoPath,
1269
+ ref,
1270
+ async (commonPath, signal) => {
1271
+ try {
1272
+ // getGkConfig has built-in fallback to regular config for backward compatibility
1273
+ let mergeBase = await this.provider.config.getGkConfig(commonPath, `branch.${ref}.gk-merge-base`);
1274
+ let update = false;
1275
+
1276
+ // Also check vscode-merge-base in regular config (VS Code compatibility)
1277
+ if (mergeBase == null) {
1278
+ mergeBase = await this.provider.config.getConfig(commonPath, `branch.${ref}.vscode-merge-base`);
1279
+ update = mergeBase != null;
1280
+ }
1224
1281
 
1225
- if (mergeBase != null) {
1226
- const branch = await this.provider.refs.getSymbolicReferenceName(repoPath, mergeBase);
1227
- if (branch != null) {
1228
- if (update) {
1229
- void this.storeBaseBranchName(repoPath, ref, branch);
1282
+ if (mergeBase != null) {
1283
+ const branch = await this.provider.refs.getSymbolicReferenceName(commonPath, mergeBase);
1284
+ if (branch != null) {
1285
+ if (update) {
1286
+ void this.storeBaseBranchName(commonPath, ref, branch);
1287
+ }
1288
+ return branch;
1289
+ }
1230
1290
  }
1291
+ } catch {}
1292
+
1293
+ const branch = await this.getBaseBranchFromReflog(commonPath, ref, { upstream: true }, signal);
1294
+ if (branch != null) {
1295
+ void this.storeBaseBranchName(commonPath, ref, branch);
1231
1296
  return branch;
1232
1297
  }
1233
- }
1234
- } catch {}
1235
1298
 
1236
- const branch = await this.getBaseBranchFromReflog(repoPath, ref, { upstream: true }, cancellation);
1237
- if (branch != null) {
1238
- void this.storeBaseBranchName(repoPath, ref, branch);
1239
- return branch;
1240
- }
1241
-
1242
- return undefined;
1299
+ return undefined;
1300
+ },
1301
+ cancellation,
1302
+ );
1243
1303
  }
1244
1304
 
1245
1305
  private async getBaseBranchFromReflog(
@@ -1249,7 +1309,7 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
1249
1309
  cancellation?: AbortSignal,
1250
1310
  ): Promise<string | undefined> {
1251
1311
  try {
1252
- let result = await this.git.exec(
1312
+ let result = await this.git.run(
1253
1313
  { cwd: repoPath, cancellation: cancellation },
1254
1314
  'reflog',
1255
1315
  ref,
@@ -1275,7 +1335,7 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
1275
1335
  }
1276
1336
 
1277
1337
  // Check if branch was created from HEAD
1278
- result = await this.git.exec(
1338
+ result = await this.git.run(
1279
1339
  { cwd: repoPath, cancellation: cancellation },
1280
1340
  'reflog',
1281
1341
  'HEAD',
@@ -1347,11 +1407,21 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
1347
1407
  await this.storeBranchMetadata(repoPath, 'gk-last-modified', branch.name, now);
1348
1408
  }
1349
1409
 
1410
+ /** Updates the agent last activity timestamp for the current branch */
1411
+ @debounce(2.5 * 60 * 1000)
1412
+ @debug()
1413
+ async onCurrentBranchAgentActivity(repoPath: string): Promise<void> {
1414
+ const branch = await this.getBranch(repoPath);
1415
+ if (branch == null || branch.remote || branch.detached) return;
1416
+
1417
+ await this.storeBranchMetadata(repoPath, 'gk-agent-last-activity', branch.name, new Date());
1418
+ }
1419
+
1350
1420
  @debug()
1351
1421
  async renameBranch(repoPath: string, oldName: string, newName: string): Promise<void> {
1352
1422
  const args = ['branch', '-m', oldName, newName];
1353
1423
  try {
1354
- await this.git.exec({ cwd: repoPath }, ...args);
1424
+ await this.git.run({ cwd: repoPath }, ...args);
1355
1425
  this.context.hooks?.cache?.onReset?.(repoPath, 'branches');
1356
1426
  this.context.hooks?.repository?.onChanged?.(repoPath, ['heads']);
1357
1427
  } catch (ex) {
@@ -1377,7 +1447,7 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
1377
1447
  const args =
1378
1448
  upstream == null ? ['branch', '--unset-upstream', name] : ['branch', '--set-upstream-to', upstream, name];
1379
1449
  try {
1380
- await this.git.exec({ cwd: repoPath }, ...args);
1450
+ await this.git.run({ cwd: repoPath }, ...args);
1381
1451
  this.context.hooks?.cache?.onReset?.(repoPath, 'branches');
1382
1452
  this.context.hooks?.repository?.onChanged?.(repoPath, ['heads']);
1383
1453
  } catch (ex) {
@@ -1417,56 +1487,60 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
1417
1487
  * Gets all branch metadata (dates and disposition) from git config in a single batch operation.
1418
1488
  * @returns A map of branch name to metadata.
1419
1489
  */
1420
- private async getBranchMetadataMap(repoPath: string): Promise<Map<string, BranchMetadata>> {
1421
- const scope = getScopedLogger();
1422
- const metadataMap = new Map<string, BranchMetadata>();
1490
+ private getBranchMetadataMap(repoPath: string): Promise<Map<string, BranchMetadata>> {
1491
+ return this.cache.getBranchMetadataMap(repoPath, async commonPath => {
1492
+ const scope = getScopedLogger();
1493
+ const metadataMap = new Map<string, BranchMetadata>();
1423
1494
 
1424
- try {
1425
- // Use git config --get-regexp to load all gk-* branch metadata in one call
1426
- const configMap = await this.provider.config.getGkConfigRegex(
1427
- repoPath,
1428
- '^branch\\..*\\.gk-(last-(accessed|modified)|disposition)$',
1429
- );
1430
- if (!configMap.size) return metadataMap;
1495
+ try {
1496
+ // Use git config --get-regexp to load all gk-* branch metadata in one call
1497
+ const configMap = await this.provider.config.getGkConfigRegex(
1498
+ commonPath,
1499
+ '^branch\\..*\\.gk-(last-(accessed|modified)|disposition|agent-last-activity)$',
1500
+ );
1501
+ if (!configMap.size) return metadataMap;
1431
1502
 
1432
- for (const [key, value] of configMap) {
1433
- // Extract branch name and metadata key from "branch.{name}.gk-{key}"
1434
- if (!key.startsWith('branch.')) continue;
1503
+ for (const [key, value] of configMap) {
1504
+ // Extract branch name and metadata key from "branch.{name}.gk-{key}"
1505
+ if (!key.startsWith('branch.')) continue;
1435
1506
 
1436
- const keyParts = key.split('.');
1437
- if (keyParts.length < 3) continue;
1507
+ const keyParts = key.split('.');
1508
+ if (keyParts.length < 3) continue;
1438
1509
 
1439
- // Branch name is everything between "branch." and the last ".gk-*"
1440
- const metaKey = keyParts.at(-1);
1441
- const branchName = keyParts.slice(1, -1).join('.');
1510
+ // Branch name is everything between "branch." and the last ".gk-*"
1511
+ const metaKey = keyParts.at(-1);
1512
+ const branchName = keyParts.slice(1, -1).join('.');
1442
1513
 
1443
- let metadata = metadataMap.get(branchName);
1444
- if (metadata == null) {
1445
- metadata = {};
1446
- metadataMap.set(branchName, metadata);
1447
- }
1514
+ let metadata = metadataMap.get(branchName);
1515
+ if (metadata == null) {
1516
+ metadata = {};
1517
+ metadataMap.set(branchName, metadata);
1518
+ }
1448
1519
 
1449
- if (metaKey === 'gk-last-accessed') {
1450
- metadata.lastAccessedAt = value;
1451
- } else if (metaKey === 'gk-last-modified') {
1452
- metadata.lastModifiedAt = value;
1453
- } else if (metaKey === 'gk-disposition') {
1454
- if (value === 'starred' || value === 'archived') {
1455
- metadata.disposition = value;
1520
+ if (metaKey === 'gk-last-accessed') {
1521
+ metadata.lastAccessedAt = value;
1522
+ } else if (metaKey === 'gk-last-modified') {
1523
+ metadata.lastModifiedAt = value;
1524
+ } else if (metaKey === 'gk-agent-last-activity') {
1525
+ metadata.agentLastActivityAt = value;
1526
+ } else if (metaKey === 'gk-disposition') {
1527
+ if (value === 'starred' || value === 'archived') {
1528
+ metadata.disposition = value;
1529
+ }
1456
1530
  }
1457
1531
  }
1532
+ } catch (ex) {
1533
+ debugger;
1534
+ scope?.error(ex);
1458
1535
  }
1459
- } catch (ex) {
1460
- debugger;
1461
- scope?.error(ex);
1462
- }
1463
1536
 
1464
- return metadataMap;
1537
+ return metadataMap;
1538
+ });
1465
1539
  }
1466
1540
 
1467
1541
  private async storeBranchMetadata(
1468
1542
  repoPath: string,
1469
- key: 'gk-last-accessed' | 'gk-last-modified',
1543
+ key: 'gk-last-accessed' | 'gk-last-modified' | 'gk-agent-last-activity',
1470
1544
  ref: string,
1471
1545
  date: Date,
1472
1546
  ): Promise<void> {