@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
@@ -113,7 +113,7 @@ export class BranchesGitSubProvider {
113
113
  ? this.provider.pausedOps?.getPausedOperationStatus(repoPath, cancellation)
114
114
  : undefined,
115
115
  this.git
116
- .exec({
116
+ .run({
117
117
  cwd: repoPath,
118
118
  cancellation: cancellation,
119
119
  configs: gitConfigsLog,
@@ -150,7 +150,7 @@ export class BranchesGitSubProvider {
150
150
  const supported = await this.git.supported('git:for-each-ref');
151
151
  const parser = getBranchParser(supported);
152
152
  const [gitResult, defaultWorktreePathResult, metadataMapResult] = await Promise.allSettled([
153
- this.git.exec({ cwd: commonPath, cancellation: signal }, 'for-each-ref', ...parser.arguments, 'refs/heads/', 'refs/remotes/'),
153
+ this.git.run({ cwd: commonPath, cancellation: signal }, 'for-each-ref', ...parser.arguments, 'refs/heads/', 'refs/remotes/'),
154
154
  this.provider.config.getDefaultWorktreePath?.(commonPath),
155
155
  this.getBranchMetadataMap(commonPath),
156
156
  ]);
@@ -286,35 +286,46 @@ export class BranchesGitSubProvider {
286
286
  }
287
287
  async getBranchContributionsOverview(repoPath, ref, options, cancellation) {
288
288
  const scope = getScopedLogger();
289
- try {
290
- const branch = await this.getBranch(repoPath, ref, cancellation);
291
- if (branch == null)
292
- return undefined;
289
+ const getCore = async (_commonPath, _cacheable, signal, storedTarget) => {
293
290
  let mergeTarget;
294
- // Tier 1: Check stored merge target (user-set or previously detected from PR)
295
- const storedTarget = await this.getStoredMergeTargetBranchName(repoPath, ref);
291
+ // Tier 1: Stored merge target (user-set or previously detected from PR). Caller has
292
+ // already read this once for the cache decision; reuse rather than re-fetching.
296
293
  if (storedTarget) {
297
294
  const validated = await this.provider.refs.getSymbolicReferenceName(repoPath, storedTarget);
298
295
  mergeTarget = validated || storedTarget;
299
296
  }
300
- // Tier 2: PR-based lookup (caller provides the PR promise)
297
+ // Tier 2: PR-based lookup. The branch object is only needed here (for `remoteName`),
298
+ // so lazy-load it on the cold path instead of paying for it on every call.
301
299
  if (mergeTarget == null && options?.associatedPullRequest != null) {
302
300
  const pr = await options.associatedPullRequest;
303
301
  if (pr?.refs?.base != null) {
302
+ const branch = await this.getBranch(repoPath, ref, signal);
303
+ if (branch == null)
304
+ return undefined;
304
305
  mergeTarget = `${branch.remoteName}/${pr.refs.base.branch}`;
305
- // Store for future reuse
306
+ // Store for future reuse — turns the next call into a Tier 1 cache-eligible path.
306
307
  void this.storeMergeTargetBranchName(repoPath, ref, mergeTarget);
307
308
  }
308
309
  }
309
310
  // Tier 3: Base branch, then default branch
310
- mergeTarget ??= await this.getBaseBranchName(repoPath, ref, cancellation);
311
- mergeTarget ??= await this.getDefaultBranchName(repoPath, undefined, cancellation);
311
+ mergeTarget ??= await this.getBaseBranchName(repoPath, ref, signal);
312
+ mergeTarget ??= await this.getDefaultBranchName(repoPath, undefined, signal);
312
313
  if (mergeTarget == null)
313
314
  return undefined;
314
- const mergeBase = await this.provider.refs.getMergeBase(repoPath, ref, mergeTarget, undefined, cancellation);
315
+ const mergeBase = await this.provider.refs.getMergeBase(repoPath, ref, mergeTarget, undefined, signal);
315
316
  if (mergeBase == null)
316
317
  return undefined;
317
- const result = await this.provider.contributors.getContributors(repoPath, createRevisionRange(mergeBase, ref, '..'), { stats: true }, cancellation);
318
+ // Fetch the merge-base commit's dates in parallel with contributors so consumers
319
+ // (e.g. the graph minimap scope window) don't pay a separate round-trip for them.
320
+ // `allSettled` so a transient failure in one side doesn't drop the overview entirely.
321
+ const [contributorsSettled, mergeBaseDatesSettled] = await Promise.allSettled([
322
+ this.provider.contributors.getContributors(repoPath, createRevisionRange(mergeBase, ref, '..'), { stats: true }, signal),
323
+ this.provider.commits.getCommitDates(repoPath, mergeBase, signal),
324
+ ]);
325
+ if (signal?.aborted)
326
+ throw new CancellationError();
327
+ const result = getSettledValue(contributorsSettled) ?? { contributors: [] };
328
+ const mergeBaseDates = getSettledValue(mergeBaseDatesSettled);
318
329
  sortContributors(result.contributors, { orderBy: 'score:desc' });
319
330
  let totalCommits = 0;
320
331
  let totalFiles = 0;
@@ -345,6 +356,7 @@ export class BranchesGitSubProvider {
345
356
  branch: ref,
346
357
  mergeTarget: mergeTarget,
347
358
  mergeBase: mergeBase,
359
+ mergeBaseDate: mergeBaseDates?.committerDate,
348
360
  commits: totalCommits,
349
361
  files: totalFiles,
350
362
  additions: totalAdditions,
@@ -353,6 +365,16 @@ export class BranchesGitSubProvider {
353
365
  firstCommitDate: firstCommitTimestamp != null ? new Date(firstCommitTimestamp) : undefined,
354
366
  contributors: result.contributors,
355
367
  };
368
+ };
369
+ try {
370
+ // Pre-check Tier 1: when a stored merge target exists the result is deterministic for
371
+ // `ref` (Tier 2's PR resolution is short-circuited), so it's safe to cache. Otherwise
372
+ // the result depends on PR resolution which the cache key can't capture — bypass.
373
+ const storedTarget = await this.getStoredMergeTargetBranchName(repoPath, ref);
374
+ if (storedTarget != null) {
375
+ return await this.cache.getBranchOverview(repoPath, ref, (commonPath, cacheable, signal) => getCore(commonPath, cacheable, signal ?? cancellation, storedTarget), { cancellation: cancellation });
376
+ }
377
+ return await getCore(repoPath, undefined, cancellation, undefined);
356
378
  }
357
379
  catch (ex) {
358
380
  scope?.error(ex);
@@ -387,7 +409,7 @@ export class BranchesGitSubProvider {
387
409
  if (options?.name != null) {
388
410
  params.push(options.name);
389
411
  }
390
- return this.git.exec({
412
+ return this.git.run({
391
413
  cwd: repoPath,
392
414
  cancellation: cancellation,
393
415
  configs: gitConfigsBranch,
@@ -414,7 +436,7 @@ export class BranchesGitSubProvider {
414
436
  let data;
415
437
  let result;
416
438
  try {
417
- result = await this.git.exec({ cwd: repoPath, cancellation: signal, errors: 'throw' }, 'rev-parse', '--abbrev-ref', '--symbolic-full-name', '@', '@{u}', '--');
439
+ result = await this.git.run({ cwd: repoPath, cancellation: signal, errors: 'throw' }, 'rev-parse', '--abbrev-ref', '--symbolic-full-name', '@', '@{u}', '--');
418
440
  data = [result.stdout, undefined];
419
441
  }
420
442
  catch (ex) {
@@ -427,7 +449,7 @@ export class BranchesGitSubProvider {
427
449
  }
428
450
  else {
429
451
  try {
430
- result = await this.git.exec({ cwd: repoPath, cancellation: signal }, 'symbolic-ref', '--short', 'HEAD');
452
+ result = await this.git.run({ cwd: repoPath, cancellation: signal }, 'symbolic-ref', '--short', 'HEAD');
431
453
  if (result.stdout) {
432
454
  data = [result.stdout.trim(), undefined];
433
455
  }
@@ -463,7 +485,7 @@ export class BranchesGitSubProvider {
463
485
  }
464
486
  }
465
487
  else if (GitWarnings.headNotABranch.test(msg)) {
466
- result = await this.git.exec({
488
+ result = await this.git.run({
467
489
  cwd: repoPath,
468
490
  cancellation: signal,
469
491
  configs: gitConfigsLog,
@@ -502,7 +524,7 @@ export class BranchesGitSubProvider {
502
524
  let retried = false;
503
525
  while (true) {
504
526
  try {
505
- const result = await this.git.exec({ cwd: commonPath, cancellation: cancellation }, 'symbolic-ref', '--short', `refs/remotes/${remote}/HEAD`);
527
+ const result = await this.git.run({ cwd: commonPath, cancellation: cancellation }, 'symbolic-ref', '--short', `refs/remotes/${remote}/HEAD`);
506
528
  return result.stdout.trim() || undefined;
507
529
  }
508
530
  catch (ex) {
@@ -510,10 +532,10 @@ export class BranchesGitSubProvider {
510
532
  try {
511
533
  if (!retried) {
512
534
  retried = true;
513
- await this.git.exec({ cwd: commonPath, cancellation: cancellation }, 'remote', 'set-head', '-a', remote);
535
+ await this.git.run({ cwd: commonPath, cancellation: cancellation }, 'remote', 'set-head', '-a', remote);
514
536
  continue;
515
537
  }
516
- const result = await this.git.exec({ cwd: commonPath, cancellation: cancellation }, 'ls-remote', '--symref', remote, 'HEAD');
538
+ const result = await this.git.run({ cwd: commonPath, cancellation: cancellation }, 'ls-remote', '--symref', remote, 'HEAD');
517
539
  if (result.stdout) {
518
540
  const match = /ref:\s(\S+)\s+HEAD/m.exec(result.stdout);
519
541
  if (match != null) {
@@ -538,7 +560,7 @@ export class BranchesGitSubProvider {
538
560
  args.push('--no-track');
539
561
  }
540
562
  try {
541
- await this.git.exec({ cwd: repoPath }, ...args);
563
+ await this.git.run({ cwd: repoPath }, ...args);
542
564
  this.context.hooks?.cache?.onReset?.(repoPath, 'branches');
543
565
  this.context.hooks?.repository?.onChanged?.(repoPath, ['heads']);
544
566
  }
@@ -555,7 +577,7 @@ export class BranchesGitSubProvider {
555
577
  const branches = ensureArray(names);
556
578
  const args = ['branch', options?.force ? '-D' : '-d', ...branches];
557
579
  try {
558
- await this.git.exec({ cwd: repoPath }, ...args);
580
+ await this.git.run({ cwd: repoPath }, ...args);
559
581
  this.context.hooks?.cache?.onReset?.(repoPath, 'branches');
560
582
  this.context.hooks?.repository?.onChanged?.(repoPath, ['heads']);
561
583
  }
@@ -578,7 +600,7 @@ export class BranchesGitSubProvider {
578
600
  const branches = ensureArray(names);
579
601
  const args = ['push', '-d', remote, ...branches];
580
602
  try {
581
- await this.git.exec({ cwd: repoPath }, ...args);
603
+ await this.git.run({ cwd: repoPath }, ...args);
582
604
  this.context.hooks?.cache?.onReset?.(repoPath, 'branches');
583
605
  this.context.hooks?.repository?.onChanged?.(repoPath, ['remotes']);
584
606
  }
@@ -593,42 +615,55 @@ export class BranchesGitSubProvider {
593
615
  throw new BranchError({ action: 'delete', branch: branches.join(', '), gitCommand: { repoPath: repoPath, args: args } }, ex);
594
616
  }
595
617
  }
596
- async getBranchMergedStatus(repoPath, branch, into, cancellation) {
618
+ getBranchMergedStatus(repoPath, branch, into, cancellation) {
597
619
  if (branch.name === into.name || branch.upstream?.name === into.name) {
598
- return { merged: false };
620
+ return Promise.resolve({ merged: false });
599
621
  }
600
- const result = await this.getBranchMergedStatusCore(repoPath, branch, into, cancellation);
601
- if (result.merged)
602
- return result;
603
- // If the branch we are checking is a remote branch, check if it has been merged into its local branch (if there is one)
604
- if (into.remote) {
605
- const localIntoBranch = await this.getLocalBranchByUpstream(repoPath, into.name, cancellation);
606
- // If there is a local branch and it is not the branch we are checking, check if it has been merged into it
607
- if (localIntoBranch != null && localIntoBranch.name !== branch.name) {
608
- const result = await this.getBranchMergedStatusCore(repoPath, branch, localIntoBranch, cancellation);
609
- if (result.merged) {
610
- return {
611
- ...result,
612
- localBranchOnly: createReference(localIntoBranch.ref, localIntoBranch.repoPath, {
613
- id: localIntoBranch.id,
614
- refType: 'branch',
615
- name: localIntoBranch.name,
616
- remote: localIntoBranch.remote,
617
- upstream: localIntoBranch.upstream,
618
- sha: localIntoBranch.sha,
619
- }),
620
- };
622
+ // Cache key omits repoPath so worktrees of the same common repo share the cached answer.
623
+ // Branch refs (`refs/heads/*` / `refs/remotes/*`) are unique within a common repo, so
624
+ // `name` + `remote` flag is the minimal stable identity.
625
+ const cacheKey = `${branch.remote ? 'r' : 'l'}:${branch.name}|${into.remote ? 'r' : 'l'}:${into.name}`;
626
+ return this.cache.getBranchMergedStatus(repoPath, cacheKey, async (commonPath, _cacheable, signal) => {
627
+ const result = await this.getBranchMergedStatusCore(commonPath, branch, into, signal);
628
+ if (result.merged)
629
+ return result;
630
+ // If the branch we are checking is a remote branch, check if it has been merged into its local branch (if there is one)
631
+ if (into.remote) {
632
+ const localIntoBranch = await this.getLocalBranchByUpstream(commonPath, into.name, signal);
633
+ // If there is a local branch and it is not the branch we are checking, check if it has been merged into it
634
+ if (localIntoBranch != null && localIntoBranch.name !== branch.name) {
635
+ // Skip the second full merge-check cycle when the local branch points at the same commit as the remote —
636
+ // the merge-base/cherry/diff/apply pipeline would produce the same answer we already got for `into` above
637
+ if (localIntoBranch.sha != null && localIntoBranch.sha === into.sha) {
638
+ return { merged: false };
639
+ }
640
+ const result = await this.getBranchMergedStatusCore(commonPath, branch, localIntoBranch, signal);
641
+ if (result.merged) {
642
+ // `localBranchOnly` is built against `commonPath` here; the cache mapper rewrites
643
+ // `repoPath`/`id` to the requesting worktree on retrieval.
644
+ return {
645
+ ...result,
646
+ localBranchOnly: createReference(localIntoBranch.ref, localIntoBranch.repoPath, {
647
+ id: localIntoBranch.id,
648
+ refType: 'branch',
649
+ name: localIntoBranch.name,
650
+ remote: localIntoBranch.remote,
651
+ upstream: localIntoBranch.upstream,
652
+ sha: localIntoBranch.sha,
653
+ }),
654
+ };
655
+ }
621
656
  }
622
657
  }
623
- }
624
- return { merged: false };
658
+ return { merged: false };
659
+ }, cancellation);
625
660
  }
626
661
  async getBranchMergedStatusCore(repoPath, branch, into, cancellation) {
627
662
  const scope = getScopedLogger();
628
663
  try {
629
664
  // Check if branch is direct ancestor (handles FF merges)
630
665
  try {
631
- await this.git.exec({ cwd: repoPath, cancellation: cancellation, errors: 'throw' }, 'merge-base', '--is-ancestor', branch.name, into.name);
666
+ await this.git.run({ cwd: repoPath, cancellation: cancellation, errors: 'throw' }, 'merge-base', '--is-ancestor', branch.name, into.name);
632
667
  return { merged: true, confidence: 'highest' };
633
668
  }
634
669
  catch (ex) {
@@ -636,7 +671,7 @@ export class BranchesGitSubProvider {
636
671
  throw ex;
637
672
  }
638
673
  // Cherry-pick detection (handles cherry-picks, rebases, etc)
639
- let result = await this.git.exec({ cwd: repoPath, cancellation: cancellation }, 'cherry', '--abbrev', '-v', into.name, branch.name);
674
+ let result = await this.git.run({ cwd: repoPath, cancellation: cancellation }, 'cherry', '--abbrev', '-v', into.name, branch.name);
640
675
  // Check if there are no lines or all lines startwith a `-` (i.e. likely merged)
641
676
  if (!result.stdout ||
642
677
  result.stdout
@@ -647,14 +682,14 @@ export class BranchesGitSubProvider {
647
682
  }
648
683
  // Attempt to detect squash merges by checking if the diff of the branch can be cleanly removed from the target
649
684
  const mergeBase = await this.provider.refs.getMergeBase(repoPath, into.name, branch.name, undefined, cancellation);
650
- result = await this.git.exec({ cwd: repoPath, cancellation: cancellation }, 'diff', mergeBase, branch.name);
685
+ result = await this.git.run({ cwd: repoPath, cancellation: cancellation }, 'diff', mergeBase, branch.name);
651
686
  if (result.stdout) {
652
687
  const env_2 = { stack: [], error: void 0, hasError: false };
653
688
  try {
654
689
  // Create a temporary index file
655
690
  const disposableIndex = __addDisposableResource(env_2, await this.provider.staging.createTemporaryIndex(repoPath, 'ref', into.name), true);
656
691
  const { env } = disposableIndex;
657
- result = await this.git.exec({
692
+ result = await this.git.run({
658
693
  cwd: repoPath,
659
694
  cancellation: cancellation,
660
695
  env: env,
@@ -700,7 +735,7 @@ export class BranchesGitSubProvider {
700
735
  const parentRefs = shas.map(c => `${c}^`);
701
736
  let parentShas;
702
737
  try {
703
- const result = await this.git.exec({ cwd: repoPath, cancellation: signal, errors: 'throw' }, 'rev-parse', ...parentRefs);
738
+ const result = await this.git.run({ cwd: repoPath, cancellation: signal, errors: 'throw' }, 'rev-parse', ...parentRefs);
704
739
  parentShas = result.stdout.trim().split('\n');
705
740
  }
706
741
  catch (ex) {
@@ -727,7 +762,7 @@ export class BranchesGitSubProvider {
727
762
  }
728
763
  let data;
729
764
  try {
730
- const result = await this.git.exec({ cwd: repoPath, cancellation: signal, errors: 'throw' }, 'merge-tree', '-z', '--name-only', '--no-messages', branch, targetBranch);
765
+ const result = await this.git.run({ cwd: repoPath, cancellation: signal, errors: 'throw' }, 'merge-tree', '-z', '--name-only', '--no-messages', branch, targetBranch);
731
766
  data = result.stdout;
732
767
  }
733
768
  catch (ex) {
@@ -780,7 +815,7 @@ export class BranchesGitSubProvider {
780
815
  let currentTreeOid;
781
816
  try {
782
817
  // Get the initial target branch tree OID
783
- const treeResult = await this.git.exec({ cwd: repoPath, cancellation: cancellation, errors: 'throw' }, 'rev-parse', `${targetBranch}^{tree}`);
818
+ const treeResult = await this.git.run({ cwd: repoPath, cancellation: cancellation, errors: 'throw' }, 'rev-parse', `${targetBranch}^{tree}`);
784
819
  currentTreeOid = treeResult.stdout.trim();
785
820
  }
786
821
  catch (ex) {
@@ -802,7 +837,7 @@ export class BranchesGitSubProvider {
802
837
  // - base = commit.parent (where the commit started)
803
838
  // - ours = currentTreeOid (current state of the target)
804
839
  // - theirs = commit.sha (what we're cherry-picking)
805
- const result = await this.git.exec({ cwd: repoPath, cancellation: cancellation, errors: 'throw' }, 'merge-tree', '--write-tree', '-z', '--name-only', '--no-messages', `--merge-base=${commit.parent}`, currentTreeOid, commit.sha);
840
+ const result = await this.git.run({ cwd: repoPath, cancellation: cancellation, errors: 'throw' }, 'merge-tree', '--write-tree', '-z', '--name-only', '--no-messages', `--merge-base=${commit.parent}`, currentTreeOid, commit.sha);
806
841
  data = result.stdout;
807
842
  }
808
843
  catch (ex) {
@@ -872,37 +907,39 @@ export class BranchesGitSubProvider {
872
907
  }
873
908
  return { status: 'clean' };
874
909
  }
875
- async getBaseBranchName(repoPath, ref, cancellation) {
876
- try {
877
- // getGkConfig has built-in fallback to regular config for backward compatibility
878
- let mergeBase = await this.provider.config.getGkConfig(repoPath, `branch.${ref}.gk-merge-base`);
879
- let update = false;
880
- // Also check vscode-merge-base in regular config (VS Code compatibility)
881
- if (mergeBase == null) {
882
- mergeBase = await this.provider.config.getConfig(repoPath, `branch.${ref}.vscode-merge-base`);
883
- update = mergeBase != null;
884
- }
885
- if (mergeBase != null) {
886
- const branch = await this.provider.refs.getSymbolicReferenceName(repoPath, mergeBase);
887
- if (branch != null) {
888
- if (update) {
889
- void this.storeBaseBranchName(repoPath, ref, branch);
910
+ getBaseBranchName(repoPath, ref, cancellation) {
911
+ return this.cache.getBaseBranchName(repoPath, ref, async (commonPath, signal) => {
912
+ try {
913
+ // getGkConfig has built-in fallback to regular config for backward compatibility
914
+ let mergeBase = await this.provider.config.getGkConfig(commonPath, `branch.${ref}.gk-merge-base`);
915
+ let update = false;
916
+ // Also check vscode-merge-base in regular config (VS Code compatibility)
917
+ if (mergeBase == null) {
918
+ mergeBase = await this.provider.config.getConfig(commonPath, `branch.${ref}.vscode-merge-base`);
919
+ update = mergeBase != null;
920
+ }
921
+ if (mergeBase != null) {
922
+ const branch = await this.provider.refs.getSymbolicReferenceName(commonPath, mergeBase);
923
+ if (branch != null) {
924
+ if (update) {
925
+ void this.storeBaseBranchName(commonPath, ref, branch);
926
+ }
927
+ return branch;
890
928
  }
891
- return branch;
892
929
  }
893
930
  }
894
- }
895
- catch { }
896
- const branch = await this.getBaseBranchFromReflog(repoPath, ref, { upstream: true }, cancellation);
897
- if (branch != null) {
898
- void this.storeBaseBranchName(repoPath, ref, branch);
899
- return branch;
900
- }
901
- return undefined;
931
+ catch { }
932
+ const branch = await this.getBaseBranchFromReflog(commonPath, ref, { upstream: true }, signal);
933
+ if (branch != null) {
934
+ void this.storeBaseBranchName(commonPath, ref, branch);
935
+ return branch;
936
+ }
937
+ return undefined;
938
+ }, cancellation);
902
939
  }
903
940
  async getBaseBranchFromReflog(repoPath, ref, options, cancellation) {
904
941
  try {
905
- let result = await this.git.exec({ cwd: repoPath, cancellation: cancellation }, 'reflog', ref, '--grep-reflog=branch: Created from *.');
942
+ let result = await this.git.run({ cwd: repoPath, cancellation: cancellation }, 'reflog', ref, '--grep-reflog=branch: Created from *.');
906
943
  let entries = result.stdout.split('\n').filter(entry => Boolean(entry));
907
944
  if (entries.length !== 1)
908
945
  return undefined;
@@ -922,7 +959,7 @@ export class BranchesGitSubProvider {
922
959
  }
923
960
  }
924
961
  // Check if branch was created from HEAD
925
- result = await this.git.exec({ cwd: repoPath, cancellation: cancellation }, 'reflog', 'HEAD', `--grep-reflog=checkout: moving from .* to ${ref.replace('refs/heads/', '')}`);
962
+ result = await this.git.run({ cwd: repoPath, cancellation: cancellation }, 'reflog', 'HEAD', `--grep-reflog=checkout: moving from .* to ${ref.replace('refs/heads/', '')}`);
926
963
  entries = result.stdout.split('\n').filter(entry => Boolean(entry));
927
964
  if (!entries.length)
928
965
  return undefined;
@@ -975,10 +1012,17 @@ export class BranchesGitSubProvider {
975
1012
  await this.storeBranchMetadata(repoPath, 'gk-last-accessed', branch.name, now);
976
1013
  await this.storeBranchMetadata(repoPath, 'gk-last-modified', branch.name, now);
977
1014
  }
1015
+ /** Updates the agent last activity timestamp for the current branch */
1016
+ async onCurrentBranchAgentActivity(repoPath) {
1017
+ const branch = await this.getBranch(repoPath);
1018
+ if (branch == null || branch.remote || branch.detached)
1019
+ return;
1020
+ await this.storeBranchMetadata(repoPath, 'gk-agent-last-activity', branch.name, new Date());
1021
+ }
978
1022
  async renameBranch(repoPath, oldName, newName) {
979
1023
  const args = ['branch', '-m', oldName, newName];
980
1024
  try {
981
- await this.git.exec({ cwd: repoPath }, ...args);
1025
+ await this.git.run({ cwd: repoPath }, ...args);
982
1026
  this.context.hooks?.cache?.onReset?.(repoPath, 'branches');
983
1027
  this.context.hooks?.repository?.onChanged?.(repoPath, ['heads']);
984
1028
  }
@@ -994,7 +1038,7 @@ export class BranchesGitSubProvider {
994
1038
  async setUpstreamBranch(repoPath, name, upstream) {
995
1039
  const args = upstream == null ? ['branch', '--unset-upstream', name] : ['branch', '--set-upstream-to', upstream, name];
996
1040
  try {
997
- await this.git.exec({ cwd: repoPath }, ...args);
1041
+ await this.git.run({ cwd: repoPath }, ...args);
998
1042
  this.context.hooks?.cache?.onReset?.(repoPath, 'branches');
999
1043
  this.context.hooks?.repository?.onChanged?.(repoPath, ['heads']);
1000
1044
  }
@@ -1020,47 +1064,52 @@ export class BranchesGitSubProvider {
1020
1064
  * Gets all branch metadata (dates and disposition) from git config in a single batch operation.
1021
1065
  * @returns A map of branch name to metadata.
1022
1066
  */
1023
- async getBranchMetadataMap(repoPath) {
1024
- const scope = getScopedLogger();
1025
- const metadataMap = new Map();
1026
- try {
1027
- // Use git config --get-regexp to load all gk-* branch metadata in one call
1028
- const configMap = await this.provider.config.getGkConfigRegex(repoPath, '^branch\\..*\\.gk-(last-(accessed|modified)|disposition)$');
1029
- if (!configMap.size)
1030
- return metadataMap;
1031
- for (const [key, value] of configMap) {
1032
- // Extract branch name and metadata key from "branch.{name}.gk-{key}"
1033
- if (!key.startsWith('branch.'))
1034
- continue;
1035
- const keyParts = key.split('.');
1036
- if (keyParts.length < 3)
1037
- continue;
1038
- // Branch name is everything between "branch." and the last ".gk-*"
1039
- const metaKey = keyParts.at(-1);
1040
- const branchName = keyParts.slice(1, -1).join('.');
1041
- let metadata = metadataMap.get(branchName);
1042
- if (metadata == null) {
1043
- metadata = {};
1044
- metadataMap.set(branchName, metadata);
1045
- }
1046
- if (metaKey === 'gk-last-accessed') {
1047
- metadata.lastAccessedAt = value;
1048
- }
1049
- else if (metaKey === 'gk-last-modified') {
1050
- metadata.lastModifiedAt = value;
1051
- }
1052
- else if (metaKey === 'gk-disposition') {
1053
- if (value === 'starred' || value === 'archived') {
1054
- metadata.disposition = value;
1067
+ getBranchMetadataMap(repoPath) {
1068
+ return this.cache.getBranchMetadataMap(repoPath, async (commonPath) => {
1069
+ const scope = getScopedLogger();
1070
+ const metadataMap = new Map();
1071
+ try {
1072
+ // Use git config --get-regexp to load all gk-* branch metadata in one call
1073
+ const configMap = await this.provider.config.getGkConfigRegex(commonPath, '^branch\\..*\\.gk-(last-(accessed|modified)|disposition|agent-last-activity)$');
1074
+ if (!configMap.size)
1075
+ return metadataMap;
1076
+ for (const [key, value] of configMap) {
1077
+ // Extract branch name and metadata key from "branch.{name}.gk-{key}"
1078
+ if (!key.startsWith('branch.'))
1079
+ continue;
1080
+ const keyParts = key.split('.');
1081
+ if (keyParts.length < 3)
1082
+ continue;
1083
+ // Branch name is everything between "branch." and the last ".gk-*"
1084
+ const metaKey = keyParts.at(-1);
1085
+ const branchName = keyParts.slice(1, -1).join('.');
1086
+ let metadata = metadataMap.get(branchName);
1087
+ if (metadata == null) {
1088
+ metadata = {};
1089
+ metadataMap.set(branchName, metadata);
1090
+ }
1091
+ if (metaKey === 'gk-last-accessed') {
1092
+ metadata.lastAccessedAt = value;
1093
+ }
1094
+ else if (metaKey === 'gk-last-modified') {
1095
+ metadata.lastModifiedAt = value;
1096
+ }
1097
+ else if (metaKey === 'gk-agent-last-activity') {
1098
+ metadata.agentLastActivityAt = value;
1099
+ }
1100
+ else if (metaKey === 'gk-disposition') {
1101
+ if (value === 'starred' || value === 'archived') {
1102
+ metadata.disposition = value;
1103
+ }
1055
1104
  }
1056
1105
  }
1057
1106
  }
1058
- }
1059
- catch (ex) {
1060
- debugger;
1061
- scope?.error(ex);
1062
- }
1063
- return metadataMap;
1107
+ catch (ex) {
1108
+ debugger;
1109
+ scope?.error(ex);
1110
+ }
1111
+ return metadataMap;
1112
+ });
1064
1113
  }
1065
1114
  async storeBranchMetadata(repoPath, key, ref, date) {
1066
1115
  const value = await this.provider.config.getGkConfig(repoPath, `branch.${ref}.${key}`);
@@ -1135,6 +1184,10 @@ __decorate([
1135
1184
  debounce(2.5 * 60 * 1000),
1136
1185
  debug()
1137
1186
  ], BranchesGitSubProvider.prototype, "onCurrentBranchModified", null);
1187
+ __decorate([
1188
+ debounce(2.5 * 60 * 1000),
1189
+ debug()
1190
+ ], BranchesGitSubProvider.prototype, "onCurrentBranchAgentActivity", null);
1138
1191
  __decorate([
1139
1192
  debug()
1140
1193
  ], BranchesGitSubProvider.prototype, "renameBranch", null);