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