@gitkraken/core-gitlens 0.11.1 → 0.13.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 (201) hide show
  1. package/CHANGELOG.md +84 -1
  2. package/dist/git/cache.d.ts +84 -29
  3. package/dist/git/cache.js +188 -54
  4. package/dist/git/cache.js.map +1 -1
  5. package/dist/git/features.d.ts +1 -1
  6. package/dist/git/features.js +4 -0
  7. package/dist/git/features.js.map +1 -1
  8. package/dist/git/models/branch.d.ts +15 -2
  9. package/dist/git/models/branch.js +27 -5
  10. package/dist/git/models/branch.js.map +1 -1
  11. package/dist/git/models/issue.d.ts +3 -1
  12. package/dist/git/models/issue.js.map +1 -1
  13. package/dist/git/models/status.js +1 -1
  14. package/dist/git/models/worktree.d.ts +4 -1
  15. package/dist/git/models/worktree.js +4 -2
  16. package/dist/git/models/worktree.js.map +1 -1
  17. package/dist/git/providers/branches.d.ts +73 -8
  18. package/dist/git/providers/pausedOperations.d.ts +6 -0
  19. package/dist/git/providers/provider.d.ts +34 -2
  20. package/dist/git/providers/status.d.ts +8 -2
  21. package/dist/git/remotes/azure-devops.js +61 -53
  22. package/dist/git/remotes/azure-devops.js.map +1 -1
  23. package/dist/git/service.d.ts +8 -5
  24. package/dist/git/service.js +16 -8
  25. package/dist/git/service.js.map +1 -1
  26. package/dist/git/utils/branch.utils.d.ts +8 -1
  27. package/dist/git/utils/branch.utils.js +30 -1
  28. package/dist/git/utils/branch.utils.js.map +1 -1
  29. package/dist/git/utils/issue.utils.d.ts +3 -1
  30. package/dist/git/utils/issue.utils.js +5 -1
  31. package/dist/git/utils/issue.utils.js.map +1 -1
  32. package/dist/git/watching/changeEvent.d.ts +13 -2
  33. package/dist/git/watching/changeEvent.js +17 -3
  34. package/dist/git/watching/changeEvent.js.map +1 -1
  35. package/dist/git/watching/gitDirPoller.d.ts +111 -0
  36. package/dist/git/watching/gitDirPoller.js +266 -0
  37. package/dist/git/watching/gitDirPoller.js.map +1 -0
  38. package/dist/git/watching/provider.d.ts +16 -0
  39. package/dist/git/watching/watchGroup.d.ts +40 -4
  40. package/dist/git/watching/watchGroup.js +143 -17
  41. package/dist/git/watching/watchGroup.js.map +1 -1
  42. package/dist/git/watching/watchService.d.ts +70 -1
  43. package/dist/git/watching/watchService.js +158 -13
  44. package/dist/git/watching/watchService.js.map +1 -1
  45. package/dist/git/watching/watchSession.d.ts +44 -0
  46. package/dist/git/watching/watchSession.js +91 -6
  47. package/dist/git/watching/watchSession.js.map +1 -1
  48. package/dist/git/watching/watcherPatterns.d.ts +21 -0
  49. package/dist/git/watching/watcherPatterns.js +67 -1
  50. package/dist/git/watching/watcherPatterns.js.map +1 -1
  51. package/dist/git/watching/worktreeSummary.d.ts +34 -7
  52. package/dist/git/watching/worktreeSummary.js +39 -12
  53. package/dist/git/watching/worktreeSummary.js.map +1 -1
  54. package/dist/git-cli/cliGitProvider.d.ts +34 -2
  55. package/dist/git-cli/cliGitProvider.js +119 -17
  56. package/dist/git-cli/cliGitProvider.js.map +1 -1
  57. package/dist/git-cli/exec/commandClassifier.d.ts +15 -0
  58. package/dist/git-cli/exec/commandClassifier.js +48 -2
  59. package/dist/git-cli/exec/commandClassifier.js.map +1 -1
  60. package/dist/git-cli/exec/git.d.ts +15 -3
  61. package/dist/git-cli/exec/git.js +38 -17
  62. package/dist/git-cli/exec/git.js.map +1 -1
  63. package/dist/git-cli/exec/gitQueue.d.ts +2 -0
  64. package/dist/git-cli/exec/gitQueue.js +2 -1
  65. package/dist/git-cli/exec/gitQueue.js.map +1 -1
  66. package/dist/git-cli/exec/indexLock.d.ts +38 -0
  67. package/dist/git-cli/exec/indexLock.js +150 -0
  68. package/dist/git-cli/exec/indexLock.js.map +1 -0
  69. package/dist/git-cli/parsers/worktreeParser.js +42 -2
  70. package/dist/git-cli/parsers/worktreeParser.js.map +1 -1
  71. package/dist/git-cli/providers/branches.d.ts +109 -10
  72. package/dist/git-cli/providers/branches.js +624 -131
  73. package/dist/git-cli/providers/branches.js.map +1 -1
  74. package/dist/git-cli/providers/config.d.ts +9 -5
  75. package/dist/git-cli/providers/config.js +60 -27
  76. package/dist/git-cli/providers/config.js.map +1 -1
  77. package/dist/git-cli/providers/operations.d.ts +17 -0
  78. package/dist/git-cli/providers/operations.js +153 -96
  79. package/dist/git-cli/providers/operations.js.map +1 -1
  80. package/dist/git-cli/providers/pausedOperations.d.ts +8 -0
  81. package/dist/git-cli/providers/pausedOperations.js +60 -40
  82. package/dist/git-cli/providers/pausedOperations.js.map +1 -1
  83. package/dist/git-cli/providers/refs.js +11 -8
  84. package/dist/git-cli/providers/refs.js.map +1 -1
  85. package/dist/git-cli/providers/remotes.js +19 -10
  86. package/dist/git-cli/providers/remotes.js.map +1 -1
  87. package/dist/git-cli/providers/staging.js +40 -15
  88. package/dist/git-cli/providers/staging.js.map +1 -1
  89. package/dist/git-cli/providers/stash.d.ts +2 -0
  90. package/dist/git-cli/providers/stash.js +33 -18
  91. package/dist/git-cli/providers/stash.js.map +1 -1
  92. package/dist/git-cli/providers/status.d.ts +3 -1
  93. package/dist/git-cli/providers/status.js +38 -20
  94. package/dist/git-cli/providers/status.js.map +1 -1
  95. package/dist/git-cli/providers/tags.js +6 -4
  96. package/dist/git-cli/providers/tags.js.map +1 -1
  97. package/dist/git-cli/providers/worktrees.d.ts +1 -0
  98. package/dist/git-cli/providers/worktrees.js +60 -31
  99. package/dist/git-cli/providers/worktrees.js.map +1 -1
  100. package/dist/git-cli/service.d.ts +6 -0
  101. package/dist/git-cli/service.js +5 -2
  102. package/dist/git-cli/service.js.map +1 -1
  103. package/dist/ipc/discovery.js +1 -1
  104. package/dist/plus/agents/providers/claudeCodeTranscript.js +6 -6
  105. package/dist/plus/agents/providers/claudeCodeTranscript.js.map +1 -1
  106. package/dist/plus/ai/providers/openAICompatibleProviderBase.js +1 -1
  107. package/dist/plus/git-github/providers/github/status.d.ts +3 -1
  108. package/dist/plus/git-github/providers/github/status.js +1 -1
  109. package/dist/plus/git-github/providers/github/status.js.map +1 -1
  110. package/dist/plus/integrations/collectionMetadata.d.ts +13 -2
  111. package/dist/plus/integrations/collectionMetadata.js +14 -2
  112. package/dist/plus/integrations/collectionMetadata.js.map +1 -1
  113. package/dist/plus/integrations/index.d.ts +2 -2
  114. package/dist/plus/integrations/index.js.map +1 -1
  115. package/dist/plus/integrations/integrationService.d.ts +1 -0
  116. package/dist/plus/integrations/integrationService.js +1 -1
  117. package/dist/plus/integrations/integrationService.js.map +1 -1
  118. package/dist/plus/integrations/manager.d.ts +1 -0
  119. package/dist/plus/integrations/models/gitHostIntegration.d.ts +41 -5
  120. package/dist/plus/integrations/models/gitHostIntegration.js +109 -30
  121. package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
  122. package/dist/plus/integrations/models/integration.js +1 -1
  123. package/dist/plus/integrations/models/issueReads.d.ts +17 -4
  124. package/dist/plus/integrations/providers/apiClients.d.ts +2 -0
  125. package/dist/plus/integrations/providers/apiClients.js +7 -1
  126. package/dist/plus/integrations/providers/apiClients.js.map +1 -1
  127. package/dist/plus/integrations/providers/azure/azure.d.ts +5 -4
  128. package/dist/plus/integrations/providers/azure/azure.js +38 -77
  129. package/dist/plus/integrations/providers/azure/azure.js.map +1 -1
  130. package/dist/plus/integrations/providers/azure/azure.utils.js +20 -17
  131. package/dist/plus/integrations/providers/azure/azure.utils.js.map +1 -1
  132. package/dist/plus/integrations/providers/azure/models.d.ts +27 -10
  133. package/dist/plus/integrations/providers/azure/models.js +40 -20
  134. package/dist/plus/integrations/providers/azure/models.js.map +1 -1
  135. package/dist/plus/integrations/providers/azure/workItemStateCategories.d.ts +23 -0
  136. package/dist/plus/integrations/providers/azure/workItemStateCategories.js +84 -0
  137. package/dist/plus/integrations/providers/azure/workItemStateCategories.js.map +1 -0
  138. package/dist/plus/integrations/providers/azureDevOps.d.ts +44 -0
  139. package/dist/plus/integrations/providers/azureDevOps.js +113 -22
  140. package/dist/plus/integrations/providers/azureDevOps.js.map +1 -1
  141. package/dist/plus/integrations/providers/azureDevOpsServer.d.ts +45 -1
  142. package/dist/plus/integrations/providers/azureDevOpsServer.js +121 -6
  143. package/dist/plus/integrations/providers/azureDevOpsServer.js.map +1 -1
  144. package/dist/plus/integrations/providers/issueSorts.d.ts +0 -1
  145. package/dist/plus/integrations/providers/jira-server.js +29 -19
  146. package/dist/plus/integrations/providers/jira-server.js.map +1 -1
  147. package/dist/plus/integrations/providers/jira.d.ts +5 -2
  148. package/dist/plus/integrations/providers/jira.js +63 -44
  149. package/dist/plus/integrations/providers/jira.js.map +1 -1
  150. package/dist/plus/integrations/providers/jiraIssueByKey.d.ts +3 -4
  151. package/dist/plus/integrations/providers/jiraIssueByKey.js +3 -4
  152. package/dist/plus/integrations/providers/jiraIssueByKey.js.map +1 -1
  153. package/dist/plus/integrations/providers/linearIssuesEtag.d.ts +5 -5
  154. package/dist/plus/integrations/providers/linearIssuesEtag.js +6 -6
  155. package/dist/plus/integrations/providers/linearIssuesEtag.js.map +1 -1
  156. package/dist/plus/integrations/providers/models.d.ts +12 -3
  157. package/dist/plus/integrations/providers/models.js +16 -9
  158. package/dist/plus/integrations/providers/models.js.map +1 -1
  159. package/dist/plus/integrations/providers/providersApi.d.ts +21 -33
  160. package/dist/plus/integrations/providers/providersApi.js +159 -49
  161. package/dist/plus/integrations/providers/providersApi.js.map +1 -1
  162. package/dist/plus/integrations/providers/utils/jiraIssueDrains.d.ts +49 -15
  163. package/dist/plus/integrations/providers/utils/jiraIssueDrains.js +132 -10
  164. package/dist/plus/integrations/providers/utils/jiraIssueDrains.js.map +1 -1
  165. package/dist/plus/integrations/providers/utils/providerPaging.js +8 -2
  166. package/dist/plus/integrations/providers/utils/providerPaging.js.map +1 -1
  167. package/dist/plus/integrations/reads/cursors.js +1 -1
  168. package/dist/plus/integrations/reads/drains.d.ts +0 -1
  169. package/dist/plus/integrations/reads/filters.d.ts +3 -3
  170. package/dist/plus/integrations/reads/filters.js.map +1 -1
  171. package/dist/plus/integrations/reads/hierarchy.js +2 -2
  172. package/dist/plus/integrations/reads/hierarchy.js.map +1 -1
  173. package/dist/plus/integrations/reads/hierarchy.utils.d.ts +13 -0
  174. package/dist/plus/integrations/reads/hierarchy.utils.js +26 -1
  175. package/dist/plus/integrations/reads/hierarchy.utils.js.map +1 -1
  176. package/dist/plus/integrations/reads/issueBatch.d.ts +2 -1
  177. package/dist/plus/integrations/reads/issueBatch.js +1 -1
  178. package/dist/plus/integrations/reads/issueBatch.js.map +1 -1
  179. package/dist/plus/integrations/reads/issueTracker.js +27 -18
  180. package/dist/plus/integrations/reads/issueTracker.js.map +1 -1
  181. package/dist/plus/integrations/reads/issues.d.ts +2 -1
  182. package/dist/plus/integrations/reads/issues.js +19 -4
  183. package/dist/plus/integrations/reads/issues.js.map +1 -1
  184. package/dist/plus/integrations/reads/pullRequestBatch.js +1 -1
  185. package/dist/plus/integrations/reads/resolveRepository.js +12 -3
  186. package/dist/plus/integrations/reads/resolveRepository.js.map +1 -1
  187. package/dist/plus/integrations/reads/warnings.d.ts +11 -4
  188. package/dist/plus/integrations/reads/warnings.js +15 -5
  189. package/dist/plus/integrations/reads/warnings.js.map +1 -1
  190. package/dist/plus/integrations/results.d.ts +48 -4
  191. package/dist/plus/integrations/results.js +33 -13
  192. package/dist/plus/integrations/results.js.map +1 -1
  193. package/dist/plus/integrations/utils/integration.utils.d.ts +6 -1
  194. package/dist/plus/integrations/utils/integration.utils.js +8 -1
  195. package/dist/plus/integrations/utils/integration.utils.js.map +1 -1
  196. package/dist/utils/env/node/exec.js +2 -2
  197. package/dist/utils/promiseCache.d.ts +10 -0
  198. package/dist/utils/promiseCache.js +17 -2
  199. package/dist/utils/promiseCache.js.map +1 -1
  200. package/docs/integrations.md +93 -31
  201. package/package.json +2 -2
@@ -1,33 +1,73 @@
1
+ import { sum } from "../../utils/iterable.js";
1
2
  import { ensureArray, filterMap } from "../../utils/array.js";
2
3
  import { CancellationError, isCancellationError } from "../../utils/cancellation.js";
3
4
  import { Logger as _logger } from "../../utils/logger.js";
4
5
  import { getScopedLogger } from "../../utils/logger.scoped.js";
5
- import { getSettledValue } from "../../utils/promise.js";
6
- import { debug } from "../../utils/decorators/log.js";
6
+ import { getSettledValue, mapBounded } from "../../utils/promise.js";
7
+ import { debug, trace } from "../../utils/decorators/log.js";
7
8
  import { getBranchId } from "../../utils/gitRefs.js";
8
9
  import { normalizePath } from "../../utils/path.js";
9
10
  import { createRevisionRange, shortenRevision } from "../../git/utils/revision.utils.js";
11
+ import { formatDetachedHeadName, getLocalBranchByUpstream, isDetachedHead, isRemoteHEAD, parseBranchMetadataGkConfigKey, parseRefName, parseUpstream, setBranchMetadataValue } from "../../git/utils/branch.utils.js";
10
12
  import { createReference } from "../../git/utils/reference.utils.js";
11
13
  import __decorate from "../../_virtual/@oxc-project+runtime/helpers/esm/decorate.js";
12
14
  import { BranchError, GitWarnings } from "../../git/errors.js";
15
+ import { mayWriteTracking } from "../exec/commandClassifier.js";
16
+ import { KeyedSequencer } from "../exec/indexLock.js";
13
17
  import { GitError, GitErrors, getGitCommandError, gitConfigsBranch, gitConfigsLog } from "../exec/git.js";
14
18
  import { maybeStopWatch } from "../../utils/stopwatch.js";
15
19
  import _usingCtx from "../../_virtual/@oxc-project+runtime/helpers/esm/usingCtx.js";
16
- import { formatDetachedHeadName, getLocalBranchByUpstream, isDetachedHead, isRemoteHEAD, parseRefName, parseUpstream } from "../../git/utils/branch.utils.js";
17
20
  import { GitBranch } from "../../git/models/branch.js";
18
21
  import { createConflictDetectionError } from "../../git/utils/mergeConflicts.utils.js";
19
22
  import { sortBranches, sortContributors } from "../../git/utils/sorting.js";
20
- import { debounce } from "../../utils/decorators/debounce.js";
23
+ import { LruMap } from "../../utils/lruMap.js";
21
24
  import { PageableResult, emptyPagedResult } from "../../utils/paging.js";
22
25
  import { parseMergeTreeConflict } from "../parsers/mergeTreeParser.js";
23
26
  //#region ../git-cli/src/providers/branches.ts
24
27
  /** Minimum time between writes to the config for last accessed/modified dates */
25
28
  const dateMetadataStaleThresholdMs = 3e5;
29
+ /**
30
+ * Run options that keep an unrequested remote read from prompting for credentials or hanging on a dead host.
31
+ * An empty `GIT_ASKPASS` also stops git consulting `core.askPass` and `SSH_ASKPASS` itself.
32
+ */
33
+ const nonInteractiveNetworkOptions = {
34
+ configs: ["-c", "credential.interactive=false"],
35
+ env: {
36
+ GIT_TERMINAL_PROMPT: "0",
37
+ GIT_ASKPASS: "",
38
+ SSH_ASKPASS: "",
39
+ SSH_ASKPASS_REQUIRE: "never",
40
+ GCM_INTERACTIVE: "never"
41
+ },
42
+ timeout: 1e4
43
+ };
44
+ const branchActivityKeys = [
45
+ "gk-last-accessed",
46
+ "gk-last-modified",
47
+ "gk-agent-last-activity"
48
+ ];
26
49
  var BranchesGitSubProvider = class {
27
50
  context;
28
51
  git;
29
52
  cache;
30
53
  provider;
54
+ /** Ahead/behind counts by `<baseSha>...<tipSha>` range, from the per-tip fallback; a pair of commits always counts the same */
55
+ aheadBehindBySha = new LruMap(4096);
56
+ /**
57
+ * When each `commonPath\0branch\0key` was last stamped by this process. Bounded: a dropped entry only costs a
58
+ * read of the stored stamp, which still holds off a write under 5 minutes old
59
+ */
60
+ _branchActivityStamps = new LruMap(1024);
61
+ /** One call per `commonPath\0branch` at a time, so a call held off by another's write in flight settles after it */
62
+ _branchActivityCalls = new KeyedSequencer();
63
+ /**
64
+ * What each `commonPath\0branch`'s reflog says it was created from, as git wrote it; `undefined` when it says
65
+ * nothing. A branch's creation never changes, so the base read, which reruns on every commit and ref move, reads
66
+ * it once
67
+ */
68
+ _branchCreatedFrom = new LruMap(1024);
69
+ /** Bumped each time a key's created-from is forgotten, so a reflog read in flight across that doesn't store it */
70
+ _branchCreatedFromGenerations = new LruMap(1024);
31
71
  constructor(context, git, cache, provider) {
32
72
  this.context = context;
33
73
  this.git = git;
@@ -431,6 +471,23 @@ var BranchesGitSubProvider = class {
431
471
  }
432
472
  }, cancellation);
433
473
  }
474
+ /**
475
+ * Writes the missing `refs/remotes/<remote>/HEAD` symref from the remote (`remote set-head -a`) in the background,
476
+ * never prompting for credentials; nothing runs past the cached local check when the symref exists. Rejects when
477
+ * the remote can't be read.
478
+ */
479
+ async setRemoteHeadIfMissing(repoPath, remote) {
480
+ if (await this.getDefaultBranchName(repoPath, remote, {
481
+ priority: "background",
482
+ local: true
483
+ }) != null) return;
484
+ await this.git.run({
485
+ cwd: repoPath,
486
+ priority: "background",
487
+ ...nonInteractiveNetworkOptions
488
+ }, "remote", "set-head", "-a", remote);
489
+ this.cache.deleteDefaultBranchName(repoPath, `${remote}:local`);
490
+ }
434
491
  async createBranch(repoPath, name, ref, options) {
435
492
  const args = [
436
493
  "branch",
@@ -441,8 +498,7 @@ var BranchesGitSubProvider = class {
441
498
  try {
442
499
  await this.git.run({ cwd: repoPath }, ...args);
443
500
  this.cache.deleteBaseBranchName(repoPath, name);
444
- this.context.hooks?.cache?.onReset?.(repoPath, "branches");
445
- this.context.hooks?.repository?.onChanged?.(repoPath, ["heads"]);
501
+ this.forgetBranchCreatedFrom(repoPath, name);
446
502
  } catch (ex) {
447
503
  throw getGitCommandError("branch", ex, (reason) => new BranchError({
448
504
  reason: reason ?? "other",
@@ -453,6 +509,9 @@ var BranchesGitSubProvider = class {
453
509
  args
454
510
  }
455
511
  }, ex));
512
+ } finally {
513
+ this.context.hooks?.cache?.onReset?.(repoPath, "branches");
514
+ this.context.hooks?.repository?.onChanged?.(repoPath, mayWriteTracking(ref, options?.noTracking) ? ["config", "heads"] : ["heads"]);
456
515
  }
457
516
  }
458
517
  async deleteLocalBranch(repoPath, names, options) {
@@ -466,11 +525,11 @@ var BranchesGitSubProvider = class {
466
525
  await this.git.run({ cwd: repoPath }, ...args);
467
526
  for (const branch of branches) await this.forgetDeletedBranch(repoPath, branch);
468
527
  this.context.hooks?.cache?.onReset?.(repoPath, "branches");
469
- this.context.hooks?.repository?.onChanged?.(repoPath, ["heads"]);
528
+ this.context.hooks?.repository?.onChanged?.(repoPath, ["config", "heads"]);
470
529
  } catch (ex) {
471
530
  if (await this.cleanupDeletedBranchMetadata(repoPath, branches)) {
472
531
  this.context.hooks?.cache?.onReset?.(repoPath, "branches");
473
- this.context.hooks?.repository?.onChanged?.(repoPath, ["heads"]);
532
+ this.context.hooks?.repository?.onChanged?.(repoPath, ["config", "heads"]);
474
533
  }
475
534
  if (ex instanceof BranchError) throw ex.update({
476
535
  action: options?.force ? "force delete" : "delete",
@@ -500,6 +559,8 @@ var BranchesGitSubProvider = class {
500
559
  async forgetDeletedBranch(repoPath, name) {
501
560
  this.cache.deleteBaseBranchName(repoPath, name);
502
561
  await this.provider.config.removeGkConfigBranchSection(repoPath, name);
562
+ this.clearBranchActivityStamps(repoPath, name);
563
+ this.forgetBranchCreatedFrom(repoPath, name);
503
564
  }
504
565
  /**
505
566
  * Drops the cached and persisted per-branch metadata for whichever of `names` no longer exists. Only
@@ -539,8 +600,6 @@ var BranchesGitSubProvider = class {
539
600
  ];
540
601
  try {
541
602
  await this.git.run({ cwd: repoPath }, ...args);
542
- this.context.hooks?.cache?.onReset?.(repoPath, "branches");
543
- this.context.hooks?.repository?.onChanged?.(repoPath, ["remotes"]);
544
603
  } catch (ex) {
545
604
  if (ex instanceof BranchError) throw ex.update({
546
605
  action: "delete",
@@ -559,6 +618,9 @@ var BranchesGitSubProvider = class {
559
618
  args
560
619
  }
561
620
  }, ex));
621
+ } finally {
622
+ this.context.hooks?.cache?.onReset?.(repoPath, "branches");
623
+ this.context.hooks?.repository?.onChanged?.(repoPath, ["remotes"]);
562
624
  }
563
625
  }
564
626
  async getBranchMergedStatus(repoPath, branch, into, cancellation) {
@@ -908,33 +970,26 @@ var BranchesGitSubProvider = class {
908
970
  const priority = options?.priority;
909
971
  const priorityOpts = priority != null ? { priority } : void 0;
910
972
  try {
911
- let entries = await this.provider.refs.getReflogEntries(repoPath, ref, {
912
- grep: "branch: Created from *.",
913
- priority
914
- }, cancellation);
915
- if (entries.length !== 1) return {
973
+ const createdFrom = await this.getReflogCreatedFrom(repoPath, ref, priority, cancellation);
974
+ if (createdFrom == null) return {
916
975
  branch: void 0,
917
976
  failed: false
918
977
  };
919
- let match = entries[0].message.match(/branch: Created from (.*)$/);
920
- if (match?.length === 2) {
921
- let name = match[1];
922
- if (name !== "HEAD") {
923
- if (options?.upstream) {
924
- const upstream = await this.provider.refs.getSymbolicReferenceName(repoPath, `${name}@{u}`, priorityOpts);
925
- if (upstream) return {
926
- branch: upstream,
927
- failed: false
928
- };
929
- }
930
- name = await this.provider.refs.getSymbolicReferenceName(repoPath, name, priorityOpts);
931
- if (name) return {
932
- branch: name,
978
+ if (createdFrom !== "HEAD") {
979
+ if (options?.upstream) {
980
+ const upstream = await this.provider.refs.getSymbolicReferenceName(repoPath, `${createdFrom}@{u}`, priorityOpts);
981
+ if (upstream) return {
982
+ branch: upstream,
933
983
  failed: false
934
984
  };
935
985
  }
986
+ const name = await this.provider.refs.getSymbolicReferenceName(repoPath, createdFrom, priorityOpts);
987
+ if (name) return {
988
+ branch: name,
989
+ failed: false
990
+ };
936
991
  }
937
- entries = await this.provider.refs.getReflogEntries(repoPath, "HEAD", {
992
+ const entries = await this.provider.refs.getReflogEntries(repoPath, "HEAD", {
938
993
  grep: `checkout: moving from .* to ${ref.replace("refs/heads/", "")}`,
939
994
  priority
940
995
  }, cancellation);
@@ -942,7 +997,7 @@ var BranchesGitSubProvider = class {
942
997
  branch: void 0,
943
998
  failed: false
944
999
  };
945
- match = entries.at(-1).message.match(/checkout: moving from ([^\s]+)\s/);
1000
+ const match = entries.at(-1).message.match(/checkout: moving from ([^\s]+)\s/);
946
1001
  if (match?.length === 2) {
947
1002
  let name = match[1];
948
1003
  if (options?.upstream) {
@@ -976,59 +1031,320 @@ var BranchesGitSubProvider = class {
976
1031
  failed: false
977
1032
  };
978
1033
  }
979
- async getAheadOfBase(repoPath, branch, cancellation) {
980
- if (isDetachedHead(branch)) return {
981
- base: void 0,
982
- dependsOn: []
983
- };
984
- const dependsOn = /* @__PURE__ */ new Set([`refs/heads/${branch}`]);
1034
+ /**
1035
+ * The start point `ref`'s own reflog says it was created from (`branch: Created from <name>`), as git wrote it:
1036
+ * a branch, `HEAD`, a sha, a tag or any other revision. `undefined` unless exactly one entry says so. Rejects
1037
+ * when the reflog can't be read, including for a ref that doesn't exist.
1038
+ */
1039
+ async getReflogCreatedFrom(repoPath, ref, priority, cancellation) {
1040
+ const entries = await this.provider.refs.getReflogEntries(repoPath, ref, {
1041
+ grep: "branch: Created from *.",
1042
+ priority
1043
+ }, cancellation);
1044
+ if (entries.length !== 1) return void 0;
1045
+ return entries[0].message.match(/branch: Created from (.*)$/)?.[1];
1046
+ }
1047
+ /**
1048
+ * The branch `branch` was created from, as a base for {@link getAheadOfBaseInOrder}: the name its own reflog
1049
+ * gives, when that is a local or remote-tracking branch in the cached ref list and not `branch` itself or its
1050
+ * copy on a remote (`git switch <branch>` creates it from `<remote>/<branch>`). Never the checkout history in
1051
+ * HEAD's reflog, which on a long-lived checkout names whatever was checked out last.
1052
+ */
1053
+ async getCreatedFromTarget(repoPath, branch, cancellation) {
1054
+ const key = this.getBranchActivityKey(repoPath, branch);
1055
+ let name;
1056
+ if (this._branchCreatedFrom.has(key)) name = this._branchCreatedFrom.get(key);
1057
+ else {
1058
+ const generation = this._branchCreatedFromGenerations.get(key);
1059
+ try {
1060
+ name = await this.getReflogCreatedFrom(repoPath, `refs/heads/${branch}`, void 0, cancellation);
1061
+ } catch (ex) {
1062
+ if (isCancellationError(ex)) throw ex;
1063
+ return;
1064
+ }
1065
+ if (cancellation?.aborted) throw new CancellationError();
1066
+ if (this._branchCreatedFromGenerations.get(key) === generation) this._branchCreatedFrom.set(key, name);
1067
+ }
1068
+ if (!name || name === "HEAD" || name.startsWith("-")) return void 0;
1069
+ const records = await this.provider.refs.getRefs(repoPath, cancellation);
1070
+ const fullNames = name.startsWith("refs/heads/") || name.startsWith("refs/remotes/") ? [name] : [`refs/heads/${name}`, `refs/remotes/${name}`];
1071
+ let listed = false;
1072
+ for (const fullName of fullNames) if (await this.getRefTip(repoPath, records, fullName, cancellation) != null) {
1073
+ listed = true;
1074
+ break;
1075
+ }
1076
+ if (!listed) return void 0;
1077
+ if (await this.isSelfRef(repoPath, name, branch, cancellation)) return void 0;
1078
+ return name;
1079
+ }
1080
+ async getAheadOfBase(repoPath, branch, options, cancellation) {
1081
+ if (branch != null && isDetachedHead(branch)) branch = void 0;
1082
+ const dependsOn = new Set(branch != null ? [`refs/heads/${branch}`] : []);
1083
+ const preferredTarget = options?.preferredTarget?.trim() || void 0;
1084
+ if (preferredTarget == null) return this.getAheadOfBaseInOrder(repoPath, branch, options?.skipUserTarget, dependsOn, cancellation);
1085
+ let target = await this.getNearestForkTarget(repoPath, branch, preferredTarget, dependsOn, cancellation);
1086
+ if (target != null && branch != null && await this.isSelfAtHead(repoPath, target.ref, branch, cancellation)) target = void 0;
1087
+ if (cancellation?.aborted) throw new CancellationError();
1088
+ if (target != null) return this.measureAheadOfBase(repoPath, target, "preferred", dependsOn, cancellation);
1089
+ return this.getAheadOfBaseInOrder(repoPath, branch, options?.skipUserTarget, dependsOn, cancellation);
1090
+ }
1091
+ /**
1092
+ * Resolves the base from the recorded candidates, then the branch it was created from or the default branch, for
1093
+ * {@link getAheadOfBase}
1094
+ */
1095
+ async getAheadOfBaseInOrder(repoPath, branch, skipUserTarget, dependsOn, cancellation) {
985
1096
  let target;
986
- for (const candidate of await this.getRecordedBaseCandidates(repoPath, branch)) {
987
- target = await this.getNearestForkTarget(repoPath, branch, candidate, dependsOn, cancellation);
988
- if (target != null) break;
1097
+ let tier = "default";
1098
+ if (branch != null) for (const candidate of await this.getRecordedBaseCandidates(repoPath, branch, skipUserTarget)) {
1099
+ target = await this.getNearestForkTarget(repoPath, branch, candidate.ref, dependsOn, cancellation);
1100
+ if (target != null) {
1101
+ tier = candidate.tier;
1102
+ break;
1103
+ }
989
1104
  }
990
1105
  if (target == null) {
991
1106
  const defaultBranch = await this.getLocalDefaultBranchName(repoPath, dependsOn, cancellation);
992
- target = await this.getNearestForkTarget(repoPath, branch, defaultBranch, dependsOn, cancellation);
1107
+ if (branch != null && defaultBranch === branch) return this.measureAheadOfSelfUpstream(repoPath, branch, dependsOn, cancellation);
1108
+ const createdFrom = branch != null ? await this.getCreatedFromTarget(repoPath, branch, cancellation) : void 0;
1109
+ if (createdFrom != null) {
1110
+ const created = await this.getCreatedFromForkTarget(repoPath, branch, createdFrom, defaultBranch, dependsOn, cancellation);
1111
+ if (created != null) ({target, tier} = created);
1112
+ } else target = await this.getUpstreamNearestForkTarget(repoPath, branch, defaultBranch, dependsOn, cancellation);
993
1113
  }
994
1114
  if (cancellation?.aborted) throw new CancellationError();
995
- if (target == null) return {
1115
+ if (target == null) {
1116
+ let unborn = false;
1117
+ if (branch != null) try {
1118
+ unborn = findRefTip(await this.provider.refs.getRefs(repoPath, cancellation), `refs/heads/${branch}`) == null && await this.isUnbornHead(repoPath, cancellation);
1119
+ } catch (ex) {
1120
+ if (isCancellationError(ex)) throw ex;
1121
+ }
1122
+ return {
1123
+ base: void 0,
1124
+ reason: unborn ? "unborn" : "none",
1125
+ dependsOn: [...dependsOn]
1126
+ };
1127
+ }
1128
+ if (branch != null && await this.isSelfAtHead(repoPath, target.ref, branch, cancellation)) return {
1129
+ base: void 0,
1130
+ reason: "self",
1131
+ dependsOn: [...dependsOn]
1132
+ };
1133
+ return this.measureAheadOfBase(repoPath, target, tier, dependsOn, cancellation);
1134
+ }
1135
+ /**
1136
+ * The base for a branch created from `createdFrom`: that branch, unless the default branch forked strictly nearer
1137
+ * HEAD, as it does once `createdFrom` merged into it and the branch was rebased onto it. A stacked branch's
1138
+ * unmerged parent forked nearer, and stays its base. `undefined` when neither is usable.
1139
+ */
1140
+ async getCreatedFromForkTarget(repoPath, branch, createdFrom, defaultBranch, dependsOn, cancellation) {
1141
+ const skipDefault = defaultBranch == null || createdFrom === defaultBranch || createdFrom === `refs/heads/${defaultBranch}` || createdFrom === `refs/remotes/${defaultBranch}`;
1142
+ const [created, fallback] = await Promise.all([this.getUpstreamNearestForkTarget(repoPath, branch, createdFrom, dependsOn, cancellation), skipDefault ? void 0 : this.getUpstreamNearestForkTarget(repoPath, branch, defaultBranch, dependsOn, cancellation)]);
1143
+ if (created == null) return fallback != null ? {
1144
+ target: fallback,
1145
+ tier: "default"
1146
+ } : void 0;
1147
+ if (created.forkPoint != null && fallback?.forkPoint != null && created.forkPoint !== fallback.forkPoint && await this.provider.commits.isAncestorOf(repoPath, created.forkPoint, fallback.forkPoint, cancellation)) return {
1148
+ target: fallback,
1149
+ tier: "default"
1150
+ };
1151
+ return {
1152
+ target: created,
1153
+ tier: "created"
1154
+ };
1155
+ }
1156
+ /** Counts HEAD's commits past `target` for {@link getAheadOfBase} */
1157
+ async measureAheadOfBase(repoPath, target, tier, dependsOn, cancellation) {
1158
+ const [countsResult, refsResult] = await Promise.allSettled([this.countAgainstBase(repoPath, target.ref, cancellation), this.provider.refs.getRefs(repoPath, cancellation)]);
1159
+ if (cancellation?.aborted) throw new CancellationError();
1160
+ if (countsResult.status === "rejected") throw countsResult.reason;
1161
+ const counts = countsResult.value;
1162
+ if (counts == null) return {
1163
+ base: void 0,
1164
+ reason: "unborn",
1165
+ dependsOn: [...dependsOn]
1166
+ };
1167
+ return {
1168
+ base: {
1169
+ ref: target.ref,
1170
+ ahead: counts.right,
1171
+ sha: findRefTip(getSettledValue(refsResult, []), target.ref),
1172
+ forkPoint: target.forkPoint,
1173
+ tier
1174
+ },
1175
+ dependsOn: [...dependsOn]
1176
+ };
1177
+ }
1178
+ /** Counts the default branch's own commits past its upstream (see {@link getSelfUpstreamTarget}) for {@link getAheadOfBase} */
1179
+ async measureAheadOfSelfUpstream(repoPath, branch, dependsOn, cancellation) {
1180
+ const upstream = await this.getSelfUpstreamTarget(repoPath, branch, dependsOn, cancellation);
1181
+ if (cancellation?.aborted) throw new CancellationError();
1182
+ if (upstream == null) return {
996
1183
  base: void 0,
1184
+ reason: "self",
997
1185
  dependsOn: [...dependsOn]
998
1186
  };
999
- if (await this.isSelfAtHead(repoPath, target, branch, cancellation)) return {
1187
+ const counts = await this.countAgainstBase(repoPath, upstream.ref, cancellation);
1188
+ if (counts == null) return {
1000
1189
  base: void 0,
1190
+ reason: "unborn",
1001
1191
  dependsOn: [...dependsOn]
1002
1192
  };
1003
- const range = createRevisionRange(target, "HEAD", "...");
1004
- const [countsResult, refsResult] = await Promise.allSettled([this.provider.commits.getLeftRightCommitCount(repoPath, range, void 0, cancellation), this.provider.refs.getRefs(repoPath, cancellation)]);
1193
+ if (counts.left === 0 && counts.right === 0) return {
1194
+ base: void 0,
1195
+ reason: "self",
1196
+ dependsOn: [...dependsOn]
1197
+ };
1198
+ const forkPoint = counts.left === 0 ? upstream.sha : await this.getForkPoint(repoPath, upstream.ref, cancellation);
1005
1199
  if (cancellation?.aborted) throw new CancellationError();
1006
- let counts = getSettledValue(countsResult);
1007
- if (counts == null) try {
1008
- counts = await this.getLeftRightCountUncached(repoPath, range, cancellation);
1009
- } catch (ex) {
1010
- if (isCancellationError(ex)) throw ex;
1011
- if (await this.isUnbornHead(repoPath, cancellation)) return {
1012
- base: void 0,
1013
- dependsOn: [...dependsOn]
1014
- };
1015
- throw ex;
1016
- }
1017
1200
  return {
1018
1201
  base: {
1019
- ref: target,
1202
+ ref: upstream.ref,
1020
1203
  ahead: counts.right,
1021
- sha: findRefTip(getSettledValue(refsResult, []), target)
1204
+ sha: upstream.sha,
1205
+ forkPoint,
1206
+ tier: "upstream"
1022
1207
  },
1023
1208
  dependsOn: [...dependsOn]
1024
1209
  };
1025
1210
  }
1026
- /** Counts `range` with a fresh `rev-list`, bypassing the cached count. Rejects on any failure, with git's error */
1027
- async getLeftRightCountUncached(repoPath, range, cancellation) {
1211
+ /**
1212
+ * HEAD's count against `ref` from their merge base: `left` only `ref` has, `right` only HEAD has. `undefined`
1213
+ * before HEAD's first commit, when there is nothing to count; any other failure rejects with git's error.
1214
+ */
1215
+ async countAgainstBase(repoPath, ref, cancellation) {
1216
+ const range = createRevisionRange(ref, "HEAD", "...");
1217
+ let counts;
1218
+ try {
1219
+ counts = await this.provider.commits.getLeftRightCommitCount(repoPath, range, void 0, cancellation);
1220
+ } catch (ex) {
1221
+ if (isCancellationError(ex)) throw ex;
1222
+ }
1223
+ if (cancellation?.aborted) throw new CancellationError();
1224
+ if (counts != null) return counts;
1225
+ try {
1226
+ return await this.getLeftRightCountUncached(repoPath, range, cancellation);
1227
+ } catch (ex) {
1228
+ if (isCancellationError(ex)) throw ex;
1229
+ if (await this.isUnbornHead(repoPath, cancellation)) return void 0;
1230
+ throw ex;
1231
+ }
1232
+ }
1233
+ async getAheadBehindCounts(repoPath, base, options, cancellation) {
1234
+ const counts = /* @__PURE__ */ new Map();
1235
+ const refs = options?.refs != null ? new Set(options.refs) : void 0;
1236
+ if (refs?.size === 0) return counts;
1237
+ let patterns;
1238
+ if (refs == null) patterns = options?.remotes ? ["refs/heads", "refs/remotes"] : ["refs/heads"];
1239
+ else if (sum(refs, (r) => r.length + 1) < 15e3) patterns = [...refs];
1240
+ else patterns = ["refs/heads", "refs/remotes"];
1241
+ if (!base.includes(")") && await this.git.supports("git:for-each-ref:ahead-behind")) {
1242
+ const result = await this.git.run({
1243
+ cwd: repoPath,
1244
+ cancellation,
1245
+ errors: "throw"
1246
+ }, "for-each-ref", `--format=%(refname)%09%(symref)%09%(ahead-behind:${base})`, ...patterns);
1247
+ if (result.completion.status === "cancelled" || cancellation?.aborted) throw new CancellationError();
1248
+ for (const line of result.stdout.split("\n")) {
1249
+ if (!line) continue;
1250
+ const [ref, symref, counted] = line.split(" ");
1251
+ if (symref || refs != null && !refs.has(ref)) continue;
1252
+ const [ahead, behind] = (counted ?? "").split(" ").map((n) => parseInt(n, 10));
1253
+ if (isNaN(ahead) || isNaN(behind)) throw new Error(`Unexpected output counting the commits ahead of and behind '${base}': ${line}`);
1254
+ counts.set(ref, {
1255
+ ahead,
1256
+ behind
1257
+ });
1258
+ }
1259
+ return counts;
1260
+ }
1261
+ if (base.startsWith("-")) throw new Error(`Invalid base '${base}'`);
1262
+ const [refsResult, baseResult] = await Promise.allSettled([this.git.run({
1263
+ cwd: repoPath,
1264
+ cancellation,
1265
+ errors: "throw"
1266
+ }, "for-each-ref", "--format=%(refname)%09%(symref)%09%(objectname)", ...patterns), this.git.run({
1267
+ cwd: repoPath,
1268
+ cancellation,
1269
+ errors: "throw"
1270
+ }, "rev-parse", "--is-shallow-repository", "--verify", `${base}^{commit}`)]);
1271
+ if (refsResult.status === "rejected") throw refsResult.reason;
1272
+ const result = refsResult.value;
1273
+ if (result.completion.status === "cancelled" || cancellation?.aborted) throw new CancellationError();
1274
+ const refsByTip = /* @__PURE__ */ new Map();
1275
+ for (const line of result.stdout.split("\n")) {
1276
+ const [ref, symref, sha] = line.split(" ");
1277
+ if (!ref || symref || !sha || refs != null && !refs.has(ref)) continue;
1278
+ const tipRefs = refsByTip.get(sha);
1279
+ if (tipRefs == null) refsByTip.set(sha, [ref]);
1280
+ else tipRefs.push(ref);
1281
+ }
1282
+ if (!refsByTip.size) return counts;
1283
+ if (baseResult.status === "rejected") throw baseResult.reason;
1284
+ if (baseResult.value.completion.status === "cancelled" || cancellation?.aborted) throw new CancellationError();
1285
+ const [shallow, baseSha] = baseResult.value.stdout.trim().split("\n");
1286
+ const cacheable = shallow === "false";
1287
+ const countsByTip = /* @__PURE__ */ new Map();
1288
+ const uncounted = [];
1289
+ for (const sha of refsByTip.keys()) {
1290
+ if (sha === baseSha) {
1291
+ countsByTip.set(sha, {
1292
+ ahead: 0,
1293
+ behind: 0
1294
+ });
1295
+ continue;
1296
+ }
1297
+ const range = createRevisionRange(baseSha, sha, "...");
1298
+ const cached = cacheable ? this.aheadBehindBySha.get(range) : void 0;
1299
+ if (cached != null) {
1300
+ this.aheadBehindBySha.touch(range);
1301
+ countsByTip.set(sha, cached);
1302
+ } else uncounted.push(sha);
1303
+ }
1304
+ const results = await mapBounded(uncounted, 8, async (sha) => {
1305
+ const range = createRevisionRange(baseSha, sha, "...");
1306
+ const lr = await this.getLeftRightCountUncached(repoPath, range, cancellation, "background");
1307
+ const tipCounts = {
1308
+ ahead: lr.right,
1309
+ behind: lr.left
1310
+ };
1311
+ if (cacheable) this.aheadBehindBySha.set(range, tipCounts);
1312
+ return {
1313
+ sha,
1314
+ counts: tipCounts
1315
+ };
1316
+ });
1317
+ for (const { sha, counts: tipCounts } of results) countsByTip.set(sha, tipCounts);
1318
+ for (const [sha, tipRefs] of refsByTip) {
1319
+ const tipCounts = countsByTip.get(sha);
1320
+ for (const ref of tipRefs) counts.set(ref, { ...tipCounts });
1321
+ }
1322
+ return counts;
1323
+ }
1324
+ async getMergedBranches(repoPath, into, options, cancellation) {
1325
+ const patterns = options?.remotes ? ["refs/heads", "refs/remotes"] : ["refs/heads"];
1028
1326
  const result = await this.git.run({
1029
1327
  cwd: repoPath,
1030
1328
  cancellation,
1031
1329
  errors: "throw"
1330
+ }, "for-each-ref", `--merged=${into}`, "--format=%(refname)%09%(symref)", ...patterns);
1331
+ if (result.completion.status === "cancelled" || cancellation?.aborted) throw new CancellationError();
1332
+ const merged = /* @__PURE__ */ new Set();
1333
+ for (const line of result.stdout.split("\n")) {
1334
+ if (!line) continue;
1335
+ const [ref, symref] = line.split(" ");
1336
+ if (symref) continue;
1337
+ merged.add(ref);
1338
+ }
1339
+ return merged;
1340
+ }
1341
+ /** Counts `range` with a fresh `rev-list`, bypassing the cached count. Rejects on any failure, with git's error */
1342
+ async getLeftRightCountUncached(repoPath, range, cancellation, priority) {
1343
+ const result = await this.git.run({
1344
+ cwd: repoPath,
1345
+ cancellation,
1346
+ errors: "throw",
1347
+ priority
1032
1348
  }, "rev-list", "--left-right", "--count", range, "--");
1033
1349
  if (result.completion.status === "cancelled" || cancellation?.aborted) throw new CancellationError();
1034
1350
  const parts = result.stdout.split(" ");
@@ -1050,20 +1366,27 @@ var BranchesGitSubProvider = class {
1050
1366
  }, "rev-parse", "--verify", "--quiet", "HEAD")).exitCode === 1;
1051
1367
  }
1052
1368
  /**
1053
- * Whether `ref` names `branch` itself: the same name, or `<remote>/<branch>` for one of the repo's actual remotes.
1054
- * Splitting at the first slash instead would call a local `release/main` the same branch as `main`.
1369
+ * Whether `ref` names `branch` itself: the same name, or `<remote>/<branch>` for one of the repo's actual remotes,
1370
+ * short or as a full ref. Splitting at the first slash instead would call a local `release/main` the same branch
1371
+ * as `main`.
1055
1372
  */
1056
1373
  async isSelfRef(repoPath, ref, branch, cancellation) {
1374
+ if (ref.startsWith("refs/heads/")) return ref.slice(11) === branch;
1375
+ if (ref.startsWith("refs/remotes/")) ref = ref.slice(13);
1057
1376
  if (ref === branch) return true;
1058
1377
  const suffix = `/${branch}`;
1059
1378
  if (!ref.endsWith(suffix)) return false;
1060
1379
  const remoteName = ref.slice(0, -suffix.length);
1061
1380
  if (!remoteName) return false;
1381
+ return (await this.getRemoteNames(repoPath, cancellation)).includes(remoteName);
1382
+ }
1383
+ /** The repo's remote names from the cached remote list; empty when it can't be read */
1384
+ async getRemoteNames(repoPath, cancellation) {
1062
1385
  try {
1063
- return (await this.provider.remotes.getRemotes(repoPath, void 0, cancellation)).some((r) => r.name === remoteName);
1386
+ return (await this.provider.remotes.getRemotes(repoPath, void 0, cancellation)).map((r) => r.name);
1064
1387
  } catch (ex) {
1065
1388
  if (isCancellationError(ex)) throw ex;
1066
- return false;
1389
+ return [];
1067
1390
  }
1068
1391
  }
1069
1392
  /** Whether `ref` names `branch` itself (see {@link isSelfRef}) and sits at HEAD's commit; tips are read only after the name matched */
@@ -1077,7 +1400,7 @@ var BranchesGitSubProvider = class {
1077
1400
  * base a launch recorded. Only the stored value of the latter — `getBaseBranchName` would fall back to a reflog
1078
1401
  * guess, which on a long-lived checkout names whatever branch was last checked out from. A failed read is absent.
1079
1402
  */
1080
- async getRecordedBaseCandidates(repoPath, branch) {
1403
+ async getRecordedBaseCandidates(repoPath, branch, skipUserTarget) {
1081
1404
  const tolerate = async (read) => {
1082
1405
  try {
1083
1406
  return (await read())?.trim() || void 0;
@@ -1087,28 +1410,56 @@ var BranchesGitSubProvider = class {
1087
1410
  }
1088
1411
  };
1089
1412
  const reads = await Promise.all([
1090
- tolerate(() => this.getStoredUserMergeTargetBranchName(repoPath, branch)),
1413
+ skipUserTarget ? void 0 : tolerate(() => this.getStoredUserMergeTargetBranchName(repoPath, branch)),
1091
1414
  tolerate(() => this.getStoredDetectedMergeTargetBranchName(repoPath, branch)),
1092
1415
  tolerate(() => this.provider.config.getGkConfig(repoPath, `branch.${branch}.gk-merge-base`))
1093
1416
  ]);
1417
+ const tiers = [
1418
+ "user",
1419
+ "detected",
1420
+ "recorded"
1421
+ ];
1094
1422
  const candidates = [];
1095
- for (const candidate of reads) if (candidate && !candidates.includes(candidate)) candidates.push(candidate);
1423
+ for (const [i, ref] of reads.entries()) if (ref && !candidates.some((c) => c.ref === ref)) candidates.push({
1424
+ ref,
1425
+ tier: tiers[i]
1426
+ });
1096
1427
  return candidates;
1097
1428
  }
1098
1429
  /**
1099
- * The repo's default branch from local refs only: the `origin/HEAD` symref as its local name, else
1100
- * `init.defaultBranch`, else `main`, else `master` — each only if it names a commit.
1430
+ * The repo's default branch from local refs only, never the network: the `HEAD` symref of the `upstream` remote,
1431
+ * else of `origin`, else of the first remote, as its local branch or else as the remote ref; then, in the same
1432
+ * remote order, `<remote>/main` or `<remote>/master`, again as the local branch of that name or else the remote
1433
+ * ref; then `init.defaultBranch`, `main`, `master` — each only if it names a commit.
1101
1434
  */
1102
1435
  async getLocalDefaultBranchName(repoPath, dependsOn, cancellation) {
1103
- dependsOn.add("refs/remotes/origin/HEAD");
1104
- const symref = await this.getDefaultBranchName(repoPath, "origin", { local: true }, cancellation);
1105
- if (symref) {
1106
- const slash = symref.indexOf("/");
1107
- const local = slash >= 0 ? symref.slice(slash + 1) : symref;
1108
- if (local) {
1109
- addRefDependencies(dependsOn, local);
1110
- if (await this.provider.refs.validateReference(repoPath, local, void 0, cancellation)) return local;
1111
- }
1436
+ let records;
1437
+ const exists = async (ref) => {
1438
+ records ??= await this.provider.refs.getRefs(repoPath, cancellation);
1439
+ return await this.getRefTip(repoPath, records, ref, cancellation) != null;
1440
+ };
1441
+ const remotes = await this.getRemoteNames(repoPath, cancellation);
1442
+ const preferred = new Set([
1443
+ remotes.find((r) => r === "upstream"),
1444
+ remotes.find((r) => r === "origin"),
1445
+ remotes.at(0)
1446
+ ].filter((r) => r != null));
1447
+ for (const remote of preferred) {
1448
+ dependsOn.add(`refs/remotes/${remote}/HEAD`);
1449
+ const symref = await this.getDefaultBranchName(repoPath, remote, { local: true }, cancellation);
1450
+ const local = symref?.startsWith(`${remote}/`) ? symref.slice(remote.length + 1) : void 0;
1451
+ if (!symref || !local) continue;
1452
+ addRefDependencies(dependsOn, local);
1453
+ if (await exists(local)) return local;
1454
+ addRefDependencies(dependsOn, symref);
1455
+ if (await exists(symref)) return symref;
1456
+ }
1457
+ for (const remote of preferred) for (const name of ["main", "master"]) {
1458
+ const remoteRef = `refs/remotes/${remote}/${name}`;
1459
+ dependsOn.add(remoteRef);
1460
+ if (!await exists(remoteRef)) continue;
1461
+ addRefDependencies(dependsOn, name);
1462
+ return await exists(name) ? name : `${remote}/${name}`;
1112
1463
  }
1113
1464
  let fromConfig;
1114
1465
  try {
@@ -1121,20 +1472,55 @@ var BranchesGitSubProvider = class {
1121
1472
  ]) {
1122
1473
  if (!candidate) continue;
1123
1474
  addRefDependencies(dependsOn, candidate);
1124
- if (await this.provider.refs.validateReference(repoPath, candidate, void 0, cancellation)) return candidate;
1475
+ if (await exists(candidate)) return candidate;
1476
+ }
1477
+ }
1478
+ /**
1479
+ * `upstream/<name>` for a created-from or default base `<name>` (or `<remote>/<name>`), as another ref that base
1480
+ * is measured against: in a fork whose `main` tracks `origin/main`, a branch taken off (or since rebased onto) a
1481
+ * newer `upstream/main` would otherwise count the commits `origin` hasn't caught up on. Read from the cached
1482
+ * remote and ref lists; `undefined` when there is no `upstream` remote, it lacks the branch, or the base is that
1483
+ * ref itself.
1484
+ */
1485
+ async getUpstreamContender(repoPath, base, dependsOn, cancellation) {
1486
+ if (!base) return void 0;
1487
+ const remotes = await this.getRemoteNames(repoPath, cancellation);
1488
+ if (!remotes.includes("upstream")) return void 0;
1489
+ const records = await this.provider.refs.getRefs(repoPath, cancellation);
1490
+ let name = base;
1491
+ if (name.startsWith("refs/heads/")) name = name.slice(11);
1492
+ else {
1493
+ const isRemoteRef = name.startsWith("refs/remotes/");
1494
+ if (isRemoteRef) name = name.slice(13);
1495
+ if (isRemoteRef || findRefTip(records, `refs/heads/${name}`) == null) {
1496
+ const remote = remotes.find((r) => name.startsWith(`${r}/`));
1497
+ if (remote != null) name = name.slice(remote.length + 1);
1498
+ }
1125
1499
  }
1500
+ const contender = `upstream/${name}`;
1501
+ const remoteRef = `refs/remotes/${contender}`;
1502
+ if (contender === base || remoteRef === base || findRefTip(records, remoteRef) == null) return void 0;
1503
+ dependsOn.add(remoteRef);
1504
+ return contender;
1505
+ }
1506
+ /** {@link getNearestForkTarget} with the `upstream` remote's copy of `candidate` as its contender */
1507
+ async getUpstreamNearestForkTarget(repoPath, branch, candidate, dependsOn, cancellation) {
1508
+ return this.getNearestForkTarget(repoPath, branch, candidate, dependsOn, cancellation, { contender: await this.getUpstreamContender(repoPath, candidate, dependsOn, cancellation) });
1126
1509
  }
1127
1510
  /**
1128
1511
  * A candidate base as the ref HEAD is measured against: the candidate or its tracking ref, whichever forked
1129
1512
  * nearer HEAD, so a stale local base defers to its upstream while one carrying unpushed work wins over it. A
1130
- * candidate neither forks from is used as-is if it names a commit. `undefined` when the candidate is unusable.
1513
+ * `contender` other than the tracking ref is taken only when it forked strictly nearer than both, so a tie keeps
1514
+ * the candidate's own pick. A candidate none forks from is used as-is if it names a commit. `undefined` when the
1515
+ * candidate is unusable.
1131
1516
  */
1132
- async getNearestForkTarget(repoPath, branch, candidate, dependsOn, cancellation) {
1517
+ async getNearestForkTarget(repoPath, branch, candidate, dependsOn, cancellation, options) {
1133
1518
  if (!candidate || candidate.startsWith("-")) return void 0;
1134
- if (candidate === branch) return void 0;
1519
+ if (candidate === branch || candidate === `refs/heads/${branch}`) return void 0;
1135
1520
  addRefDependencies(dependsOn, candidate);
1521
+ const upstreamName = this.provider.refs.getSymbolicReferenceName(repoPath, `${candidate}@{u}`, void 0, cancellation);
1136
1522
  const getUpstreamFork = async () => {
1137
- const upstream = await this.provider.refs.getSymbolicReferenceName(repoPath, `${candidate}@{u}`, void 0, cancellation);
1523
+ const upstream = await upstreamName;
1138
1524
  if (!upstream || upstream.startsWith("-")) return void 0;
1139
1525
  addRefDependencies(dependsOn, upstream);
1140
1526
  return {
@@ -1142,7 +1528,23 @@ var BranchesGitSubProvider = class {
1142
1528
  mergeBase: await this.getForkPoint(repoPath, upstream, cancellation)
1143
1529
  };
1144
1530
  };
1145
- const [candidateMergeBase, upstreamFork] = await Promise.all([this.getForkPoint(repoPath, candidate, cancellation), getUpstreamFork()]);
1531
+ const contender = options?.contender;
1532
+ const getContenderMergeBase = async () => {
1533
+ if (contender == null) return void 0;
1534
+ const records = await this.provider.refs.getRefs(repoPath, cancellation);
1535
+ const tip = findRefTip(records, contender);
1536
+ if (tip != null) {
1537
+ if (tip === findRefTip(records, candidate)) return void 0;
1538
+ const upstream = await upstreamName;
1539
+ if (upstream && tip === findRefTip(records, upstream)) return void 0;
1540
+ }
1541
+ return this.getForkPoint(repoPath, contender, cancellation);
1542
+ };
1543
+ const [candidateMergeBase, upstreamFork, contenderMergeBase] = await Promise.all([
1544
+ this.getForkPoint(repoPath, candidate, cancellation),
1545
+ getUpstreamFork(),
1546
+ getContenderMergeBase()
1547
+ ]);
1146
1548
  let nearest;
1147
1549
  for (const fork of [{
1148
1550
  ref: candidate,
@@ -1154,8 +1556,59 @@ var BranchesGitSubProvider = class {
1154
1556
  mergeBase: fork.mergeBase
1155
1557
  };
1156
1558
  }
1157
- if (nearest != null) return nearest.ref;
1158
- return await this.provider.refs.validateReference(repoPath, candidate, void 0, cancellation) ? candidate : void 0;
1559
+ if (contender != null && contender !== upstreamFork?.ref && contenderMergeBase != null && nearest != null && nearest.mergeBase !== contenderMergeBase && await this.provider.commits.isAncestorOf(repoPath, nearest.mergeBase, contenderMergeBase, cancellation)) nearest = {
1560
+ ref: contender,
1561
+ mergeBase: contenderMergeBase
1562
+ };
1563
+ if (nearest != null) return {
1564
+ ref: nearest.ref,
1565
+ forkPoint: nearest.mergeBase
1566
+ };
1567
+ return await this.provider.refs.validateReference(repoPath, candidate, void 0, cancellation) ? {
1568
+ ref: candidate,
1569
+ forkPoint: void 0
1570
+ } : void 0;
1571
+ }
1572
+ /**
1573
+ * The base for a branch that is itself the default branch, with its tip: its own upstream, `upstream/<branch>`
1574
+ * when an `upstream` remote has it, else `<branch>@{u}`. Read from the cached ref and branch lists; only a
1575
+ * `<branch>` that isn't checked out spawns, for its `@{u}`. `undefined` when there is neither, or its ref is gone.
1576
+ */
1577
+ async getSelfUpstreamTarget(repoPath, branch, dependsOn, cancellation) {
1578
+ const records = await this.provider.refs.getRefs(repoPath, cancellation);
1579
+ let upstream;
1580
+ if ((await this.getRemoteNames(repoPath, cancellation)).includes("upstream")) {
1581
+ const remoteRef = `refs/remotes/upstream/${branch}`;
1582
+ dependsOn.add(remoteRef);
1583
+ if (await this.getRefTip(repoPath, records, remoteRef, cancellation) != null) upstream = `upstream/${branch}`;
1584
+ }
1585
+ if (upstream == null) {
1586
+ let tracking;
1587
+ const current = await this.getBranch(repoPath, void 0, void 0, cancellation);
1588
+ if (current?.name === branch && !current.detached && !current.remote) {
1589
+ tracking = current.upstream?.name;
1590
+ if (tracking && current.upstream?.missing) {
1591
+ addRefDependencies(dependsOn, tracking);
1592
+ return;
1593
+ }
1594
+ } else tracking = await this.provider.refs.getSymbolicReferenceName(repoPath, `${branch}@{u}`, void 0, cancellation);
1595
+ if (!tracking || tracking.startsWith("-")) return void 0;
1596
+ addRefDependencies(dependsOn, tracking);
1597
+ upstream = tracking;
1598
+ }
1599
+ const sha = await this.getRefTip(repoPath, records, upstream, cancellation);
1600
+ return sha != null ? {
1601
+ ref: upstream,
1602
+ sha
1603
+ } : void 0;
1604
+ }
1605
+ /**
1606
+ * {@link findRefTip} in `records`, the cached ref list. `getRefs` answers a failed enumeration with an empty list,
1607
+ * so an empty one is asked of git instead, rather than read as every ref being absent.
1608
+ */
1609
+ async getRefTip(repoPath, records, name, cancellation) {
1610
+ if (records.length) return findRefTip(records, name);
1611
+ return this.provider.refs.validateReference(repoPath, name, void 0, cancellation);
1159
1612
  }
1160
1613
  /** The merge base of HEAD and `ref`, preferring git's fork-point reading of `ref`'s reflog when it has one */
1161
1614
  async getForkPoint(repoPath, ref, cancellation) {
@@ -1191,25 +1644,61 @@ var BranchesGitSubProvider = class {
1191
1644
  async getStoredUserMergeTargetBranchName(repoPath, ref) {
1192
1645
  return (await this.provider.config.getGkConfig(repoPath, `branch.${ref}.gk-merge-target-user`))?.trim() || void 0;
1193
1646
  }
1194
- /** Updates the last accessed timestamp for the current branch */
1195
- async onCurrentBranchAccessed(repoPath) {
1196
- const branch = await this.getBranch(repoPath);
1197
- if (branch == null || branch.remote || branch.detached) return;
1198
- await this.storeBranchMetadata(repoPath, "gk-last-accessed", branch.name, /* @__PURE__ */ new Date());
1199
- }
1200
- /** Updates the last accessed and modified timestamp for the current branch */
1201
- async onCurrentBranchModified(repoPath) {
1202
- const branch = await this.getBranch(repoPath);
1203
- if (branch == null || branch.remote || branch.detached) return;
1204
- const now = /* @__PURE__ */ new Date();
1205
- await this.storeBranchMetadata(repoPath, "gk-last-accessed", branch.name, now);
1206
- await this.storeBranchMetadata(repoPath, "gk-last-modified", branch.name, now);
1207
- }
1208
- /** Updates the agent last activity timestamp for the current branch */
1209
- async onCurrentBranchAgentActivity(repoPath) {
1210
- const branch = await this.getBranch(repoPath);
1211
- if (branch == null || branch.remote || branch.detached) return;
1212
- await this.storeBranchMetadata(repoPath, "gk-agent-last-activity", branch.name, /* @__PURE__ */ new Date());
1647
+ onCurrentBranchAccessed(repoPath) {
1648
+ return this.recordBranchActivity(repoPath, ["gk-last-accessed"]);
1649
+ }
1650
+ onCurrentBranchModified(repoPath) {
1651
+ return this.recordBranchActivity(repoPath, ["gk-last-accessed", "gk-last-modified"]);
1652
+ }
1653
+ onCurrentBranchAgentActivity(repoPath) {
1654
+ return this.recordBranchActivity(repoPath, ["gk-agent-last-activity"]);
1655
+ }
1656
+ getBranchActivityKey(repoPath, name) {
1657
+ return `${this.cache.getCommonPath(repoPath)}\0${name}`;
1658
+ }
1659
+ forgetBranchCreatedFrom(repoPath, name) {
1660
+ const key = this.getBranchActivityKey(repoPath, name);
1661
+ this._branchCreatedFrom.delete(key);
1662
+ this._branchCreatedFromGenerations.set(key, (this._branchCreatedFromGenerations.get(key) ?? 0) + 1);
1663
+ }
1664
+ /** Forgets this process's throttle marks for `name`, so its next activity re-reads the stored stamps */
1665
+ clearBranchActivityStamps(repoPath, name) {
1666
+ const branchKey = this.getBranchActivityKey(repoPath, name);
1667
+ for (const key of branchActivityKeys) this._branchActivityStamps.delete(`${branchKey}\0${key}`);
1668
+ }
1669
+ /** Never rejects — activity stamps are bookkeeping and callers fire them without a catch */
1670
+ async recordBranchActivity(repoPath, keys) {
1671
+ const scope = getScopedLogger();
1672
+ try {
1673
+ const branch = await this.getBranch(repoPath);
1674
+ if (branch == null || branch.remote || branch.detached) return;
1675
+ const branchKey = this.getBranchActivityKey(repoPath, branch.name);
1676
+ await this._branchActivityCalls.run(branchKey, void 0, async () => {
1677
+ const now = Date.now();
1678
+ const due = [];
1679
+ for (const key of keys) {
1680
+ const stampKey = `${branchKey}\0${key}`;
1681
+ const last = this._branchActivityStamps.get(stampKey);
1682
+ if (last != null && now - last < dateMetadataStaleThresholdMs) continue;
1683
+ this._branchActivityStamps.set(stampKey, now);
1684
+ due.push(key);
1685
+ }
1686
+ if (!due.length) return;
1687
+ const stored = await Promise.allSettled(due.map((key) => this.provider.config.getGkConfig(repoPath, `branch.${branch.name}.${key}`)));
1688
+ const value = new Date(now).toISOString();
1689
+ for (const [i, key] of due.entries()) {
1690
+ const stamp = getSettledValue(stored[i]);
1691
+ const last = stamp != null ? new Date(stamp).getTime() : NaN;
1692
+ if (now - last < dateMetadataStaleThresholdMs) {
1693
+ this._branchActivityStamps.set(`${branchKey}\0${key}`, last);
1694
+ continue;
1695
+ }
1696
+ await this.provider.config.setGkConfig(repoPath, `branch.${branch.name}.${key}`, value);
1697
+ }
1698
+ });
1699
+ } catch (ex) {
1700
+ scope?.error(ex, `Failed to record branch activity (${keys.join(", ")})`);
1701
+ }
1213
1702
  }
1214
1703
  async renameBranch(repoPath, oldName, newName) {
1215
1704
  const args = [
@@ -1223,8 +1712,10 @@ var BranchesGitSubProvider = class {
1223
1712
  await this.provider.config.renameGkConfigBranchSection(repoPath, oldName, newName);
1224
1713
  this.cache.deleteBaseBranchName(repoPath, oldName);
1225
1714
  this.cache.deleteBaseBranchName(repoPath, newName);
1226
- this.context.hooks?.cache?.onReset?.(repoPath, "branches");
1227
- this.context.hooks?.repository?.onChanged?.(repoPath, ["heads"]);
1715
+ this.clearBranchActivityStamps(repoPath, oldName);
1716
+ this.clearBranchActivityStamps(repoPath, newName);
1717
+ this.forgetBranchCreatedFrom(repoPath, oldName);
1718
+ this.forgetBranchCreatedFrom(repoPath, newName);
1228
1719
  } catch (ex) {
1229
1720
  throw getGitCommandError("branch", ex, (reason) => new BranchError({
1230
1721
  reason: reason ?? "other",
@@ -1235,6 +1726,9 @@ var BranchesGitSubProvider = class {
1235
1726
  args
1236
1727
  }
1237
1728
  }, ex));
1729
+ } finally {
1730
+ this.context.hooks?.cache?.onReset?.(repoPath, "branches");
1731
+ this.context.hooks?.repository?.onChanged?.(repoPath, ["config", "heads"]);
1238
1732
  }
1239
1733
  }
1240
1734
  async setUpstreamBranch(repoPath, name, upstream) {
@@ -1250,8 +1744,6 @@ var BranchesGitSubProvider = class {
1250
1744
  ];
1251
1745
  try {
1252
1746
  await this.git.run({ cwd: repoPath }, ...args);
1253
- this.context.hooks?.cache?.onReset?.(repoPath, "branches");
1254
- this.context.hooks?.repository?.onChanged?.(repoPath, ["heads"]);
1255
1747
  } catch (ex) {
1256
1748
  throw getGitCommandError("branch", ex, (reason) => new BranchError({
1257
1749
  reason: reason ?? "other",
@@ -1262,6 +1754,9 @@ var BranchesGitSubProvider = class {
1262
1754
  args
1263
1755
  }
1264
1756
  }, ex));
1757
+ } finally {
1758
+ this.context.hooks?.cache?.onReset?.(repoPath, "branches");
1759
+ this.context.hooks?.repository?.onChanged?.(repoPath, ["config", "heads"]);
1265
1760
  }
1266
1761
  }
1267
1762
  async storeBaseBranchName(repoPath, ref, base, options) {
@@ -1286,22 +1781,14 @@ var BranchesGitSubProvider = class {
1286
1781
  const configMap = await this.provider.config.getGkConfigRegex(commonPath, "^branch\\..*\\.gk-(last-(accessed|modified)|disposition|agent-last-activity)$");
1287
1782
  if (!configMap.size) return metadataMap;
1288
1783
  for (const [key, value] of configMap) {
1289
- if (!key.startsWith("branch.")) continue;
1290
- const keyParts = key.split(".");
1291
- if (keyParts.length < 3) continue;
1292
- const metaKey = keyParts.at(-1);
1293
- const branchName = keyParts.slice(1, -1).join(".");
1294
- let metadata = metadataMap.get(branchName);
1784
+ const parsed = parseBranchMetadataGkConfigKey(key);
1785
+ if (parsed == null) continue;
1786
+ let metadata = metadataMap.get(parsed.branch);
1295
1787
  if (metadata == null) {
1296
1788
  metadata = {};
1297
- metadataMap.set(branchName, metadata);
1298
- }
1299
- if (metaKey === "gk-last-accessed") metadata.lastAccessedAt = value;
1300
- else if (metaKey === "gk-last-modified") metadata.lastModifiedAt = value;
1301
- else if (metaKey === "gk-agent-last-activity") metadata.agentLastActivityAt = value;
1302
- else if (metaKey === "gk-disposition") {
1303
- if (value === "starred" || value === "archived") metadata.disposition = value;
1789
+ metadataMap.set(parsed.branch, metadata);
1304
1790
  }
1791
+ setBranchMetadataValue(metadata, parsed.key, value);
1305
1792
  }
1306
1793
  } catch (ex) {
1307
1794
  debugger;
@@ -1310,11 +1797,6 @@ var BranchesGitSubProvider = class {
1310
1797
  return metadataMap;
1311
1798
  });
1312
1799
  }
1313
- async storeBranchMetadata(repoPath, key, ref, date) {
1314
- const value = await this.provider.config.getGkConfig(repoPath, `branch.${ref}.${key}`);
1315
- if (value != null && date.getTime() - new Date(value).getTime() < dateMetadataStaleThresholdMs) return;
1316
- return this.provider.config.setGkConfig(repoPath, `branch.${ref}.${key}`, date.toISOString());
1317
- }
1318
1800
  async setBranchDisposition(repoPath, branchName, disposition) {
1319
1801
  await this.provider.config.setGkConfig(repoPath, `branch.${branchName}.gk-disposition`, disposition);
1320
1802
  this.context.hooks?.cache?.onReset?.(repoPath, "branches");
@@ -1327,6 +1809,7 @@ __decorate([debug()], BranchesGitSubProvider.prototype, "getBranchContributionsO
1327
1809
  __decorate([debug()], BranchesGitSubProvider.prototype, "getBranchesWithCommits", null);
1328
1810
  __decorate([debug()], BranchesGitSubProvider.prototype, "getCurrentBranchReference", null);
1329
1811
  __decorate([debug({ exit: true })], BranchesGitSubProvider.prototype, "getDefaultBranchName", null);
1812
+ __decorate([debug()], BranchesGitSubProvider.prototype, "setRemoteHeadIfMissing", null);
1330
1813
  __decorate([debug()], BranchesGitSubProvider.prototype, "createBranch", null);
1331
1814
  __decorate([debug()], BranchesGitSubProvider.prototype, "deleteLocalBranch", null);
1332
1815
  __decorate([debug()], BranchesGitSubProvider.prototype, "deleteRemoteBranch", null);
@@ -1337,22 +1820,32 @@ __decorate([debug()], BranchesGitSubProvider.prototype, "getPotentialMergeConfli
1337
1820
  __decorate([debug()], BranchesGitSubProvider.prototype, "getPotentialStashReapplyConflicts", null);
1338
1821
  __decorate([debug({ exit: true })], BranchesGitSubProvider.prototype, "getBaseBranchName", null);
1339
1822
  __decorate([debug({ exit: true })], BranchesGitSubProvider.prototype, "getAheadOfBase", null);
1823
+ __decorate([debug({ exit: true })], BranchesGitSubProvider.prototype, "getAheadBehindCounts", null);
1824
+ __decorate([debug({ exit: true })], BranchesGitSubProvider.prototype, "getMergedBranches", null);
1340
1825
  __decorate([debug({ exit: true })], BranchesGitSubProvider.prototype, "getStoredMergeTargetBranchName", null);
1341
1826
  __decorate([debug({ exit: true })], BranchesGitSubProvider.prototype, "getStoredDetectedMergeTargetBranchName", null);
1342
1827
  __decorate([debug()], BranchesGitSubProvider.prototype, "getStoredUserMergeTargetBranchName", null);
1343
- __decorate([debounce(15e4), debug()], BranchesGitSubProvider.prototype, "onCurrentBranchAccessed", null);
1344
- __decorate([debounce(15e4), debug()], BranchesGitSubProvider.prototype, "onCurrentBranchModified", null);
1345
- __decorate([debounce(15e4), debug()], BranchesGitSubProvider.prototype, "onCurrentBranchAgentActivity", null);
1828
+ __decorate([trace()], BranchesGitSubProvider.prototype, "onCurrentBranchAccessed", null);
1829
+ __decorate([trace()], BranchesGitSubProvider.prototype, "onCurrentBranchModified", null);
1830
+ __decorate([trace()], BranchesGitSubProvider.prototype, "onCurrentBranchAgentActivity", null);
1346
1831
  __decorate([debug()], BranchesGitSubProvider.prototype, "renameBranch", null);
1347
1832
  __decorate([debug()], BranchesGitSubProvider.prototype, "setUpstreamBranch", null);
1348
1833
  __decorate([debug()], BranchesGitSubProvider.prototype, "storeBaseBranchName", null);
1349
1834
  __decorate([debug()], BranchesGitSubProvider.prototype, "storeMergeTargetBranchName", null);
1350
1835
  __decorate([debug()], BranchesGitSubProvider.prototype, "storeUserMergeTargetBranchName", null);
1836
+ /** Each ref list {@link findRefTip} searched, by full name: one base read looks up many names in the same cached list */
1837
+ const refRecordsByName = /* @__PURE__ */ new WeakMap();
1351
1838
  /**
1352
1839
  * The commit `name` resolves to among `records`, tried in git's own order for a short name (`refs/tags/` before
1353
1840
  * `refs/heads/` before `refs/remotes/`); annotated tags peel to their commit. `undefined` when no listed ref matches.
1354
1841
  */
1355
1842
  function findRefTip(records, name) {
1843
+ let byName = refRecordsByName.get(records);
1844
+ if (byName == null) {
1845
+ byName = /* @__PURE__ */ new Map();
1846
+ for (const record of records) if (!byName.has(record.name)) byName.set(record.name, record);
1847
+ refRecordsByName.set(records, byName);
1848
+ }
1356
1849
  for (const candidate of [
1357
1850
  name,
1358
1851
  `refs/${name}`,
@@ -1361,7 +1854,7 @@ function findRefTip(records, name) {
1361
1854
  `refs/remotes/${name}`,
1362
1855
  `refs/remotes/${name}/HEAD`
1363
1856
  ]) {
1364
- const record = records.find((r) => r.name === candidate);
1857
+ const record = byName.get(candidate);
1365
1858
  if (record != null) return record.peeledObjectname || record.objectname;
1366
1859
  }
1367
1860
  }