@gitkraken/core-gitlens 0.16.2 → 0.16.4

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 (62) hide show
  1. package/CHANGELOG.md +33 -1
  2. package/dist/git/cache.d.ts +64 -5
  3. package/dist/git/cache.js +75 -5
  4. package/dist/git/cache.js.map +1 -1
  5. package/dist/git/models/branch.d.ts +0 -6
  6. package/dist/git/models/branch.js.map +1 -1
  7. package/dist/git/providers/branches.d.ts +65 -1
  8. package/dist/git/providers/refs.d.ts +3 -2
  9. package/dist/git/providers/shared/remotes.js +5 -5
  10. package/dist/git/providers/shared/remotes.js.map +1 -1
  11. package/dist/git/watching/classifyChange.js +5 -2
  12. package/dist/git/watching/classifyChange.js.map +1 -1
  13. package/dist/git/watching/watchGroup.js +2 -1
  14. package/dist/git/watching/watchGroup.js.map +1 -1
  15. package/dist/git-cli/providers/branches.d.ts +151 -31
  16. package/dist/git-cli/providers/branches.js +918 -253
  17. package/dist/git-cli/providers/branches.js.map +1 -1
  18. package/dist/git-cli/providers/commits.js +37 -20
  19. package/dist/git-cli/providers/commits.js.map +1 -1
  20. package/dist/git-cli/providers/config.js +16 -10
  21. package/dist/git-cli/providers/config.js.map +1 -1
  22. package/dist/git-cli/providers/contributors.js +3 -3
  23. package/dist/git-cli/providers/contributors.js.map +1 -1
  24. package/dist/git-cli/providers/diff.js +15 -12
  25. package/dist/git-cli/providers/diff.js.map +1 -1
  26. package/dist/git-cli/providers/graph.js +17 -14
  27. package/dist/git-cli/providers/graph.js.map +1 -1
  28. package/dist/git-cli/providers/maintenance.js +3 -3
  29. package/dist/git-cli/providers/maintenance.js.map +1 -1
  30. package/dist/git-cli/providers/patch.js +4 -1
  31. package/dist/git-cli/providers/patch.js.map +1 -1
  32. package/dist/git-cli/providers/pausedOperations.js +2 -2
  33. package/dist/git-cli/providers/pausedOperations.js.map +1 -1
  34. package/dist/git-cli/providers/refs.js +16 -13
  35. package/dist/git-cli/providers/refs.js.map +1 -1
  36. package/dist/git-cli/providers/remotes.js +4 -1
  37. package/dist/git-cli/providers/remotes.js.map +1 -1
  38. package/dist/git-cli/providers/revision.js +14 -11
  39. package/dist/git-cli/providers/revision.js.map +1 -1
  40. package/dist/git-cli/providers/stash.js +2 -2
  41. package/dist/git-cli/providers/stash.js.map +1 -1
  42. package/dist/git-cli/providers/status.js +7 -7
  43. package/dist/git-cli/providers/status.js.map +1 -1
  44. package/dist/git-cli/providers/tags.js +6 -3
  45. package/dist/git-cli/providers/tags.js.map +1 -1
  46. package/dist/git-cli/providers/worktrees.js +3 -3
  47. package/dist/git-cli/providers/worktrees.js.map +1 -1
  48. package/dist/plus/git-github/providers/github/branches.d.ts +9 -1
  49. package/dist/plus/git-github/providers/github/branches.js +54 -8
  50. package/dist/plus/git-github/providers/github/branches.js.map +1 -1
  51. package/dist/plus/integrations/providers/issueSorts.d.ts +1 -0
  52. package/dist/plus/integrations/reads/drains.d.ts +1 -0
  53. package/dist/utils/decorators/log.d.ts +3 -1
  54. package/dist/utils/decorators/log.js +61 -45
  55. package/dist/utils/decorators/log.js.map +1 -1
  56. package/dist/utils/logger.d.ts +15 -1
  57. package/dist/utils/logger.js +30 -1
  58. package/dist/utils/logger.js.map +1 -1
  59. package/dist/utils/logger.scoped.d.ts +12 -1
  60. package/dist/utils/logger.scoped.js +38 -4
  61. package/dist/utils/logger.scoped.js.map +1 -1
  62. package/package.json +1 -1
@@ -56,6 +56,26 @@ const branchActivityKeys = [
56
56
  * read costs less
57
57
  */
58
58
  const maxOffHistoryCommits = 64;
59
+ /** Reads a `%(ahead-behind:<base>)` field, `<ahead> <behind>`; `undefined` when it isn't one */
60
+ function parseAheadBehindField(field) {
61
+ const [ahead, behind] = (field ?? "").split(" ").map((n) => parseInt(n, 10));
62
+ if (isNaN(ahead) || isNaN(behind)) return void 0;
63
+ return {
64
+ ahead,
65
+ behind
66
+ };
67
+ }
68
+ /** The named refs as `for-each-ref` patterns, or `undefined` when they're too many to fit one command line */
69
+ function toRefPatterns(refs) {
70
+ return sum(refs, (r) => r.length + 1) < 15e3 ? [...refs] : void 0;
71
+ }
72
+ /** The stored keys a merge target is read from: the user's pick, the detected target and its legacy key, and the recorded base */
73
+ const storedMergeKeys = [
74
+ "gk-merge-target-user",
75
+ "gk-merge-target",
76
+ "gk-target-base",
77
+ "gk-merge-base"
78
+ ];
59
79
  var BranchesGitSubProvider = class {
60
80
  context;
61
81
  git;
@@ -64,12 +84,22 @@ var BranchesGitSubProvider = class {
64
84
  /** Ahead/behind counts by `<baseSha>...<tipSha>` range, from the per-tip fallback; a pair of commits always counts the same */
65
85
  aheadBehindBySha = new LruMap(4096);
66
86
  /**
67
- * For a base that moved only forward, the commits its reflog reaches off the new tip's history (else `false`), by
68
- * `<ref>\0<oldSha>\0<newSha>`, for each cached ref list it was read against: a ref write replaces that list, along
69
- * with the reflog the answer came from. `undefined` when git couldn't say, which isn't kept. A fetch moves one base
70
- * under every worktree at once, so they share the one check, joining it while it runs
87
+ * For a base that moved only forward, what its reflog reaches (else `false`), by `<commonPath>\0<ref>`, then by the
88
+ * tip it moved from. A fetch moves one base under every worktree at once, so they share the one check, joining it
89
+ * while it runs. The reads hold for the tip the ref moved to, the reflog they read, stamped by its file's size and
90
+ * mtime (else by the cached ref list, through what it derives so the list itself isn't held, when there's no file
91
+ * to stamp), and the replace refs, which change what's reachable; a change to any of them starts the ref over. Every
92
+ * move appends to the reflog, so a ref moved away and back to the same tip between reads doesn't find the answer
93
+ * taken before
71
94
  */
72
- baseMovedForward = /* @__PURE__ */ new WeakMap();
95
+ baseMovedForward = new LruMap(64);
96
+ /**
97
+ * For a base that moved, the count of the commits its new tip lacks for each local branch a worktree has checked
98
+ * out, by tip sha, by `<commonPath>\0<newTip>`, with the replace refs it was counted under and the tips it counted.
99
+ * The counts don't depend on the ref or where it moved from, so each worktree the move reached finds its HEAD's
100
+ * count in the one read, even one whose previous answer was taken at another old tip
101
+ */
102
+ aheadOfMovedBase = new LruMap(64);
73
103
  /**
74
104
  * When each `commonPath\0branch\0key` was last stamped by this process. Bounded: a dropped entry only costs a
75
105
  * read of the stored stamp, which still holds off a write under 5 minutes old
@@ -83,6 +113,11 @@ var BranchesGitSubProvider = class {
83
113
  * it once
84
114
  */
85
115
  _branchCreatedFrom = new LruMap(1024);
116
+ /**
117
+ * The branch HEAD was on when each `commonPath\0branch` was first checked out, as git wrote it, for a branch
118
+ * created from `HEAD`; only found ones, since a later checkout can write one
119
+ */
120
+ _branchCheckedOutFrom = new LruMap(1024);
86
121
  /** Bumped each time a key's created-from is forgotten, so a reflog read in flight across that doesn't store it */
87
122
  _branchCreatedFromGenerations = new LruMap(1024);
88
123
  /**
@@ -320,28 +355,17 @@ var BranchesGitSubProvider = class {
320
355
  };
321
356
  };
322
357
  try {
323
- let mergeTarget;
324
- mergeTarget = await this.getStoredMergeTargetBranchName(repoPath, ref, priorityOpts, cancellation);
325
- if (mergeTarget == null && options?.associatedPullRequest != null) {
326
- const pr = await options.associatedPullRequest;
327
- if (pr?.refs?.base != null) {
328
- const branch = await this.getBranch(repoPath, ref, void 0, cancellation);
329
- if (branch == null) return void 0;
330
- const { remote: remoteName } = await resolveBranchRemote(this.provider, repoPath, branch, cancellation);
331
- if (remoteName != null) {
332
- const prTarget = `${remoteName}/${pr.refs.base.branch}`;
333
- const { missing } = await this.provider.refs.getSymbolicReferenceNameOrMissing(repoPath, prTarget, priorityOpts, cancellation);
334
- if (!missing) {
335
- mergeTarget = prTarget;
336
- if (pr.stack == null) this.storeMergeTargetBranchName(repoPath, ref, prTarget, { skipInvalidation: ["branchOverviews"] });
337
- }
338
- }
339
- }
340
- }
341
- mergeTarget ??= await this.getBaseBranchName(repoPath, ref, priorityOpts, cancellation);
342
- mergeTarget ??= await this.getDefaultBranchName(repoPath, void 0, priorityOpts, cancellation);
343
- if (mergeTarget == null) return void 0;
344
- const resolvedTarget = mergeTarget;
358
+ const associatedPullRequest = options?.associatedPullRequest;
359
+ const target = await this.getMergeTarget(repoPath, ref, {
360
+ getAssociatedPullRequest: associatedPullRequest != null ? () => associatedPullRequest : void 0,
361
+ inferBase: true,
362
+ defaultBranch: "remote",
363
+ getFallbackDefaultBranch: options?.getFallbackDefaultBranch,
364
+ keepSelfAtTip: true,
365
+ priority
366
+ }, cancellation);
367
+ if (target == null) return void 0;
368
+ const resolvedTarget = target.ref;
345
369
  return await this.cache.getBranchOverview(repoPath, `${ref}|${resolvedTarget}`, (_commonPath, _cacheable, signal) => fetchBody(resolvedTarget, signal ?? cancellation), { cancellation });
346
370
  } catch (ex) {
347
371
  scope?.error(ex);
@@ -1026,75 +1050,63 @@ var BranchesGitSubProvider = class {
1026
1050
  update = mergeBase != null;
1027
1051
  }
1028
1052
  if (mergeBase != null) {
1029
- const branch = await this.provider.refs.getSymbolicReferenceName(commonPath, mergeBase, priorityOpts);
1030
- if (branch != null) {
1031
- if (update) this.storeBaseBranchName(commonPath, ref, branch, { skipInvalidation: ["branchOverviews", "baseBranchName"] });
1032
- return branch;
1053
+ const { name, missing } = await this.provider.refs.getSymbolicReferenceNameOrMissing(commonPath, mergeBase, priorityOpts, signal);
1054
+ if (!missing) {
1055
+ if (!name) {
1056
+ cacheable.invalidate();
1057
+ return mergeBase;
1058
+ }
1059
+ if (update) this.storeBaseBranchName(commonPath, ref, name, { skipInvalidation: ["branchOverviews", "baseBranchName"] });
1060
+ return name;
1033
1061
  }
1034
1062
  }
1035
1063
  } catch {}
1036
- const reflog = await this.getBaseBranchFromReflog(commonPath, ref, {
1037
- upstream: true,
1038
- priority
1039
- }, signal);
1040
- if (reflog.branch != null) {
1041
- this.storeBaseBranchName(commonPath, ref, reflog.branch, { skipInvalidation: ["branchOverviews", "baseBranchName"] });
1042
- return reflog.branch;
1043
- }
1064
+ const reflog = await this.getBaseBranchFromReflog(repoPath, commonPath, ref, priority, signal);
1065
+ if (reflog.branch != null) return reflog.branch;
1044
1066
  if (reflog.failed) cacheable.invalidate();
1045
1067
  }, cancellation);
1046
1068
  }
1047
1069
  /**
1048
- * `failed` separates "the reflog ran and had no answer" from "the read never happened" — the caller
1049
- * caches the former forever and must not cache the latter.
1070
+ * The branch `ref` was created from, else the branch HEAD was on when it was first checked out, each as its
1071
+ * upstream when it has one. `failed` separates "the reflog ran and had no answer" from "the read never happened" —
1072
+ * the caller caches the former forever and must not cache the latter.
1050
1073
  */
1051
- async getBaseBranchFromReflog(repoPath, ref, options, cancellation) {
1052
- const priority = options?.priority;
1074
+ async getBaseBranchFromReflog(repoPath, commonPath, ref, priority, cancellation) {
1053
1075
  const priorityOpts = priority != null ? { priority } : void 0;
1054
1076
  try {
1055
- const createdFrom = await this.getReflogCreatedFrom(repoPath, ref, priority, cancellation);
1077
+ const createdFrom = await this.getReflogCreatedFrom(commonPath, ref, priority, cancellation);
1056
1078
  if (createdFrom == null) return {
1057
1079
  branch: void 0,
1058
1080
  failed: false
1059
1081
  };
1060
1082
  if (createdFrom !== "HEAD") {
1061
- if (options?.upstream) {
1062
- const upstream = await this.getUpstreamName(repoPath, createdFrom, priorityOpts, cancellation);
1063
- if (upstream) return {
1064
- branch: upstream,
1065
- failed: false
1066
- };
1067
- }
1068
- const name = await this.provider.refs.getSymbolicReferenceName(repoPath, createdFrom, priorityOpts);
1083
+ const upstream = await this.getUpstreamName(commonPath, createdFrom, priorityOpts, cancellation);
1084
+ if (upstream) return {
1085
+ branch: upstream,
1086
+ failed: false
1087
+ };
1088
+ const name = await this.provider.refs.getSymbolicReferenceName(commonPath, createdFrom, priorityOpts);
1069
1089
  if (name) return {
1070
1090
  branch: name,
1071
1091
  failed: false
1072
1092
  };
1073
1093
  }
1074
- const entries = await this.provider.refs.getReflogEntries(repoPath, "HEAD", {
1075
- grep: `checkout: moving from .* to ${ref.replace("refs/heads/", "")}`,
1076
- priority
1077
- }, cancellation);
1078
- if (!entries.length) return {
1094
+ const branch = ref.replace(/^refs\/heads\//, "");
1095
+ const source = await this.getCheckoutSource(await this.getCheckoutPath(repoPath, branch, cancellation), branch, priority, cancellation);
1096
+ if (source == null) return {
1079
1097
  branch: void 0,
1080
1098
  failed: false
1081
1099
  };
1082
- const match = entries.at(-1).message.match(/checkout: moving from ([^\s]+)\s/);
1083
- if (match?.length === 2) {
1084
- let name = match[1];
1085
- if (options?.upstream) {
1086
- const upstream = await this.getUpstreamName(repoPath, name, priorityOpts, cancellation);
1087
- if (upstream) return {
1088
- branch: upstream,
1089
- failed: false
1090
- };
1091
- }
1092
- name = await this.provider.refs.getSymbolicReferenceName(repoPath, name, priorityOpts);
1093
- if (name) return {
1094
- branch: name,
1095
- failed: false
1096
- };
1097
- }
1100
+ const upstream = await this.getUpstreamName(commonPath, source, priorityOpts, cancellation);
1101
+ if (upstream) return {
1102
+ branch: upstream,
1103
+ failed: false
1104
+ };
1105
+ const name = await this.provider.refs.getSymbolicReferenceName(commonPath, source, priorityOpts);
1106
+ if (name) return {
1107
+ branch: name,
1108
+ failed: false
1109
+ };
1098
1110
  } catch (ex) {
1099
1111
  if (isCancellationError(ex)) throw ex;
1100
1112
  const msg = ex?.toString() ?? "";
@@ -1127,12 +1139,36 @@ var BranchesGitSubProvider = class {
1127
1139
  return entries[0].message.match(/branch: Created from (.*)$/)?.[1];
1128
1140
  }
1129
1141
  /**
1130
- * The branch `branch` was created from, as a base for {@link getAheadOfBaseInOrder}: the name its own reflog
1131
- * gives, when that is a local or remote-tracking branch in the cached ref list and not `branch` itself or its
1132
- * copy on a remote (`git switch <branch>` creates it from `<remote>/<branch>`). Never the checkout history in
1133
- * HEAD's reflog, which on a long-lived checkout names whatever was checked out last.
1142
+ * The worktree `branch` is checked out in, whose HEAD reflog holds its checkouts; else the main worktree, never the
1143
+ * caller's, since every worktree of the repository shares the answer
1144
+ */
1145
+ async getCheckoutPath(repoPath, branch, cancellation) {
1146
+ const worktreePath = findRefRecord(await this.provider.refs.getRefs(repoPath, cancellation), `refs/heads/${branch}`)?.worktreePath;
1147
+ return worktreePath ? normalizePath(worktreePath) : this.cache.getCommonPath(repoPath);
1148
+ }
1149
+ /**
1150
+ * The branch HEAD was on when `branch` was first checked out at `repoPath` (the oldest `checkout: moving from
1151
+ * <name> to <branch>` in its HEAD reflog), as git wrote it. `undefined` when there is no such entry. Rejects when the
1152
+ * reflog can't be read.
1134
1153
  */
1135
- async getCreatedFromTarget(repoPath, branch, cancellation) {
1154
+ async getCheckoutSource(repoPath, branch, priority, cancellation) {
1155
+ const entries = await this.provider.refs.getReflogEntries(repoPath, "HEAD", {
1156
+ grep: `checkout: moving from .* to ${toReflogGrepLiteral(branch)}$`,
1157
+ priority
1158
+ }, cancellation);
1159
+ for (let i = entries.length - 1; i >= 0; i--) {
1160
+ const match = /^checkout: moving from (\S+) to (.+)$/.exec(entries[i].message);
1161
+ if (match?.[2] === branch) return match[1];
1162
+ }
1163
+ }
1164
+ /**
1165
+ * The branch `branch` was created from, as a base for {@link resolveBase}: the name its own reflog gives, when that
1166
+ * is a local or remote-tracking branch in the cached ref list and not `branch` itself or its copy on a remote
1167
+ * (`git switch <branch>` creates it from `<remote>/<branch>`). Never the checkout history in HEAD's reflog, which
1168
+ * on a long-lived checkout names whatever was checked out last. `fromHead` when the reflog says it was created
1169
+ * from `HEAD`.
1170
+ */
1171
+ async getCreatedFromTarget(repoPath, branch, reads, cancellation) {
1136
1172
  const key = this.getBranchActivityKey(repoPath, branch);
1137
1173
  let name;
1138
1174
  if (this._branchCreatedFrom.has(key)) name = this._branchCreatedFrom.get(key);
@@ -1142,20 +1178,73 @@ var BranchesGitSubProvider = class {
1142
1178
  name = await this.getReflogCreatedFrom(repoPath, `refs/heads/${branch}`, void 0, cancellation);
1143
1179
  } catch (ex) {
1144
1180
  if (isCancellationError(ex)) throw ex;
1145
- return;
1181
+ reads.pinnable = false;
1182
+ return {
1183
+ name: void 0,
1184
+ fromHead: false
1185
+ };
1146
1186
  }
1147
1187
  if (cancellation?.aborted) throw new CancellationError();
1148
1188
  if (this._branchCreatedFromGenerations.get(key) === generation) this._branchCreatedFrom.set(key, name);
1149
1189
  }
1190
+ if (name === "HEAD") return {
1191
+ name: void 0,
1192
+ fromHead: true
1193
+ };
1194
+ return {
1195
+ name: await this.getListedBaseName(repoPath, name, branch, reads, cancellation),
1196
+ fromHead: false
1197
+ };
1198
+ }
1199
+ /**
1200
+ * For a branch its reflog says was created from `HEAD`, the branch HEAD was on when it was first checked out (see
1201
+ * {@link getCheckoutSource}), read in the worktree it is checked out in, else the main worktree, and kept as a base
1202
+ * like {@link getCreatedFromTarget}'s. Remembered once found, as the created-from name is; a reflog with no such
1203
+ * entry yet marks the answer for eviction on the next checkout, which may write one.
1204
+ */
1205
+ async getInferredCreatedFrom(repoPath, branch, reads, cancellation) {
1206
+ const key = this.getBranchActivityKey(repoPath, branch);
1207
+ let source = this._branchCheckedOutFrom.get(key);
1208
+ if (source == null) {
1209
+ const generation = this._branchCreatedFromGenerations.get(key);
1210
+ try {
1211
+ source = await this.getCheckoutSource(await this.getCheckoutPath(repoPath, branch, cancellation), branch, reads.priority, cancellation);
1212
+ } catch (ex) {
1213
+ if (isCancellationError(ex)) throw ex;
1214
+ reads.pinnable = false;
1215
+ return;
1216
+ }
1217
+ if (cancellation?.aborted) throw new CancellationError();
1218
+ if (source == null) {
1219
+ reads.evictOnCheckout = true;
1220
+ return;
1221
+ }
1222
+ if (this._branchCreatedFromGenerations.get(key) === generation) this._branchCheckedOutFrom.set(key, source);
1223
+ }
1224
+ return this.getListedBaseName(repoPath, source, branch, reads, cancellation);
1225
+ }
1226
+ /**
1227
+ * `name` as a base for `branch`: only a local or remote-tracking branch in the cached ref list — a sha, a tag or a
1228
+ * revision expression (`main~2`) names no `refs/heads/` or `refs/remotes/` ref — and not `branch` itself or its
1229
+ * copy on a remote.
1230
+ */
1231
+ async getListedBaseName(repoPath, name, branch, reads, cancellation) {
1150
1232
  if (!name || name === "HEAD" || name.startsWith("-")) return void 0;
1151
1233
  const records = await this.provider.refs.getRefs(repoPath, cancellation);
1152
1234
  const fullNames = name.startsWith("refs/heads/") || name.startsWith("refs/remotes/") ? [name] : [`refs/heads/${name}`, `refs/remotes/${name}`];
1153
1235
  let listed = false;
1154
- for (const fullName of fullNames) if (await this.getRefTip(repoPath, records, fullName, cancellation) != null) {
1155
- listed = true;
1156
- break;
1236
+ for (const fullName of fullNames) {
1237
+ reads.tipRefs.add(fullName);
1238
+ if (await this.getRefTip(repoPath, records, fullName, cancellation) != null) {
1239
+ listed = true;
1240
+ break;
1241
+ }
1157
1242
  }
1158
1243
  if (!listed) return void 0;
1244
+ if (fullNames.length === 1 && records.length) {
1245
+ const short = name.slice(name.startsWith("refs/heads/") ? 11 : 13);
1246
+ if (resolveRefRecord(records, short)?.name === name) name = short;
1247
+ }
1159
1248
  if (await this.isSelfRef(repoPath, name, branch, cancellation)) return void 0;
1160
1249
  return name;
1161
1250
  }
@@ -1206,14 +1295,30 @@ var BranchesGitSubProvider = class {
1206
1295
  }
1207
1296
  async getAheadOfBase(repoPath, branch, options, cancellation) {
1208
1297
  if (branch != null && isDetachedHead(branch)) branch = void 0;
1209
- const dependsOn = new Set(branch != null ? [`refs/heads/${branch}`] : []);
1210
- const preferredTarget = options?.preferredTarget?.trim() || void 0;
1211
- if (preferredTarget == null) return this.getAheadOfBaseInOrder(repoPath, branch, options?.skipUserTarget, dependsOn, cancellation);
1212
- let target = await this.getNearestForkTarget(repoPath, branch, preferredTarget, dependsOn, cancellation);
1213
- if (target != null && branch != null && await this.isSelfAtHead(repoPath, target.ref, branch, cancellation)) target = void 0;
1214
- if (cancellation?.aborted) throw new CancellationError();
1215
- if (target != null) return this.measureAheadOfBase(repoPath, target, "preferred", dependsOn, cancellation);
1216
- return this.getAheadOfBaseInOrder(repoPath, branch, options?.skipUserTarget, dependsOn, cancellation);
1298
+ const reads = createBaseReads("HEAD", branch != null ? [`refs/heads/${branch}`] : [], void 0);
1299
+ const { dependsOn } = reads;
1300
+ const resolved = await this.resolveBase(repoPath, branch, branch, false, options, reads, cancellation);
1301
+ if (resolved.kind === "upstream") return withDefaultBranch(await this.measureAheadOfSelfUpstream(repoPath, resolved.upstream, reads, cancellation), reads.localDefault);
1302
+ if (resolved.kind === "none") {
1303
+ let unborn = false;
1304
+ if (branch != null) try {
1305
+ unborn = findRefTip(await this.provider.refs.getRefs(repoPath, cancellation), `refs/heads/${branch}`) == null && await this.isUnbornHead(repoPath, cancellation);
1306
+ } catch (ex) {
1307
+ if (isCancellationError(ex)) throw ex;
1308
+ }
1309
+ return withDefaultBranch({
1310
+ base: void 0,
1311
+ reason: unborn ? "unborn" : "none",
1312
+ dependsOn: [...dependsOn]
1313
+ }, reads.localDefault);
1314
+ }
1315
+ if (resolved.tier !== "preferred" && branch != null && await this.isSelfAtTip(repoPath, resolved.target.ref, branch, reads, cancellation)) return withDefaultBranch({
1316
+ base: void 0,
1317
+ reason: "self",
1318
+ dependsOn: [...dependsOn]
1319
+ }, reads.localDefault);
1320
+ const tier = resolved.tier;
1321
+ return withDefaultBranch(await this.measureAheadOfBase(repoPath, resolved.target, tier, dependsOn, cancellation), reads.localDefault);
1217
1322
  }
1218
1323
  async getAheadOfBaseAfterBaseMove(repoPath, branch, previous, moved, cancellation) {
1219
1324
  const { head, base } = previous;
@@ -1246,17 +1351,24 @@ var BranchesGitSubProvider = class {
1246
1351
  if (tip === oldTip || tip === newTip) return void 0;
1247
1352
  }
1248
1353
  if (await this.isShallowRepository(repoPath) !== false) return void 0;
1249
- const offHistory = await this.getMovedForward(repoPath, records, record.name, oldTip, newTip);
1354
+ const movedForward = await this.getMovedForward(repoPath, records, record.name, oldTip, newTip);
1250
1355
  if (cancellation?.aborted) throw new CancellationError();
1251
- if (!offHistory) return void 0;
1252
- const result = await this.git.run({
1253
- cwd: repoPath,
1254
- cancellation,
1255
- errors: "ignore"
1256
- }, "rev-list", "--count", head, "--not", newTip, ...base.forkPoint != null ? [base.forkPoint] : [], ...offHistory, "--");
1356
+ if (!movedForward) return void 0;
1357
+ const { offHistory, logged } = movedForward;
1358
+ const aheadOfNewTip = await this.getAheadOfMovedBase(repoPath, records, newTip, head);
1257
1359
  if (cancellation?.aborted) throw new CancellationError();
1258
- if (result.completion.status !== "exited" || result.exitCode !== 0) return void 0;
1259
- if (parseInt(result.stdout.trim(), 10) !== base.ahead) return void 0;
1360
+ if (aheadOfNewTip != null && aheadOfNewTip !== base.ahead) return void 0;
1361
+ const forkPoint = base.forkPoint;
1362
+ if (aheadOfNewTip == null || offHistory.length !== 0 || forkPoint != null && forkPoint !== oldTip && forkPoint !== newTip && !logged.has(forkPoint)) {
1363
+ const result = await this.git.run({
1364
+ cwd: repoPath,
1365
+ cancellation,
1366
+ errors: "ignore"
1367
+ }, "rev-list", "--count", head, "--not", newTip, ...forkPoint != null ? [forkPoint] : [], ...offHistory, "--");
1368
+ if (cancellation?.aborted) throw new CancellationError();
1369
+ if (result.completion.status !== "exited" || result.exitCode !== 0) return void 0;
1370
+ if (parseInt(result.stdout.trim(), 10) !== base.ahead) return void 0;
1371
+ }
1260
1372
  return withDefaultBranch({
1261
1373
  base: {
1262
1374
  ...base,
@@ -1291,27 +1403,64 @@ var BranchesGitSubProvider = class {
1291
1403
  }
1292
1404
  /**
1293
1405
  * For `ref` (a full name in `records`, the cached ref list) moving from `oldTip` to `newTip`, the commits its reflog
1294
- * reaches off `newTip`'s history when it moved only forward: `oldTip` and every value its reflog took after it are in
1295
- * `newTip`'s history. `--fork-point` reads every reflog entry, so one in between that shares more of HEAD's history
1296
- * than `newTip` does (a branch's commit that landed and was force-pushed away) moves the fork point while the count
1297
- * stands; an older one off `newTip`'s history may hold such a commit too, which each worktree's count checks for.
1298
- * Without a reflog it reads only the tip. `undefined` when it didn't move only forward, or git can't say.
1406
+ * holds and those it reaches off `newTip`'s history, when it moved only forward: `oldTip` and every value its reflog
1407
+ * took after it are in `newTip`'s history. `--fork-point` reads every reflog entry, so one in between that shares
1408
+ * more of HEAD's history than `newTip` does (a branch's commit that landed and was force-pushed away) moves the fork
1409
+ * point while the count stands; an older one off `newTip`'s history may hold such a commit too, which each
1410
+ * worktree's count checks for. Without a reflog it reads only the tip. `undefined` when it didn't move only forward,
1411
+ * or git can't say.
1299
1412
  */
1300
1413
  async getMovedForward(repoPath, records, ref, oldTip, newTip) {
1301
- let checks = this.baseMovedForward.get(records);
1302
- if (checks == null) {
1303
- checks = /* @__PURE__ */ new Map();
1304
- this.baseMovedForward.set(records, checks);
1414
+ const commonPath = this.cache.getCommonPath(repoPath);
1415
+ const key = `${commonPath}\0${ref}`;
1416
+ const inputs = getBaseMoveReadInputs(records);
1417
+ const reflog = await this.getReflogStamp(repoPath, ref) ?? inputs;
1418
+ const { replaced } = inputs;
1419
+ let slot = this.baseMovedForward.get(key);
1420
+ if (slot == null || slot.newTip !== newTip || slot.reflog !== reflog || slot.replaced !== replaced) {
1421
+ slot = {
1422
+ newTip,
1423
+ reflog,
1424
+ replaced,
1425
+ reads: /* @__PURE__ */ new Map()
1426
+ };
1427
+ this.baseMovedForward.set(key, slot);
1428
+ } else this.baseMovedForward.touch(key);
1429
+ const { reads } = slot;
1430
+ let pending = reads.get(oldTip);
1431
+ if (pending == null) {
1432
+ pending = this.readMovedForward(commonPath, ref, oldTip, newTip);
1433
+ reads.set(oldTip, pending);
1305
1434
  }
1306
- const key = `${ref}\0${oldTip}\0${newTip}`;
1307
- let check = checks.get(key);
1308
- if (check == null) {
1309
- check = this.readMovedForward(this.cache.getCommonPath(repoPath), ref, oldTip, newTip);
1310
- checks.set(key, check);
1435
+ const movedForward = await pending;
1436
+ if (movedForward === void 0 && reads.get(oldTip) === pending) reads.delete(oldTip);
1437
+ return movedForward || void 0;
1438
+ }
1439
+ /**
1440
+ * A stamp of `ref`'s reflog file in the common git directory, by its size and mtime; `undefined` when there's no
1441
+ * file to stat (a reftable repository, or a ref without a reflog)
1442
+ */
1443
+ async getReflogStamp(repoPath, ref) {
1444
+ try {
1445
+ const gitDir = await this.provider.config.getGitDir(repoPath);
1446
+ const stat = await this.context.fs.stat(joinUriPath(gitDir.commonUri ?? gitDir.uri, "logs", ref));
1447
+ return stat != null ? `${stat.size}\0${stat.mtime}` : void 0;
1448
+ } catch {
1449
+ return;
1450
+ }
1451
+ }
1452
+ /**
1453
+ * A stamp of the common git directory's `shallow` file, by its size and mtime; `null` when the repository isn't
1454
+ * shallow, `undefined` when that can't be read
1455
+ */
1456
+ async getShallowStamp(repoPath) {
1457
+ try {
1458
+ const gitDir = await this.provider.config.getGitDir(repoPath);
1459
+ const stat = await this.context.fs.stat(joinUriPath(gitDir.commonUri ?? gitDir.uri, "shallow"));
1460
+ return stat != null ? `${stat.size}\0${stat.mtime}` : null;
1461
+ } catch {
1462
+ return;
1311
1463
  }
1312
- const offHistory = await check;
1313
- if (offHistory == null && checks.get(key) === check) checks.delete(key);
1314
- return offHistory || void 0;
1315
1464
  }
1316
1465
  /**
1317
1466
  * Reads {@link getMovedForward} from git, in `commonPath`, which holds the refs, their reflogs and the objects for
@@ -1356,65 +1505,444 @@ var BranchesGitSubProvider = class {
1356
1505
  const off = result.stdout.split("\n").filter((sha) => sha.length > 0);
1357
1506
  if (off.length > maxOffHistoryCommits) return false;
1358
1507
  if (off.some((sha) => since.has(sha))) return false;
1359
- return off;
1508
+ return {
1509
+ offHistory: off,
1510
+ logged
1511
+ };
1512
+ } catch {
1513
+ return;
1514
+ }
1515
+ }
1516
+ /**
1517
+ * For a base moved to `newTip`, the count of the commits `newTip` lacks for `head`, from one count of each local
1518
+ * branch a worktree has checked out in `records` (the cached ref list), by tip sha, so a detached HEAD at one of
1519
+ * those tips finds its count too. Only those branches are counted: the one walk runs until every counted branch
1520
+ * meets `newTip`, so an unrelated old or orphan branch would make it deeper than any worktree's own count. Which
1521
+ * branches it counts only bounds that walk, since a commit's count past `newTip` doesn't depend on what else is
1522
+ * counted, so a read that counted `head` answers it for a later list (one with a branch created at `newTip` since,
1523
+ * say) while the replace refs match; a `head` it didn't count starts a read over the current list's checked-out
1524
+ * branches. `undefined` when no checked-out branch is at `head` or `records` can't tell which are checked out, git
1525
+ * can't count them in one pass (before 2.41), or git couldn't say.
1526
+ */
1527
+ async getAheadOfMovedBase(repoPath, records, newTip, head) {
1528
+ const { checkedOut, checkedOutTips, replaced } = getBaseMoveReadInputs(records);
1529
+ if (!checkedOut?.size || !checkedOutTips.has(head)) return void 0;
1530
+ const commonPath = this.cache.getCommonPath(repoPath);
1531
+ const key = `${commonPath}\0${newTip}`;
1532
+ let slot = this.aheadOfMovedBase.get(key);
1533
+ if (slot == null || slot.replaced !== replaced || !slot.tips.has(head)) {
1534
+ slot = {
1535
+ replaced,
1536
+ tips: checkedOutTips,
1537
+ read: this.readAheadOfMovedBase(commonPath, checkedOut, newTip)
1538
+ };
1539
+ this.aheadOfMovedBase.set(key, slot);
1540
+ } else this.aheadOfMovedBase.touch(key);
1541
+ const { read } = slot;
1542
+ const aheadByTip = await read;
1543
+ if (aheadByTip === void 0 && this.aheadOfMovedBase.get(key)?.read === read) this.aheadOfMovedBase.delete(key);
1544
+ return aheadByTip?.get(head);
1545
+ }
1546
+ /**
1547
+ * Reads {@link getAheadOfMovedBase} for the `checkedOut` branches from git, in `commonPath`, which holds every
1548
+ * worktree's branches. Not bound to one caller's cancellation, since every worktree the move reached rides this one
1549
+ * read.
1550
+ */
1551
+ async readAheadOfMovedBase(commonPath, checkedOut, newTip) {
1552
+ try {
1553
+ if (!await this.git.supports("git:for-each-ref:ahead-behind")) return void 0;
1554
+ const patterns = toRefPatterns(checkedOut) ?? ["refs/heads"];
1555
+ const result = await this.git.run({
1556
+ cwd: commonPath,
1557
+ errors: "ignore"
1558
+ }, "for-each-ref", `--format=%(refname)%09%(objectname)%09%(ahead-behind:${newTip})`, ...patterns);
1559
+ this.provider.maintenance?.request(commonPath, "commit-graph");
1560
+ if (result.completion.status !== "exited" || result.exitCode !== 0) return void 0;
1561
+ const aheadByTip = /* @__PURE__ */ new Map();
1562
+ for (const line of result.stdout.split("\n")) {
1563
+ if (!line) continue;
1564
+ const [ref, sha, field] = line.split(" ");
1565
+ if (!ref || !sha) return void 0;
1566
+ if (!checkedOut.has(ref)) continue;
1567
+ const counted = parseAheadBehindField(field);
1568
+ if (counted == null) continue;
1569
+ aheadByTip.set(sha, counted.ahead);
1570
+ }
1571
+ return aheadByTip;
1360
1572
  } catch {
1361
1573
  return;
1362
1574
  }
1363
1575
  }
1576
+ async getMergeTarget(repoPath, branch, options, cancellation) {
1577
+ if (!branch || isDetachedHead(branch)) return void 0;
1578
+ const cacheKey = getMergeTargetCacheKey(branch, options);
1579
+ let resolved = false;
1580
+ const factory = async (_commonPath, cacheable, signal) => {
1581
+ resolved = true;
1582
+ const { entry, pinnable } = await this.resolveMergeTarget(repoPath, branch, options, signal ?? cancellation);
1583
+ if (!pinnable) cacheable.invalidate();
1584
+ return entry;
1585
+ };
1586
+ let entry = await this.cache.getMergeTarget(repoPath, cacheKey, factory, cancellation);
1587
+ if (!resolved && !await this.isMergeTargetCurrent(repoPath, branch, entry, options, cancellation)) {
1588
+ this.cache.deleteMergeTarget(repoPath, cacheKey, entry);
1589
+ entry = await this.cache.getMergeTarget(repoPath, cacheKey, factory, cancellation);
1590
+ }
1591
+ if (cancellation?.aborted) throw new CancellationError();
1592
+ return entry.selfAtTip && !options?.keepSelfAtTip ? void 0 : entry.result;
1593
+ }
1594
+ /**
1595
+ * Where {@link getMergeTarget} measures `branch` from: its local branch's ref, or — for a remote-tracking branch given
1596
+ * by its short name (`origin/feature`) — that ref, with `selfName` its name on the remote
1597
+ */
1598
+ async getMergeTargetSource(repoPath, branch, records, cancellation) {
1599
+ if (findRefRecord(records, `refs/heads/${branch}`) != null || findRefRecord(records, `refs/remotes/${branch}`) == null) return {
1600
+ from: `refs/heads/${branch}`,
1601
+ selfName: branch,
1602
+ remoteBranch: false
1603
+ };
1604
+ const split = splitRemoteBranchName(branch, await this.getRemoteNames(repoPath, cancellation));
1605
+ return {
1606
+ from: `refs/remotes/${branch}`,
1607
+ selfName: split?.[1] ?? branch,
1608
+ remoteBranch: true
1609
+ };
1610
+ }
1611
+ /** Resolves {@link getMergeTarget}'s answer with what it was read from; `pinnable` is false when it can't be confirmed later */
1612
+ async resolveMergeTarget(repoPath, branch, options, cancellation) {
1613
+ const [records, configKey, shallow] = await Promise.all([
1614
+ this.provider.refs.getRefs(repoPath, cancellation),
1615
+ tolerateRead(() => this.getAheadOfBaseConfigKey(repoPath, cancellation)),
1616
+ this.getShallowStamp(repoPath)
1617
+ ]);
1618
+ const { replaced } = getBaseMoveReadInputs(records);
1619
+ const { from, selfName, remoteBranch } = await this.getMergeTargetSource(repoPath, branch, records, cancellation);
1620
+ if (records.length && getListedTip(records, from) == null) return {
1621
+ entry: {
1622
+ result: void 0,
1623
+ selfAtTip: false,
1624
+ tips: [[from, void 0], [`refs/remotes/${branch}`, void 0]],
1625
+ replaced,
1626
+ shallow: shallow ?? null,
1627
+ configKey,
1628
+ stored: void 0,
1629
+ evictOnCheckout: false
1630
+ },
1631
+ pinnable: configKey != null && shallow !== void 0
1632
+ };
1633
+ const reads = createBaseReads(from, [from], options?.priority);
1634
+ reads.pinnable = records.length !== 0 && configKey != null && shallow !== void 0;
1635
+ const resolved = await this.resolveBase(repoPath, branch, selfName, remoteBranch, options, reads, cancellation);
1636
+ let result;
1637
+ let selfAtTip = false;
1638
+ if (resolved.kind === "upstream") {
1639
+ const upstream = resolved.upstream;
1640
+ if (upstream != null) {
1641
+ selfAtTip = upstream.sha === await this.getRefTip(repoPath, records, from, cancellation);
1642
+ result = {
1643
+ ref: upstream.ref,
1644
+ forkPoint: selfAtTip ? upstream.sha : await this.getForkPoint(repoPath, upstream.ref, reads, cancellation),
1645
+ tier: "upstream"
1646
+ };
1647
+ }
1648
+ } else if (resolved.kind === "target") {
1649
+ result = {
1650
+ ref: resolved.target.ref,
1651
+ forkPoint: resolved.target.forkPoint,
1652
+ tier: resolved.tier
1653
+ };
1654
+ selfAtTip = resolved.tier !== "preferred" && await this.isSelfAtTip(repoPath, result.ref, selfName, reads, cancellation);
1655
+ }
1656
+ if (cancellation?.aborted) throw new CancellationError();
1657
+ const tips = [];
1658
+ for (const ref of /* @__PURE__ */ new Set([...reads.dependsOn, ...reads.tipRefs])) tips.push([ref, getListedTip(records, ref)]);
1659
+ return {
1660
+ entry: {
1661
+ result,
1662
+ selfAtTip,
1663
+ tips,
1664
+ replaced,
1665
+ shallow: shallow ?? null,
1666
+ configKey,
1667
+ stored: reads.stored,
1668
+ pullRequest: reads.pullRequest,
1669
+ localDefault: reads.localDefault,
1670
+ remoteDefault: reads.remoteDefault,
1671
+ fallbackDefault: reads.fallbackDefault,
1672
+ evictOnCheckout: reads.evictOnCheckout
1673
+ },
1674
+ pinnable: reads.pinnable
1675
+ };
1676
+ }
1677
+ /** Whether everything `entry` was read from still reads the same; while nothing changed, every read here is cached */
1678
+ async isMergeTargetCurrent(repoPath, branch, entry, options, cancellation) {
1679
+ const records = await this.provider.refs.getRefs(repoPath, cancellation);
1680
+ if (!records.length) return false;
1681
+ for (const [ref, tip] of entry.tips) if (getListedTip(records, ref) !== tip) return false;
1682
+ if (getBaseMoveReadInputs(records).replaced !== entry.replaced) return false;
1683
+ if (entry.shallow != null && await this.getShallowStamp(repoPath) !== entry.shallow) return false;
1684
+ if (await tolerateRead(() => this.getAheadOfBaseConfigKey(repoPath, cancellation)) !== entry.configKey) return false;
1685
+ if (entry.stored != null) {
1686
+ if ((await this.readStoredMergeKeys(repoPath, branch, options?.inferBase, void 0)).some((value, i) => value !== entry.stored[i])) return false;
1687
+ }
1688
+ if (entry.localDefault !== void 0) {
1689
+ if ((await this.getLocalDefaultBranchName(repoPath, /* @__PURE__ */ new Set(), cancellation) ?? null) !== entry.localDefault) return false;
1690
+ }
1691
+ if (entry.remoteDefault != null) {
1692
+ const cached = this.cache.peekDefaultBranchName(repoPath, entry.remoteDefault.remote);
1693
+ if (cached == null) return false;
1694
+ if ((await tolerateRead(() => cached) ?? null) !== entry.remoteDefault.name) return false;
1695
+ }
1696
+ if (entry.fallbackDefault !== void 0) {
1697
+ if ((((await tolerateRead(options?.getFallbackDefaultBranch))?.trim() || void 0) ?? null) !== entry.fallbackDefault) return false;
1698
+ }
1699
+ if (entry.pullRequest !== void 0) {
1700
+ const getAssociatedPullRequest = options?.getAssociatedPullRequest;
1701
+ if (getAssociatedPullRequest == null) return false;
1702
+ if (((await this.getPullRequestBaseCandidate(repoPath, branch, getAssociatedPullRequest, void 0, cancellation))?.ref ?? null) !== entry.pullRequest) return false;
1703
+ }
1704
+ return true;
1705
+ }
1364
1706
  /**
1365
- * Resolves the base from the recorded candidates, then the branch it was created from or the default branch, for
1366
- * {@link getAheadOfBase}
1707
+ * The base `branch` is measured against from `reads.from`, in {@link getMergeTarget}'s order; `getAheadOfBase`'s
1708
+ * options never reach the pull request, the reflog guess or the remote. `selfName` is the name a self check
1709
+ * compares with: `branch`, or a remote-tracking branch's name on its remote, which has no stored targets, reflog or
1710
+ * upstream of its own. A `'preferred'` target is already known not to be the branch itself at `from`'s commit; any
1711
+ * other target isn't checked here.
1367
1712
  */
1368
- async getAheadOfBaseInOrder(repoPath, branch, skipUserTarget, dependsOn, cancellation) {
1713
+ async resolveBase(repoPath, branch, selfName, remoteBranch, options, reads, cancellation) {
1714
+ const preferredTarget = options?.preferredTarget?.trim() || void 0;
1715
+ if (preferredTarget != null) {
1716
+ let target = await this.getNearestForkTarget(repoPath, branch, preferredTarget, reads, cancellation);
1717
+ if (target != null && selfName != null && await this.isSelfAtTip(repoPath, target.ref, selfName, reads, cancellation)) target = void 0;
1718
+ if (cancellation?.aborted) throw new CancellationError();
1719
+ if (target != null) return {
1720
+ kind: "target",
1721
+ target,
1722
+ tier: "preferred"
1723
+ };
1724
+ }
1369
1725
  let target;
1370
1726
  let tier = "default";
1371
- let resolvedDefault;
1372
- if (branch != null) for (const candidate of await this.getRecordedBaseCandidates(repoPath, branch, skipUserTarget)) {
1373
- target = await this.getNearestForkTarget(repoPath, branch, candidate.ref, dependsOn, cancellation);
1374
- if (target != null) {
1375
- tier = candidate.tier;
1376
- break;
1377
- }
1727
+ if (branch != null) {
1728
+ const stored = await this.getStoredBaseTarget(repoPath, branch, remoteBranch, options, reads, cancellation);
1729
+ if (stored != null) ({target, tier} = stored);
1378
1730
  }
1379
1731
  if (target == null) {
1380
- const defaultBranch = await this.getLocalDefaultBranchName(repoPath, dependsOn, cancellation);
1381
- resolvedDefault = defaultBranch ?? null;
1382
- if (branch != null && defaultBranch === branch) return withDefaultBranch(await this.measureAheadOfSelfUpstream(repoPath, branch, dependsOn, cancellation), resolvedDefault);
1383
- const createdFrom = branch != null ? await this.getCreatedFromTarget(repoPath, branch, cancellation) : void 0;
1732
+ const defaultBranch = await this.getDefaultBranchForBase(repoPath, options, reads, cancellation);
1733
+ if (branch != null && defaultBranch === branch) {
1734
+ if (remoteBranch) {
1735
+ addRefDependencies(reads.dependsOn, branch);
1736
+ const records = await this.provider.refs.getRefs(repoPath, cancellation);
1737
+ const tip = await this.getRefTip(repoPath, records, reads.from, cancellation);
1738
+ if (cancellation?.aborted) throw new CancellationError();
1739
+ return tip != null ? {
1740
+ kind: "target",
1741
+ target: {
1742
+ ref: branch,
1743
+ forkPoint: tip
1744
+ },
1745
+ tier: "default"
1746
+ } : { kind: "none" };
1747
+ }
1748
+ return {
1749
+ kind: "upstream",
1750
+ upstream: await this.getSelfUpstreamTarget(repoPath, branch, reads, cancellation)
1751
+ };
1752
+ }
1753
+ const createdFrom = branch != null && !remoteBranch ? await this.getCreatedFromSource(repoPath, branch, options, reads, cancellation) : void 0;
1384
1754
  if (createdFrom != null) {
1385
- const created = await this.getCreatedFromForkTarget(repoPath, branch, createdFrom, defaultBranch, dependsOn, cancellation);
1755
+ const created = await this.getCreatedFromForkTarget(repoPath, branch, createdFrom, defaultBranch, reads, cancellation);
1386
1756
  if (created != null) ({target, tier} = created);
1387
- } else target = await this.getUpstreamNearestForkTarget(repoPath, branch, defaultBranch, dependsOn, cancellation);
1757
+ } else target = await this.getUpstreamNearestForkTarget(repoPath, branch, defaultBranch, reads, cancellation);
1388
1758
  }
1389
1759
  if (cancellation?.aborted) throw new CancellationError();
1390
- if (target == null) {
1391
- let unborn = false;
1392
- if (branch != null) try {
1393
- unborn = findRefTip(await this.provider.refs.getRefs(repoPath, cancellation), `refs/heads/${branch}`) == null && await this.isUnbornHead(repoPath, cancellation);
1760
+ return target != null ? {
1761
+ kind: "target",
1762
+ target,
1763
+ tier
1764
+ } : { kind: "none" };
1765
+ }
1766
+ /**
1767
+ * The first usable stored candidate, most trusted first: the user's merge target (unless `skipUserTarget`), the
1768
+ * detected one, the pull request's base (`getAssociatedPullRequest`, read only when neither is usable), then the
1769
+ * recorded base — only its stored value (and with `inferBase`, VS Code's), never `getBaseBranchName`'s reflog
1770
+ * guess, which on a long-lived checkout names whatever branch was last checked out from. A remote-tracking branch
1771
+ * has no stored keys of its own, so only its pull request's base is tried, and never recorded.
1772
+ */
1773
+ async getStoredBaseTarget(repoPath, branch, remoteBranch, options, reads, cancellation) {
1774
+ let stored = [];
1775
+ if (!remoteBranch) {
1776
+ stored = await this.readStoredMergeKeys(repoPath, branch, options?.inferBase, reads);
1777
+ reads.stored = stored;
1778
+ }
1779
+ const [user, detected, legacyDetected, recorded, vscodeRecorded] = stored;
1780
+ const tried = /* @__PURE__ */ new Set();
1781
+ const measure = async (candidate) => {
1782
+ candidate = candidate?.trim();
1783
+ if (!candidate || tried.has(candidate)) return void 0;
1784
+ tried.add(candidate);
1785
+ return this.getNearestForkTarget(repoPath, branch, candidate, reads, cancellation);
1786
+ };
1787
+ let target;
1788
+ if (!options?.skipUserTarget) {
1789
+ target = await measure(user);
1790
+ if (target != null) return {
1791
+ target,
1792
+ tier: "user"
1793
+ };
1794
+ }
1795
+ target = await measure(detected ?? legacyDetected);
1796
+ if (target != null) return {
1797
+ target,
1798
+ tier: "detected"
1799
+ };
1800
+ if (options?.getAssociatedPullRequest != null) {
1801
+ const pr = await this.getPullRequestBaseCandidate(repoPath, branch, options.getAssociatedPullRequest, reads, cancellation);
1802
+ target = await measure(pr?.ref);
1803
+ if (pr != null && target != null) {
1804
+ if (pr.stacked || remoteBranch) reads.pullRequest = pr.ref;
1805
+ else {
1806
+ this.storeMergeTargetBranchName(repoPath, branch, pr.ref, { skipInvalidation: ["branchOverviews"] });
1807
+ reads.pinnable = false;
1808
+ }
1809
+ return {
1810
+ target,
1811
+ tier: "pr"
1812
+ };
1813
+ }
1814
+ reads.pullRequest = pr?.ref ?? null;
1815
+ }
1816
+ target = await measure(recorded);
1817
+ if (target != null) return {
1818
+ target,
1819
+ tier: "recorded"
1820
+ };
1821
+ if (!recorded?.trim() && vscodeRecorded?.trim()) {
1822
+ target = await measure(vscodeRecorded);
1823
+ if (target != null) {
1824
+ this.storeBaseBranchName(repoPath, branch, vscodeRecorded.trim(), { skipInvalidation: ["branchOverviews", "baseBranchName"] });
1825
+ reads.pinnable = false;
1826
+ return {
1827
+ target,
1828
+ tier: "recorded"
1829
+ };
1830
+ }
1831
+ }
1832
+ }
1833
+ /**
1834
+ * The branch's stored merge keys as read, in {@link storedMergeKeys} order, then VS Code's recorded base when
1835
+ * `inferBase`. A failed read is absent, and leaves `reads`' answer unpinned.
1836
+ */
1837
+ readStoredMergeKeys(repoPath, branch, inferBase, reads) {
1838
+ const read = async (get) => {
1839
+ try {
1840
+ return await get();
1394
1841
  } catch (ex) {
1395
1842
  if (isCancellationError(ex)) throw ex;
1843
+ if (reads != null) reads.pinnable = false;
1844
+ return;
1396
1845
  }
1397
- return withDefaultBranch({
1398
- base: void 0,
1399
- reason: unborn ? "unborn" : "none",
1400
- dependsOn: [...dependsOn]
1401
- }, resolvedDefault);
1846
+ };
1847
+ return Promise.all([...storedMergeKeys.map((key) => read(() => this.provider.config.getGkConfig(repoPath, `branch.${branch}.${key}`))), inferBase ? read(() => this.provider.config.getConfig(repoPath, `branch.${branch}.vscode-merge-base`)) : void 0]);
1848
+ }
1849
+ /**
1850
+ * The pull request's base as a candidate on the branch's remote; `undefined` when there is none, the branch has no
1851
+ * remote, or either can't be read (which leaves `reads`' answer unpinned)
1852
+ */
1853
+ async getPullRequestBaseCandidate(repoPath, branch, getAssociatedPullRequest, reads, cancellation) {
1854
+ try {
1855
+ const pr = await getAssociatedPullRequest();
1856
+ const base = pr?.refs?.base?.branch?.trim();
1857
+ if (!base) return void 0;
1858
+ const ref = await this.getBranch(repoPath, branch, void 0, cancellation);
1859
+ if (ref == null) return void 0;
1860
+ const { remote } = await resolveBranchRemote(this.provider, repoPath, ref, cancellation);
1861
+ if (remote == null) return void 0;
1862
+ return {
1863
+ ref: `${remote}/${base}`,
1864
+ stacked: pr?.stack != null
1865
+ };
1866
+ } catch (ex) {
1867
+ if (isCancellationError(ex) && (cancellation?.aborted ?? true)) throw ex;
1868
+ if (reads != null) reads.pinnable = false;
1869
+ return;
1402
1870
  }
1403
- if (branch != null && await this.isSelfAtHead(repoPath, target.ref, branch, cancellation)) return withDefaultBranch({
1404
- base: void 0,
1405
- reason: "self",
1406
- dependsOn: [...dependsOn]
1407
- }, resolvedDefault);
1408
- return withDefaultBranch(await this.measureAheadOfBase(repoPath, target, tier, dependsOn, cancellation), resolvedDefault);
1871
+ }
1872
+ /**
1873
+ * The default branch for {@link resolveBase}: {@link getLocalDefaultBranchName}; when that names none, with
1874
+ * `defaultBranch: 'remote'`, the default the first of those remotes reports; then `getFallbackDefaultBranch`. A
1875
+ * remote-qualified answer is taken as its local branch when there is one
1876
+ */
1877
+ async getDefaultBranchForBase(repoPath, options, reads, cancellation) {
1878
+ let name = await this.getLocalDefaultBranchName(repoPath, reads.dependsOn, cancellation);
1879
+ reads.localDefault = name ?? null;
1880
+ if (name != null) return name;
1881
+ if (options?.defaultBranch === "remote") {
1882
+ const [remote] = await this.getPreferredRemotes(repoPath, cancellation);
1883
+ if (remote != null) {
1884
+ let symref;
1885
+ try {
1886
+ symref = await this.getDefaultBranchName(repoPath, remote, reads.priority != null ? { priority: reads.priority } : void 0, cancellation);
1887
+ } catch (ex) {
1888
+ if (isCancellationError(ex)) throw ex;
1889
+ reads.pinnable = false;
1890
+ }
1891
+ reads.remoteDefault = {
1892
+ remote,
1893
+ name: symref ?? null
1894
+ };
1895
+ const local = symref?.startsWith(`${remote}/`) ? symref.slice(remote.length + 1) : void 0;
1896
+ if (symref && local) name = await this.preferListedLocalBranch(repoPath, symref, local, reads, cancellation);
1897
+ }
1898
+ }
1899
+ if (name == null && options?.getFallbackDefaultBranch != null) {
1900
+ let fallback;
1901
+ try {
1902
+ fallback = (await options.getFallbackDefaultBranch())?.trim() || void 0;
1903
+ } catch (ex) {
1904
+ if (isCancellationError(ex) && (cancellation?.aborted ?? true)) throw ex;
1905
+ reads.pinnable = false;
1906
+ }
1907
+ reads.fallbackDefault = fallback ?? null;
1908
+ name = fallback;
1909
+ const local = fallback != null ? splitRemoteBranchName(fallback, await this.getRemoteNames(repoPath, cancellation))?.[1] : void 0;
1910
+ if (fallback != null && local) name = await this.preferListedLocalBranch(repoPath, fallback, local, reads, cancellation);
1911
+ }
1912
+ return name;
1913
+ }
1914
+ /** `local` when the cached ref list names that branch (depended on either way), else `remoteName` */
1915
+ async preferListedLocalBranch(repoPath, remoteName, local, reads, cancellation) {
1916
+ addRefDependencies(reads.dependsOn, local);
1917
+ const records = await this.provider.refs.getRefs(repoPath, cancellation);
1918
+ return await this.getRefTip(repoPath, records, local, cancellation) != null ? local : remoteName;
1919
+ }
1920
+ /**
1921
+ * The branch `branch` was created from (tier `'created'`); with `inferBase`, for one created from `HEAD`, the
1922
+ * branch HEAD was on when it was first checked out (tier `'inferred'`)
1923
+ */
1924
+ async getCreatedFromSource(repoPath, branch, options, reads, cancellation) {
1925
+ const createdFrom = await this.getCreatedFromTarget(repoPath, branch, reads, cancellation);
1926
+ if (createdFrom.name != null) return {
1927
+ name: createdFrom.name,
1928
+ tier: "created"
1929
+ };
1930
+ if (!createdFrom.fromHead || !options?.inferBase) return void 0;
1931
+ const inferred = await this.getInferredCreatedFrom(repoPath, branch, reads, cancellation);
1932
+ return inferred != null ? {
1933
+ name: inferred,
1934
+ tier: "inferred"
1935
+ } : void 0;
1409
1936
  }
1410
1937
  /**
1411
1938
  * The base for a branch created from `createdFrom`: that branch, unless the default branch forked strictly nearer
1412
1939
  * HEAD, as it does once `createdFrom` merged into it and the branch was rebased onto it. A stacked branch's
1413
1940
  * unmerged parent forked nearer, and stays its base. `undefined` when neither is usable.
1414
1941
  */
1415
- async getCreatedFromForkTarget(repoPath, branch, createdFrom, defaultBranch, dependsOn, cancellation) {
1416
- const skipDefault = defaultBranch == null || createdFrom === defaultBranch || createdFrom === `refs/heads/${defaultBranch}` || createdFrom === `refs/remotes/${defaultBranch}`;
1417
- const [created, fallback] = await Promise.all([this.getUpstreamNearestForkTarget(repoPath, branch, createdFrom, dependsOn, cancellation), skipDefault ? void 0 : this.getUpstreamNearestForkTarget(repoPath, branch, defaultBranch, dependsOn, cancellation)]);
1942
+ async getCreatedFromForkTarget(repoPath, branch, createdFrom, defaultBranch, reads, cancellation) {
1943
+ const { name } = createdFrom;
1944
+ const skipDefault = defaultBranch == null || name === defaultBranch || name === `refs/heads/${defaultBranch}` || name === `refs/remotes/${defaultBranch}`;
1945
+ const [created, fallback] = await Promise.all([this.getUpstreamNearestForkTarget(repoPath, branch, name, reads, cancellation), skipDefault ? void 0 : this.getUpstreamNearestForkTarget(repoPath, branch, defaultBranch, reads, cancellation)]);
1418
1946
  if (created == null) return fallback != null ? {
1419
1947
  target: fallback,
1420
1948
  tier: "default"
@@ -1425,7 +1953,7 @@ var BranchesGitSubProvider = class {
1425
1953
  };
1426
1954
  return {
1427
1955
  target: created,
1428
- tier: "created"
1956
+ tier: createdFrom.tier
1429
1957
  };
1430
1958
  }
1431
1959
  /** Counts HEAD's commits past `target` for {@link getAheadOfBase} */
@@ -1451,8 +1979,8 @@ var BranchesGitSubProvider = class {
1451
1979
  };
1452
1980
  }
1453
1981
  /** Counts the default branch's own commits past its upstream (see {@link getSelfUpstreamTarget}) for {@link getAheadOfBase} */
1454
- async measureAheadOfSelfUpstream(repoPath, branch, dependsOn, cancellation) {
1455
- const upstream = await this.getSelfUpstreamTarget(repoPath, branch, dependsOn, cancellation);
1982
+ async measureAheadOfSelfUpstream(repoPath, upstream, reads, cancellation) {
1983
+ const { dependsOn } = reads;
1456
1984
  if (cancellation?.aborted) throw new CancellationError();
1457
1985
  if (upstream == null) return {
1458
1986
  base: void 0,
@@ -1470,7 +1998,7 @@ var BranchesGitSubProvider = class {
1470
1998
  reason: "self",
1471
1999
  dependsOn: [...dependsOn]
1472
2000
  };
1473
- const forkPoint = counts.left === 0 ? upstream.sha : await this.getForkPoint(repoPath, upstream.ref, cancellation);
2001
+ const forkPoint = counts.left === 0 ? upstream.sha : await this.getForkPoint(repoPath, upstream.ref, reads, cancellation);
1474
2002
  if (cancellation?.aborted) throw new CancellationError();
1475
2003
  return {
1476
2004
  base: {
@@ -1511,8 +2039,7 @@ var BranchesGitSubProvider = class {
1511
2039
  if (refs?.size === 0) return counts;
1512
2040
  let patterns;
1513
2041
  if (refs == null) patterns = options?.remotes ? ["refs/heads", "refs/remotes"] : ["refs/heads"];
1514
- else if (sum(refs, (r) => r.length + 1) < 15e3) patterns = [...refs];
1515
- else patterns = ["refs/heads", "refs/remotes"];
2042
+ else patterns = toRefPatterns(refs) ?? ["refs/heads", "refs/remotes"];
1516
2043
  if (!base.includes(")") && await this.git.supports("git:for-each-ref:ahead-behind")) {
1517
2044
  const result = await this.git.run({
1518
2045
  cwd: repoPath,
@@ -1524,12 +2051,9 @@ var BranchesGitSubProvider = class {
1524
2051
  if (!line) continue;
1525
2052
  const [ref, symref, counted] = line.split(" ");
1526
2053
  if (symref || refs != null && !refs.has(ref)) continue;
1527
- const [ahead, behind] = (counted ?? "").split(" ").map((n) => parseInt(n, 10));
1528
- if (isNaN(ahead) || isNaN(behind)) throw new Error(`Unexpected output counting the commits ahead of and behind '${base}': ${line}`);
1529
- counts.set(ref, {
1530
- ahead,
1531
- behind
1532
- });
2054
+ const aheadBehind = parseAheadBehindField(counted);
2055
+ if (aheadBehind == null) throw new Error(`Unexpected output counting the commits ahead of and behind '${base}': ${line}`);
2056
+ counts.set(ref, aheadBehind);
1533
2057
  }
1534
2058
  return counts;
1535
2059
  }
@@ -1664,42 +2188,28 @@ var BranchesGitSubProvider = class {
1664
2188
  return [];
1665
2189
  }
1666
2190
  }
1667
- /** Whether `ref` names `branch` itself (see {@link isSelfRef}) and sits at HEAD's commit; tips are read only after the name matched */
1668
- async isSelfAtHead(repoPath, ref, branch, cancellation) {
1669
- if (!await this.isSelfRef(repoPath, ref, branch, cancellation)) return false;
1670
- const [refSha, headSha] = await Promise.all([this.provider.refs.validateReference(repoPath, ref, void 0, cancellation), this.provider.refs.validateReference(repoPath, "HEAD", void 0, cancellation)]);
1671
- return refSha != null && refSha === headSha;
1672
- }
1673
2191
  /**
1674
- * The bases recorded against a branch, most trusted first: the user's merge target, the detected one, then the
1675
- * base a launch recorded. Only the stored value of the latter — `getBaseBranchName` would fall back to a reflog
1676
- * guess, which on a long-lived checkout names whatever branch was last checked out from. A failed read is absent.
2192
+ * Whether `ref` names `selfName` itself (see {@link isSelfRef}) and sits at `reads.from`'s commit; tips are read
2193
+ * only after the name matched. HEAD's commit is asked of git, a branch's read from the cached ref list
1677
2194
  */
1678
- async getRecordedBaseCandidates(repoPath, branch, skipUserTarget) {
1679
- const tolerate = async (read) => {
1680
- try {
1681
- return (await read())?.trim() || void 0;
1682
- } catch (ex) {
1683
- if (isCancellationError(ex)) throw ex;
1684
- return;
1685
- }
1686
- };
1687
- const reads = await Promise.all([
1688
- skipUserTarget ? void 0 : tolerate(() => this.getStoredUserMergeTargetBranchName(repoPath, branch)),
1689
- tolerate(() => this.getStoredDetectedMergeTargetBranchName(repoPath, branch)),
1690
- tolerate(() => this.provider.config.getGkConfig(repoPath, `branch.${branch}.gk-merge-base`))
1691
- ]);
1692
- const tiers = [
1693
- "user",
1694
- "detected",
1695
- "recorded"
1696
- ];
1697
- const candidates = [];
1698
- for (const [i, ref] of reads.entries()) if (ref && !candidates.some((c) => c.ref === ref)) candidates.push({
1699
- ref,
1700
- tier: tiers[i]
1701
- });
1702
- return candidates;
2195
+ async isSelfAtTip(repoPath, ref, selfName, reads, cancellation) {
2196
+ if (!await this.isSelfRef(repoPath, ref, selfName, cancellation)) return false;
2197
+ if (reads.from === "HEAD") {
2198
+ const [refSha, headSha] = await Promise.all([this.provider.refs.validateReference(repoPath, ref, void 0, cancellation), this.provider.refs.validateReference(repoPath, "HEAD", void 0, cancellation)]);
2199
+ return refSha != null && refSha === headSha;
2200
+ }
2201
+ const records = await this.provider.refs.getRefs(repoPath, cancellation);
2202
+ const [refSha, fromSha] = await Promise.all([this.getRefTip(repoPath, records, ref, cancellation), this.getRefTip(repoPath, records, reads.from, cancellation)]);
2203
+ return refSha != null && refSha === fromSha;
2204
+ }
2205
+ /** The remotes a default branch is read from, in order: `upstream`, `origin`, then the first remote */
2206
+ async getPreferredRemotes(repoPath, cancellation) {
2207
+ const remotes = await this.getRemoteNames(repoPath, cancellation);
2208
+ return [...new Set([
2209
+ remotes.find((r) => r === "upstream"),
2210
+ remotes.find((r) => r === "origin"),
2211
+ remotes.at(0)
2212
+ ].filter((r) => r != null))];
1703
2213
  }
1704
2214
  /**
1705
2215
  * The repo's default branch from local refs only, never the network: the `HEAD` symref of the `upstream` remote,
@@ -1713,12 +2223,7 @@ var BranchesGitSubProvider = class {
1713
2223
  records ??= await this.provider.refs.getRefs(repoPath, cancellation);
1714
2224
  return await this.getRefTip(repoPath, records, ref, cancellation) != null;
1715
2225
  };
1716
- const remotes = await this.getRemoteNames(repoPath, cancellation);
1717
- const preferred = new Set([
1718
- remotes.find((r) => r === "upstream"),
1719
- remotes.find((r) => r === "origin"),
1720
- remotes.at(0)
1721
- ].filter((r) => r != null));
2226
+ const preferred = await this.getPreferredRemotes(repoPath, cancellation);
1722
2227
  for (const remote of preferred) {
1723
2228
  dependsOn.add(`refs/remotes/${remote}/HEAD`);
1724
2229
  const symref = await this.getDefaultBranchName(repoPath, remote, { local: true }, cancellation);
@@ -1757,7 +2262,7 @@ var BranchesGitSubProvider = class {
1757
2262
  * remote and ref lists; `undefined` when there is no `upstream` remote, it lacks the branch, or the base is that
1758
2263
  * ref itself.
1759
2264
  */
1760
- async getUpstreamContender(repoPath, base, dependsOn, cancellation) {
2265
+ async getUpstreamContender(repoPath, base, reads, cancellation) {
1761
2266
  if (!base) return void 0;
1762
2267
  const remotes = await this.getRemoteNames(repoPath, cancellation);
1763
2268
  if (!remotes.includes("upstream")) return void 0;
@@ -1774,33 +2279,37 @@ var BranchesGitSubProvider = class {
1774
2279
  }
1775
2280
  const contender = `upstream/${name}`;
1776
2281
  const remoteRef = `refs/remotes/${contender}`;
1777
- if (contender === base || remoteRef === base || findRefTip(records, remoteRef) == null) return void 0;
1778
- dependsOn.add(remoteRef);
2282
+ if (contender === base || remoteRef === base) return void 0;
2283
+ if (findRefTip(records, remoteRef) == null) {
2284
+ reads.tipRefs.add(remoteRef);
2285
+ return;
2286
+ }
2287
+ reads.dependsOn.add(remoteRef);
1779
2288
  return contender;
1780
2289
  }
1781
2290
  /** {@link getNearestForkTarget} with the `upstream` remote's copy of `candidate` as its contender */
1782
- async getUpstreamNearestForkTarget(repoPath, branch, candidate, dependsOn, cancellation) {
1783
- return this.getNearestForkTarget(repoPath, branch, candidate, dependsOn, cancellation, { contender: await this.getUpstreamContender(repoPath, candidate, dependsOn, cancellation) });
2291
+ async getUpstreamNearestForkTarget(repoPath, branch, candidate, reads, cancellation) {
2292
+ return this.getNearestForkTarget(repoPath, branch, candidate, reads, cancellation, { contender: await this.getUpstreamContender(repoPath, candidate, reads, cancellation) });
1784
2293
  }
1785
2294
  /**
1786
- * A candidate base as the ref HEAD is measured against: the candidate or its tracking ref, whichever forked
1787
- * nearer HEAD, so a stale local base defers to its upstream while one carrying unpushed work wins over it. A
2295
+ * A candidate base as the ref `reads.from` is measured against: the candidate or its tracking ref, whichever forked
2296
+ * nearer, so a stale local base defers to its upstream while one carrying unpushed work wins over it. A
1788
2297
  * `contender` other than the tracking ref is taken only when it forked strictly nearer than both, so a tie keeps
1789
- * the candidate's own pick. A candidate none forks from is used as-is if it names a commit. `undefined` when the
1790
- * candidate is unusable.
2298
+ * the candidate's own pick. A candidate none forks from is used as-is unless git confirms it names no ref: one the
2299
+ * cached ref list names is kept, and so is one whose check fails. `undefined` when the candidate is unusable.
1791
2300
  */
1792
- async getNearestForkTarget(repoPath, branch, candidate, dependsOn, cancellation, options) {
2301
+ async getNearestForkTarget(repoPath, branch, candidate, reads, cancellation, options) {
1793
2302
  if (!candidate || candidate.startsWith("-")) return void 0;
1794
2303
  if (candidate === branch || candidate === `refs/heads/${branch}`) return void 0;
1795
- addRefDependencies(dependsOn, candidate);
2304
+ addRefDependencies(reads.dependsOn, candidate);
1796
2305
  const upstreamName = this.getUpstreamName(repoPath, candidate, void 0, cancellation);
1797
2306
  const getUpstreamFork = async () => {
1798
2307
  const upstream = await upstreamName;
1799
2308
  if (!upstream || upstream.startsWith("-")) return void 0;
1800
- addRefDependencies(dependsOn, upstream);
2309
+ addRefDependencies(reads.dependsOn, upstream);
1801
2310
  return {
1802
2311
  ref: upstream,
1803
- mergeBase: await this.getForkPoint(repoPath, upstream, cancellation)
2312
+ mergeBase: await this.getForkPoint(repoPath, upstream, reads, cancellation)
1804
2313
  };
1805
2314
  };
1806
2315
  const contender = options?.contender;
@@ -1813,10 +2322,15 @@ var BranchesGitSubProvider = class {
1813
2322
  const upstream = await upstreamName;
1814
2323
  if (upstream && tip === findRefTip(records, upstream)) return void 0;
1815
2324
  }
1816
- return this.getForkPoint(repoPath, contender, cancellation);
2325
+ return this.getForkPoint(repoPath, contender, reads, cancellation);
2326
+ };
2327
+ const candidateReads = {
2328
+ from: reads.from,
2329
+ priority: reads.priority,
2330
+ pinnable: true
1817
2331
  };
1818
2332
  const [candidateMergeBase, upstreamFork, contenderMergeBase] = await Promise.all([
1819
- this.getForkPoint(repoPath, candidate, cancellation),
2333
+ this.getForkPoint(repoPath, candidate, candidateReads, cancellation),
1820
2334
  getUpstreamFork(),
1821
2335
  getContenderMergeBase()
1822
2336
  ]);
@@ -1835,18 +2349,33 @@ var BranchesGitSubProvider = class {
1835
2349
  ref: contender,
1836
2350
  mergeBase: contenderMergeBase
1837
2351
  };
1838
- if (nearest != null) return {
1839
- ref: nearest.ref,
1840
- forkPoint: nearest.mergeBase
1841
- };
1842
- return await this.provider.refs.validateReference(repoPath, candidate, void 0, cancellation) ? {
2352
+ if (nearest != null) {
2353
+ if (!candidateReads.pinnable) reads.pinnable = false;
2354
+ return {
2355
+ ref: nearest.ref,
2356
+ forkPoint: nearest.mergeBase
2357
+ };
2358
+ }
2359
+ const records = await this.provider.refs.getRefs(repoPath, cancellation);
2360
+ if (records.length && findRefTip(records, candidate) != null) {
2361
+ if (!candidateReads.pinnable) reads.pinnable = false;
2362
+ return {
2363
+ ref: candidate,
2364
+ forkPoint: void 0
2365
+ };
2366
+ }
2367
+ const { missing } = await this.provider.refs.getSymbolicReferenceNameOrMissing(repoPath, candidate, reads.priority != null ? { priority: reads.priority } : void 0, cancellation);
2368
+ if (missing) return void 0;
2369
+ reads.pinnable = false;
2370
+ return {
1843
2371
  ref: candidate,
1844
2372
  forkPoint: void 0
1845
- } : void 0;
2373
+ };
1846
2374
  }
1847
2375
  /**
1848
2376
  * `<name>@{u}`, without asking git when the cached ref list shows `name` can't have one: a failed lookup isn't
1849
- * cached, so asking it of a remote-tracking ref (`origin/main`, the usual default) re-spawns on every read.
2377
+ * cached, so asking it of a remote-tracking ref (`origin/main`, the usual default) re-spawns on every read. The
2378
+ * list's own upstream isn't used: a config-only change (`git branch -u`) doesn't refresh it.
1850
2379
  */
1851
2380
  async getUpstreamName(repoPath, name, options, cancellation) {
1852
2381
  if (!mayHaveUpstream(await this.provider.refs.getRefs(repoPath, cancellation), name)) return void 0;
@@ -1857,7 +2386,8 @@ var BranchesGitSubProvider = class {
1857
2386
  * when an `upstream` remote has it, else `<branch>@{u}`. Read from the cached ref and branch lists; only a
1858
2387
  * `<branch>` that isn't checked out spawns, for its `@{u}`. `undefined` when there is neither, or its ref is gone.
1859
2388
  */
1860
- async getSelfUpstreamTarget(repoPath, branch, dependsOn, cancellation) {
2389
+ async getSelfUpstreamTarget(repoPath, branch, reads, cancellation) {
2390
+ const { dependsOn } = reads;
1861
2391
  const records = await this.provider.refs.getRefs(repoPath, cancellation);
1862
2392
  let upstream;
1863
2393
  if ((await this.getRemoteNames(repoPath, cancellation)).includes("upstream")) {
@@ -1893,9 +2423,36 @@ var BranchesGitSubProvider = class {
1893
2423
  if (records.length) return findRefTip(records, name);
1894
2424
  return this.provider.refs.validateReference(repoPath, name, void 0, cancellation);
1895
2425
  }
1896
- /** The merge base of HEAD and `ref`, preferring git's fork-point reading of `ref`'s reflog when it has one */
1897
- async getForkPoint(repoPath, ref, cancellation) {
1898
- return await this.provider.refs.getMergeBase(repoPath, ref, "HEAD", { forkPoint: true }, cancellation) ?? this.provider.refs.getMergeBase(repoPath, "HEAD", ref, void 0, cancellation);
2426
+ /** Where `reads.from` forked from `ref`: git's fork-point reading of `ref`'s reflog when it has one, else their merge base */
2427
+ async getForkPoint(repoPath, ref, reads, cancellation) {
2428
+ return await this.readMergeBase(repoPath, ref, reads.from, true, reads, cancellation) ?? this.readMergeBase(repoPath, reads.from, ref, false, reads, cancellation);
2429
+ }
2430
+ /**
2431
+ * `getMergeBase`, sharing its cached and in-flight reads, but telling git's exit 1 (no common ancestor, or no fork
2432
+ * point) from a read that failed. Both answer `undefined`; a failed one also clears `reads.pinnable`.
2433
+ */
2434
+ async readMergeBase(repoPath, ref1, ref2, forkPoint, reads, cancellation) {
2435
+ try {
2436
+ const result = await this.git.run({
2437
+ cwd: repoPath,
2438
+ cancellation,
2439
+ errors: "throw",
2440
+ expectedExitCodes: [1],
2441
+ caching: {
2442
+ cache: this.cache.gitResults,
2443
+ options: { accessTTL: 3e5 }
2444
+ },
2445
+ ...reads.priority != null ? { priority: reads.priority } : void 0
2446
+ }, "merge-base", forkPoint ? "--fork-point" : void 0, ref1, ref2);
2447
+ if (result.completion.status === "exited") {
2448
+ if (result.exitCode === 1) return void 0;
2449
+ const sha = result.stdout.split("\n")[0].trim();
2450
+ if (sha) return sha;
2451
+ }
2452
+ } catch (ex) {
2453
+ if (isCancellationError(ex)) throw ex;
2454
+ }
2455
+ reads.pinnable = false;
1899
2456
  }
1900
2457
  /**
1901
2458
  * The stored merge target — the user's pick, else the one detected from its PR — skipping any whose branch git
@@ -1942,6 +2499,7 @@ var BranchesGitSubProvider = class {
1942
2499
  forgetBranchCreatedFrom(repoPath, name) {
1943
2500
  const key = this.getBranchActivityKey(repoPath, name);
1944
2501
  this._branchCreatedFrom.delete(key);
2502
+ this._branchCheckedOutFrom.delete(key);
1945
2503
  this._branchCreatedFromGenerations.set(key, (this._branchCreatedFromGenerations.get(key) ?? 0) + 1);
1946
2504
  }
1947
2505
  /** Forgets this process's throttle marks for `name`, so its next activity re-reads the stored stamps */
@@ -2086,32 +2644,66 @@ var BranchesGitSubProvider = class {
2086
2644
  this.context.hooks?.repository?.onChanged?.(repoPath, ["starred"]);
2087
2645
  }
2088
2646
  };
2089
- __decorate([debug()], BranchesGitSubProvider.prototype, "getBranch", null);
2090
- __decorate([debug({ args: (repoPath) => ({ repoPath }) })], BranchesGitSubProvider.prototype, "getBranches", null);
2091
- __decorate([debug()], BranchesGitSubProvider.prototype, "getBranchContributionsOverview", null);
2092
- __decorate([debug()], BranchesGitSubProvider.prototype, "getBranchesWithCommits", null);
2093
- __decorate([debug()], BranchesGitSubProvider.prototype, "getCurrentBranchReference", null);
2094
- __decorate([debug({ exit: true })], BranchesGitSubProvider.prototype, "getDefaultBranchName", null);
2647
+ __decorate([debug({ quiet: true })], BranchesGitSubProvider.prototype, "getBranch", null);
2648
+ __decorate([debug({
2649
+ quiet: true,
2650
+ args: (repoPath) => ({ repoPath })
2651
+ })], BranchesGitSubProvider.prototype, "getBranches", null);
2652
+ __decorate([debug({ quiet: true })], BranchesGitSubProvider.prototype, "getBranchContributionsOverview", null);
2653
+ __decorate([debug({ quiet: true })], BranchesGitSubProvider.prototype, "getBranchesWithCommits", null);
2654
+ __decorate([debug({ quiet: true })], BranchesGitSubProvider.prototype, "getCurrentBranchReference", null);
2655
+ __decorate([debug({
2656
+ quiet: true,
2657
+ exit: true
2658
+ })], BranchesGitSubProvider.prototype, "getDefaultBranchName", null);
2095
2659
  __decorate([debug()], BranchesGitSubProvider.prototype, "setRemoteHeadIfMissing", null);
2096
2660
  __decorate([debug()], BranchesGitSubProvider.prototype, "createBranch", null);
2097
2661
  __decorate([debug()], BranchesGitSubProvider.prototype, "deleteLocalBranch", null);
2098
2662
  __decorate([debug()], BranchesGitSubProvider.prototype, "deleteRemoteBranch", null);
2099
- __decorate([debug()], BranchesGitSubProvider.prototype, "getBranchMergedStatus", null);
2100
- __decorate([debug()], BranchesGitSubProvider.prototype, "getLocalBranchByUpstream", null);
2101
- __decorate([trace({ exit: true })], BranchesGitSubProvider.prototype, "getPushTarget", null);
2102
- __decorate([debug()], BranchesGitSubProvider.prototype, "getPotentialApplyConflicts", null);
2103
- __decorate([debug()], BranchesGitSubProvider.prototype, "getPotentialMergeConflicts", null);
2104
- __decorate([debug()], BranchesGitSubProvider.prototype, "getPotentialStashReapplyConflicts", null);
2105
- __decorate([debug({ exit: true })], BranchesGitSubProvider.prototype, "getBaseBranchName", null);
2106
- __decorate([trace()], BranchesGitSubProvider.prototype, "getAheadOfBaseConfigKey", null);
2107
- __decorate([trace()], BranchesGitSubProvider.prototype, "getAheadOfBaseConfigEntries", null);
2108
- __decorate([debug({ exit: true })], BranchesGitSubProvider.prototype, "getAheadOfBase", null);
2109
- __decorate([debug({ exit: true })], BranchesGitSubProvider.prototype, "getAheadOfBaseAfterBaseMove", null);
2110
- __decorate([debug({ exit: true })], BranchesGitSubProvider.prototype, "getAheadBehindCounts", null);
2111
- __decorate([debug({ exit: true })], BranchesGitSubProvider.prototype, "getMergedBranches", null);
2112
- __decorate([debug({ exit: true })], BranchesGitSubProvider.prototype, "getStoredMergeTargetBranchName", null);
2113
- __decorate([trace({ exit: true })], BranchesGitSubProvider.prototype, "getStoredDetectedMergeTargetBranchName", null);
2114
- __decorate([trace()], BranchesGitSubProvider.prototype, "getStoredUserMergeTargetBranchName", null);
2663
+ __decorate([debug({ quiet: true })], BranchesGitSubProvider.prototype, "getBranchMergedStatus", null);
2664
+ __decorate([debug({ quiet: true })], BranchesGitSubProvider.prototype, "getLocalBranchByUpstream", null);
2665
+ __decorate([trace({
2666
+ quiet: true,
2667
+ exit: true
2668
+ })], BranchesGitSubProvider.prototype, "getPushTarget", null);
2669
+ __decorate([debug({ quiet: true })], BranchesGitSubProvider.prototype, "getPotentialApplyConflicts", null);
2670
+ __decorate([debug({ quiet: true })], BranchesGitSubProvider.prototype, "getPotentialMergeConflicts", null);
2671
+ __decorate([debug({ quiet: true })], BranchesGitSubProvider.prototype, "getPotentialStashReapplyConflicts", null);
2672
+ __decorate([debug({
2673
+ quiet: true,
2674
+ exit: true
2675
+ })], BranchesGitSubProvider.prototype, "getBaseBranchName", null);
2676
+ __decorate([trace({ quiet: true })], BranchesGitSubProvider.prototype, "getAheadOfBaseConfigKey", null);
2677
+ __decorate([trace({ quiet: true })], BranchesGitSubProvider.prototype, "getAheadOfBaseConfigEntries", null);
2678
+ __decorate([debug({
2679
+ quiet: true,
2680
+ exit: true
2681
+ })], BranchesGitSubProvider.prototype, "getAheadOfBase", null);
2682
+ __decorate([debug({
2683
+ quiet: true,
2684
+ exit: true
2685
+ })], BranchesGitSubProvider.prototype, "getAheadOfBaseAfterBaseMove", null);
2686
+ __decorate([debug({
2687
+ quiet: true,
2688
+ exit: true
2689
+ })], BranchesGitSubProvider.prototype, "getMergeTarget", null);
2690
+ __decorate([debug({
2691
+ quiet: true,
2692
+ exit: true
2693
+ })], BranchesGitSubProvider.prototype, "getAheadBehindCounts", null);
2694
+ __decorate([debug({
2695
+ quiet: true,
2696
+ exit: true
2697
+ })], BranchesGitSubProvider.prototype, "getMergedBranches", null);
2698
+ __decorate([debug({
2699
+ quiet: true,
2700
+ exit: true
2701
+ })], BranchesGitSubProvider.prototype, "getStoredMergeTargetBranchName", null);
2702
+ __decorate([trace({
2703
+ quiet: true,
2704
+ exit: true
2705
+ })], BranchesGitSubProvider.prototype, "getStoredDetectedMergeTargetBranchName", null);
2706
+ __decorate([trace({ quiet: true })], BranchesGitSubProvider.prototype, "getStoredUserMergeTargetBranchName", null);
2115
2707
  __decorate([trace()], BranchesGitSubProvider.prototype, "onCurrentBranchAccessed", null);
2116
2708
  __decorate([trace()], BranchesGitSubProvider.prototype, "onCurrentBranchModified", null);
2117
2709
  __decorate([trace()], BranchesGitSubProvider.prototype, "onCurrentBranchAgentActivity", null);
@@ -2177,15 +2769,46 @@ function getRefRecordsByName(records) {
2177
2769
  }
2178
2770
  return byName;
2179
2771
  }
2772
+ /** Each ref list's {@link BaseMoveReadInputs}: every worktree a base move reached derives them from the same cached list */
2773
+ const baseMoveReadInputs = /* @__PURE__ */ new WeakMap();
2774
+ function getBaseMoveReadInputs(records) {
2775
+ let inputs = baseMoveReadInputs.get(records);
2776
+ if (inputs != null) return inputs;
2777
+ let checkedOut = /* @__PURE__ */ new Set();
2778
+ const checkedOutTips = /* @__PURE__ */ new Set();
2779
+ const replaced = [];
2780
+ for (const record of records) {
2781
+ if (record.name.startsWith("refs/replace/")) {
2782
+ replaced.push(`${record.name}\0${record.objectname}`);
2783
+ continue;
2784
+ }
2785
+ if (checkedOut == null || !record.name.startsWith("refs/heads/")) continue;
2786
+ if (record.worktreePath == null) {
2787
+ checkedOut = void 0;
2788
+ checkedOutTips.clear();
2789
+ continue;
2790
+ }
2791
+ if (record.worktreePath) {
2792
+ checkedOut.add(record.name);
2793
+ checkedOutTips.add(record.objectname);
2794
+ }
2795
+ }
2796
+ inputs = {
2797
+ checkedOut,
2798
+ checkedOutTips,
2799
+ replaced: replaced.sort().join("\n")
2800
+ };
2801
+ baseMoveReadInputs.set(records, inputs);
2802
+ return inputs;
2803
+ }
2180
2804
  /** The record for the full ref name `fullName` in `records`, the cached ref list; no short-name resolution */
2181
2805
  function findRefRecord(records, fullName) {
2182
2806
  return getRefRecordsByName(records).get(fullName);
2183
2807
  }
2184
2808
  function toPushTargetRef(records, ref) {
2185
- const record = findRefRecord(records, ref);
2186
2809
  return {
2187
2810
  ref,
2188
- sha: record != null ? record.peeledObjectname || record.objectname : void 0
2811
+ sha: getListedTip(records, ref)
2189
2812
  };
2190
2813
  }
2191
2814
  /**
@@ -2252,6 +2875,48 @@ function addRefDependencies(dependsOn, name) {
2252
2875
  dependsOn.add(`refs/heads/${name}`);
2253
2876
  dependsOn.add(`refs/remotes/${name}`);
2254
2877
  }
2878
+ function createBaseReads(from, dependsOn, priority) {
2879
+ return {
2880
+ from,
2881
+ priority,
2882
+ dependsOn: new Set(dependsOn),
2883
+ tipRefs: /* @__PURE__ */ new Set(),
2884
+ pinnable: true,
2885
+ evictOnCheckout: false
2886
+ };
2887
+ }
2888
+ /**
2889
+ * Keys a merge target by `branch` and every option that shapes the answer. `keepSelfAtTip` is applied to the cached
2890
+ * answer instead, and `priority` doesn't change it
2891
+ */
2892
+ function getMergeTargetCacheKey(branch, options) {
2893
+ return `${branch}\0${[
2894
+ options?.skipUserTarget ? "u" : "",
2895
+ options?.getAssociatedPullRequest != null ? "p" : "",
2896
+ options?.inferBase ? "i" : "",
2897
+ options?.defaultBranch === "remote" ? "r" : "",
2898
+ options?.getFallbackDefaultBranch != null ? "f" : ""
2899
+ ].join("")}\0${options?.preferredTarget?.trim() ?? ""}`;
2900
+ }
2901
+ /** `read`'s answer, with a failure read as none; cancellation still rejects */
2902
+ async function tolerateRead(read) {
2903
+ if (read == null) return void 0;
2904
+ try {
2905
+ return await read();
2906
+ } catch (ex) {
2907
+ if (isCancellationError(ex)) throw ex;
2908
+ return;
2909
+ }
2910
+ }
2911
+ /** `value` as a literal in `getReflogEntries`' `grep`, a basic regular expression */
2912
+ function toReflogGrepLiteral(value) {
2913
+ return value.replace(/[\\.*[\]^$]/g, "\\$&");
2914
+ }
2915
+ /** The tip of the full ref name `fullName` in `records`, the cached ref list; `undefined` when it isn't listed */
2916
+ function getListedTip(records, fullName) {
2917
+ const record = findRefRecord(records, fullName);
2918
+ return record != null ? record.peeledObjectname || record.objectname : void 0;
2919
+ }
2255
2920
  //#endregion
2256
2921
  export { BranchesGitSubProvider };
2257
2922