@gitkraken/core-gitlens 0.16.3 → 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 (58) hide show
  1. package/CHANGELOG.md +25 -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-cli/providers/branches.d.ts +135 -39
  12. package/dist/git-cli/providers/branches.js +846 -264
  13. package/dist/git-cli/providers/branches.js.map +1 -1
  14. package/dist/git-cli/providers/commits.js +37 -20
  15. package/dist/git-cli/providers/commits.js.map +1 -1
  16. package/dist/git-cli/providers/config.js +16 -10
  17. package/dist/git-cli/providers/config.js.map +1 -1
  18. package/dist/git-cli/providers/contributors.js +3 -3
  19. package/dist/git-cli/providers/contributors.js.map +1 -1
  20. package/dist/git-cli/providers/diff.js +15 -12
  21. package/dist/git-cli/providers/diff.js.map +1 -1
  22. package/dist/git-cli/providers/graph.js +17 -14
  23. package/dist/git-cli/providers/graph.js.map +1 -1
  24. package/dist/git-cli/providers/maintenance.js +3 -3
  25. package/dist/git-cli/providers/maintenance.js.map +1 -1
  26. package/dist/git-cli/providers/patch.js +4 -1
  27. package/dist/git-cli/providers/patch.js.map +1 -1
  28. package/dist/git-cli/providers/pausedOperations.js +2 -2
  29. package/dist/git-cli/providers/pausedOperations.js.map +1 -1
  30. package/dist/git-cli/providers/refs.js +16 -13
  31. package/dist/git-cli/providers/refs.js.map +1 -1
  32. package/dist/git-cli/providers/remotes.js +4 -1
  33. package/dist/git-cli/providers/remotes.js.map +1 -1
  34. package/dist/git-cli/providers/revision.js +14 -11
  35. package/dist/git-cli/providers/revision.js.map +1 -1
  36. package/dist/git-cli/providers/stash.js +2 -2
  37. package/dist/git-cli/providers/stash.js.map +1 -1
  38. package/dist/git-cli/providers/status.js +7 -7
  39. package/dist/git-cli/providers/status.js.map +1 -1
  40. package/dist/git-cli/providers/tags.js +6 -3
  41. package/dist/git-cli/providers/tags.js.map +1 -1
  42. package/dist/git-cli/providers/worktrees.js +3 -3
  43. package/dist/git-cli/providers/worktrees.js.map +1 -1
  44. package/dist/plus/git-github/providers/github/branches.d.ts +9 -1
  45. package/dist/plus/git-github/providers/github/branches.js +54 -8
  46. package/dist/plus/git-github/providers/github/branches.js.map +1 -1
  47. package/dist/plus/integrations/providers/issueSorts.d.ts +1 -0
  48. package/dist/plus/integrations/reads/drains.d.ts +1 -0
  49. package/dist/utils/decorators/log.d.ts +3 -1
  50. package/dist/utils/decorators/log.js +61 -45
  51. package/dist/utils/decorators/log.js.map +1 -1
  52. package/dist/utils/logger.d.ts +15 -1
  53. package/dist/utils/logger.js +30 -1
  54. package/dist/utils/logger.js.map +1 -1
  55. package/dist/utils/logger.scoped.d.ts +12 -1
  56. package/dist/utils/logger.scoped.js +38 -4
  57. package/dist/utils/logger.scoped.js.map +1 -1
  58. package/package.json +1 -1
@@ -69,26 +69,13 @@ function parseAheadBehindField(field) {
69
69
  function toRefPatterns(refs) {
70
70
  return sum(refs, (r) => r.length + 1) < 15e3 ? [...refs] : void 0;
71
71
  }
72
- /**
73
- * Joins the read `key` names against `records` (a cached ref list) in `memo`, or starts it with `read`. An answer is
74
- * kept as long as that list is: a ref write replaces the list, along with the refs and reflog the answer came from.
75
- * `undefined`, when git couldn't say, isn't kept
76
- */
77
- async function shareBaseMoveRead(memo, records, key, read) {
78
- let reads = memo.get(records);
79
- if (reads == null) {
80
- reads = /* @__PURE__ */ new Map();
81
- memo.set(records, reads);
82
- }
83
- let pending = reads.get(key);
84
- if (pending == null) {
85
- pending = read();
86
- reads.set(key, pending);
87
- }
88
- const answer = await pending;
89
- if (answer === void 0 && reads.get(key) === pending) reads.delete(key);
90
- return answer;
91
- }
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
+ ];
92
79
  var BranchesGitSubProvider = class {
93
80
  context;
94
81
  git;
@@ -97,20 +84,22 @@ var BranchesGitSubProvider = class {
97
84
  /** Ahead/behind counts by `<baseSha>...<tipSha>` range, from the per-tip fallback; a pair of commits always counts the same */
98
85
  aheadBehindBySha = new LruMap(4096);
99
86
  /**
100
- * For a base that moved only forward, what its reflog reaches (else `false`), by `<ref>\0<oldSha>\0<newSha>`, for
101
- * each cached ref list it was read against. A fetch moves one base under every worktree at once, so they share the
102
- * 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
103
94
  */
104
- baseMovedForward = /* @__PURE__ */ new WeakMap();
95
+ baseMovedForward = new LruMap(64);
105
96
  /**
106
97
  * For a base that moved, the count of the commits its new tip lacks for each local branch a worktree has checked
107
- * out, by tip sha, keyed by that new tip alone for each cached ref list it was read against (which also fixes the
108
- * branches counted). The counts don't depend on the ref or where it moved from, so each worktree the move reached
109
- * finds its HEAD's count in the one read, even one whose previous answer was taken at another old tip. Only the
110
- * checked-out branches are counted: the shared walk runs until every counted branch meets the new tip, so an
111
- * unrelated old or orphan branch no worktree uses would make it deeper than any worktree's own count
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
112
101
  */
113
- aheadOfMovedBase = /* @__PURE__ */ new WeakMap();
102
+ aheadOfMovedBase = new LruMap(64);
114
103
  /**
115
104
  * When each `commonPath\0branch\0key` was last stamped by this process. Bounded: a dropped entry only costs a
116
105
  * read of the stored stamp, which still holds off a write under 5 minutes old
@@ -124,6 +113,11 @@ var BranchesGitSubProvider = class {
124
113
  * it once
125
114
  */
126
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);
127
121
  /** Bumped each time a key's created-from is forgotten, so a reflog read in flight across that doesn't store it */
128
122
  _branchCreatedFromGenerations = new LruMap(1024);
129
123
  /**
@@ -361,28 +355,17 @@ var BranchesGitSubProvider = class {
361
355
  };
362
356
  };
363
357
  try {
364
- let mergeTarget;
365
- mergeTarget = await this.getStoredMergeTargetBranchName(repoPath, ref, priorityOpts, cancellation);
366
- if (mergeTarget == null && options?.associatedPullRequest != null) {
367
- const pr = await options.associatedPullRequest;
368
- if (pr?.refs?.base != null) {
369
- const branch = await this.getBranch(repoPath, ref, void 0, cancellation);
370
- if (branch == null) return void 0;
371
- const { remote: remoteName } = await resolveBranchRemote(this.provider, repoPath, branch, cancellation);
372
- if (remoteName != null) {
373
- const prTarget = `${remoteName}/${pr.refs.base.branch}`;
374
- const { missing } = await this.provider.refs.getSymbolicReferenceNameOrMissing(repoPath, prTarget, priorityOpts, cancellation);
375
- if (!missing) {
376
- mergeTarget = prTarget;
377
- if (pr.stack == null) this.storeMergeTargetBranchName(repoPath, ref, prTarget, { skipInvalidation: ["branchOverviews"] });
378
- }
379
- }
380
- }
381
- }
382
- mergeTarget ??= await this.getBaseBranchName(repoPath, ref, priorityOpts, cancellation);
383
- mergeTarget ??= await this.getDefaultBranchName(repoPath, void 0, priorityOpts, cancellation);
384
- if (mergeTarget == null) return void 0;
385
- 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;
386
369
  return await this.cache.getBranchOverview(repoPath, `${ref}|${resolvedTarget}`, (_commonPath, _cacheable, signal) => fetchBody(resolvedTarget, signal ?? cancellation), { cancellation });
387
370
  } catch (ex) {
388
371
  scope?.error(ex);
@@ -1067,75 +1050,63 @@ var BranchesGitSubProvider = class {
1067
1050
  update = mergeBase != null;
1068
1051
  }
1069
1052
  if (mergeBase != null) {
1070
- const branch = await this.provider.refs.getSymbolicReferenceName(commonPath, mergeBase, priorityOpts);
1071
- if (branch != null) {
1072
- if (update) this.storeBaseBranchName(commonPath, ref, branch, { skipInvalidation: ["branchOverviews", "baseBranchName"] });
1073
- 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;
1074
1061
  }
1075
1062
  }
1076
1063
  } catch {}
1077
- const reflog = await this.getBaseBranchFromReflog(commonPath, ref, {
1078
- upstream: true,
1079
- priority
1080
- }, signal);
1081
- if (reflog.branch != null) {
1082
- this.storeBaseBranchName(commonPath, ref, reflog.branch, { skipInvalidation: ["branchOverviews", "baseBranchName"] });
1083
- return reflog.branch;
1084
- }
1064
+ const reflog = await this.getBaseBranchFromReflog(repoPath, commonPath, ref, priority, signal);
1065
+ if (reflog.branch != null) return reflog.branch;
1085
1066
  if (reflog.failed) cacheable.invalidate();
1086
1067
  }, cancellation);
1087
1068
  }
1088
1069
  /**
1089
- * `failed` separates "the reflog ran and had no answer" from "the read never happened" — the caller
1090
- * 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.
1091
1073
  */
1092
- async getBaseBranchFromReflog(repoPath, ref, options, cancellation) {
1093
- const priority = options?.priority;
1074
+ async getBaseBranchFromReflog(repoPath, commonPath, ref, priority, cancellation) {
1094
1075
  const priorityOpts = priority != null ? { priority } : void 0;
1095
1076
  try {
1096
- const createdFrom = await this.getReflogCreatedFrom(repoPath, ref, priority, cancellation);
1077
+ const createdFrom = await this.getReflogCreatedFrom(commonPath, ref, priority, cancellation);
1097
1078
  if (createdFrom == null) return {
1098
1079
  branch: void 0,
1099
1080
  failed: false
1100
1081
  };
1101
1082
  if (createdFrom !== "HEAD") {
1102
- if (options?.upstream) {
1103
- const upstream = await this.getUpstreamName(repoPath, createdFrom, priorityOpts, cancellation);
1104
- if (upstream) return {
1105
- branch: upstream,
1106
- failed: false
1107
- };
1108
- }
1109
- 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);
1110
1089
  if (name) return {
1111
1090
  branch: name,
1112
1091
  failed: false
1113
1092
  };
1114
1093
  }
1115
- const entries = await this.provider.refs.getReflogEntries(repoPath, "HEAD", {
1116
- grep: `checkout: moving from .* to ${ref.replace("refs/heads/", "")}`,
1117
- priority
1118
- }, cancellation);
1119
- 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 {
1120
1097
  branch: void 0,
1121
1098
  failed: false
1122
1099
  };
1123
- const match = entries.at(-1).message.match(/checkout: moving from ([^\s]+)\s/);
1124
- if (match?.length === 2) {
1125
- let name = match[1];
1126
- if (options?.upstream) {
1127
- const upstream = await this.getUpstreamName(repoPath, name, priorityOpts, cancellation);
1128
- if (upstream) return {
1129
- branch: upstream,
1130
- failed: false
1131
- };
1132
- }
1133
- name = await this.provider.refs.getSymbolicReferenceName(repoPath, name, priorityOpts);
1134
- if (name) return {
1135
- branch: name,
1136
- failed: false
1137
- };
1138
- }
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
+ };
1139
1110
  } catch (ex) {
1140
1111
  if (isCancellationError(ex)) throw ex;
1141
1112
  const msg = ex?.toString() ?? "";
@@ -1168,12 +1139,36 @@ var BranchesGitSubProvider = class {
1168
1139
  return entries[0].message.match(/branch: Created from (.*)$/)?.[1];
1169
1140
  }
1170
1141
  /**
1171
- * The branch `branch` was created from, as a base for {@link getAheadOfBaseInOrder}: the name its own reflog
1172
- * gives, when that is a local or remote-tracking branch in the cached ref list and not `branch` itself or its
1173
- * copy on a remote (`git switch <branch>` creates it from `<remote>/<branch>`). Never the checkout history in
1174
- * 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
1175
1144
  */
1176
- async getCreatedFromTarget(repoPath, branch, cancellation) {
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.
1153
+ */
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) {
1177
1172
  const key = this.getBranchActivityKey(repoPath, branch);
1178
1173
  let name;
1179
1174
  if (this._branchCreatedFrom.has(key)) name = this._branchCreatedFrom.get(key);
@@ -1183,20 +1178,73 @@ var BranchesGitSubProvider = class {
1183
1178
  name = await this.getReflogCreatedFrom(repoPath, `refs/heads/${branch}`, void 0, cancellation);
1184
1179
  } catch (ex) {
1185
1180
  if (isCancellationError(ex)) throw ex;
1186
- return;
1181
+ reads.pinnable = false;
1182
+ return {
1183
+ name: void 0,
1184
+ fromHead: false
1185
+ };
1187
1186
  }
1188
1187
  if (cancellation?.aborted) throw new CancellationError();
1189
1188
  if (this._branchCreatedFromGenerations.get(key) === generation) this._branchCreatedFrom.set(key, name);
1190
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) {
1191
1232
  if (!name || name === "HEAD" || name.startsWith("-")) return void 0;
1192
1233
  const records = await this.provider.refs.getRefs(repoPath, cancellation);
1193
1234
  const fullNames = name.startsWith("refs/heads/") || name.startsWith("refs/remotes/") ? [name] : [`refs/heads/${name}`, `refs/remotes/${name}`];
1194
1235
  let listed = false;
1195
- for (const fullName of fullNames) if (await this.getRefTip(repoPath, records, fullName, cancellation) != null) {
1196
- listed = true;
1197
- 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
+ }
1198
1242
  }
1199
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
+ }
1200
1248
  if (await this.isSelfRef(repoPath, name, branch, cancellation)) return void 0;
1201
1249
  return name;
1202
1250
  }
@@ -1247,14 +1295,30 @@ var BranchesGitSubProvider = class {
1247
1295
  }
1248
1296
  async getAheadOfBase(repoPath, branch, options, cancellation) {
1249
1297
  if (branch != null && isDetachedHead(branch)) branch = void 0;
1250
- const dependsOn = new Set(branch != null ? [`refs/heads/${branch}`] : []);
1251
- const preferredTarget = options?.preferredTarget?.trim() || void 0;
1252
- if (preferredTarget == null) return this.getAheadOfBaseInOrder(repoPath, branch, options?.skipUserTarget, dependsOn, cancellation);
1253
- let target = await this.getNearestForkTarget(repoPath, branch, preferredTarget, dependsOn, cancellation);
1254
- if (target != null && branch != null && await this.isSelfAtHead(repoPath, target.ref, branch, cancellation)) target = void 0;
1255
- if (cancellation?.aborted) throw new CancellationError();
1256
- if (target != null) return this.measureAheadOfBase(repoPath, target, "preferred", dependsOn, cancellation);
1257
- 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);
1258
1322
  }
1259
1323
  async getAheadOfBaseAfterBaseMove(repoPath, branch, previous, moved, cancellation) {
1260
1324
  const { head, base } = previous;
@@ -1291,7 +1355,7 @@ var BranchesGitSubProvider = class {
1291
1355
  if (cancellation?.aborted) throw new CancellationError();
1292
1356
  if (!movedForward) return void 0;
1293
1357
  const { offHistory, logged } = movedForward;
1294
- const aheadOfNewTip = (await this.getAheadOfMovedBase(repoPath, records, newTip))?.get(head);
1358
+ const aheadOfNewTip = await this.getAheadOfMovedBase(repoPath, records, newTip, head);
1295
1359
  if (cancellation?.aborted) throw new CancellationError();
1296
1360
  if (aheadOfNewTip != null && aheadOfNewTip !== base.ahead) return void 0;
1297
1361
  const forkPoint = base.forkPoint;
@@ -1347,7 +1411,56 @@ var BranchesGitSubProvider = class {
1347
1411
  * or git can't say.
1348
1412
  */
1349
1413
  async getMovedForward(repoPath, records, ref, oldTip, newTip) {
1350
- return await shareBaseMoveRead(this.baseMovedForward, records, `${ref}\0${oldTip}\0${newTip}`, () => this.readMovedForward(this.cache.getCommonPath(repoPath), ref, oldTip, newTip)) || void 0;
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);
1434
+ }
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;
1463
+ }
1351
1464
  }
1352
1465
  /**
1353
1466
  * Reads {@link getMovedForward} from git, in `commonPath`, which holds the refs, their reflogs and the objects for
@@ -1401,31 +1514,43 @@ var BranchesGitSubProvider = class {
1401
1514
  }
1402
1515
  }
1403
1516
  /**
1404
- * For a base moved to `newTip`, read against `records` (the cached ref list), the count of the commits `newTip`
1405
- * lacks for each local branch a worktree has checked out, by tip sha, so a detached HEAD at one of those tips finds
1406
- * its count too. Only those branches are counted: the one walk runs until every counted branch meets `newTip`, so an
1407
- * unrelated old or orphan branch would make it deeper than any worktree's own count, and a HEAD at a commit no
1408
- * checked-out branch points at isn't in the answer. The branches derive from `records`, which the read is kept
1409
- * per, so `newTip` alone keys it. `undefined` when git can't count them in one pass (before 2.41), there is no
1410
- * checked-out branch or `records` can't tell which are, or git couldn't say.
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.
1411
1526
  */
1412
- getAheadOfMovedBase(repoPath, records, newTip) {
1413
- return shareBaseMoveRead(this.aheadOfMovedBase, records, newTip, () => this.readAheadOfMovedBase(this.cache.getCommonPath(repoPath), records, newTip));
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);
1414
1545
  }
1415
1546
  /**
1416
- * Reads {@link getAheadOfMovedBase} from git, in `commonPath`, which holds every worktree's branches. Not bound to
1417
- * one caller's cancellation, since every worktree the move reached rides this one read.
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.
1418
1550
  */
1419
- async readAheadOfMovedBase(commonPath, records, newTip) {
1551
+ async readAheadOfMovedBase(commonPath, checkedOut, newTip) {
1420
1552
  try {
1421
1553
  if (!await this.git.supports("git:for-each-ref:ahead-behind")) return void 0;
1422
- const checkedOut = /* @__PURE__ */ new Set();
1423
- for (const record of records) {
1424
- if (!record.name.startsWith("refs/heads/")) continue;
1425
- if (record.worktreePath == null) return void 0;
1426
- if (record.worktreePath) checkedOut.add(record.name);
1427
- }
1428
- if (!checkedOut.size) return void 0;
1429
1554
  const patterns = toRefPatterns(checkedOut) ?? ["refs/heads"];
1430
1555
  const result = await this.git.run({
1431
1556
  cwd: commonPath,
@@ -1448,60 +1573,376 @@ var BranchesGitSubProvider = class {
1448
1573
  return;
1449
1574
  }
1450
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
+ }
1451
1594
  /**
1452
- * Resolves the base from the recorded candidates, then the branch it was created from or the default branch, for
1453
- * {@link getAheadOfBase}
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
1454
1597
  */
1455
- async getAheadOfBaseInOrder(repoPath, branch, skipUserTarget, dependsOn, cancellation) {
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
+ }
1706
+ /**
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.
1712
+ */
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
+ }
1456
1725
  let target;
1457
1726
  let tier = "default";
1458
- let resolvedDefault;
1459
- if (branch != null) for (const candidate of await this.getRecordedBaseCandidates(repoPath, branch, skipUserTarget)) {
1460
- target = await this.getNearestForkTarget(repoPath, branch, candidate.ref, dependsOn, cancellation);
1461
- if (target != null) {
1462
- tier = candidate.tier;
1463
- break;
1464
- }
1727
+ if (branch != null) {
1728
+ const stored = await this.getStoredBaseTarget(repoPath, branch, remoteBranch, options, reads, cancellation);
1729
+ if (stored != null) ({target, tier} = stored);
1465
1730
  }
1466
1731
  if (target == null) {
1467
- const defaultBranch = await this.getLocalDefaultBranchName(repoPath, dependsOn, cancellation);
1468
- resolvedDefault = defaultBranch ?? null;
1469
- if (branch != null && defaultBranch === branch) return withDefaultBranch(await this.measureAheadOfSelfUpstream(repoPath, branch, dependsOn, cancellation), resolvedDefault);
1470
- 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;
1471
1754
  if (createdFrom != null) {
1472
- const created = await this.getCreatedFromForkTarget(repoPath, branch, createdFrom, defaultBranch, dependsOn, cancellation);
1755
+ const created = await this.getCreatedFromForkTarget(repoPath, branch, createdFrom, defaultBranch, reads, cancellation);
1473
1756
  if (created != null) ({target, tier} = created);
1474
- } else target = await this.getUpstreamNearestForkTarget(repoPath, branch, defaultBranch, dependsOn, cancellation);
1757
+ } else target = await this.getUpstreamNearestForkTarget(repoPath, branch, defaultBranch, reads, cancellation);
1475
1758
  }
1476
1759
  if (cancellation?.aborted) throw new CancellationError();
1477
- if (target == null) {
1478
- let unborn = false;
1479
- if (branch != null) try {
1480
- 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();
1481
1841
  } catch (ex) {
1482
1842
  if (isCancellationError(ex)) throw ex;
1843
+ if (reads != null) reads.pinnable = false;
1844
+ return;
1483
1845
  }
1484
- return withDefaultBranch({
1485
- base: void 0,
1486
- reason: unborn ? "unborn" : "none",
1487
- dependsOn: [...dependsOn]
1488
- }, 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;
1489
1870
  }
1490
- if (branch != null && await this.isSelfAtHead(repoPath, target.ref, branch, cancellation)) return withDefaultBranch({
1491
- base: void 0,
1492
- reason: "self",
1493
- dependsOn: [...dependsOn]
1494
- }, resolvedDefault);
1495
- 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;
1496
1936
  }
1497
1937
  /**
1498
1938
  * The base for a branch created from `createdFrom`: that branch, unless the default branch forked strictly nearer
1499
1939
  * HEAD, as it does once `createdFrom` merged into it and the branch was rebased onto it. A stacked branch's
1500
1940
  * unmerged parent forked nearer, and stays its base. `undefined` when neither is usable.
1501
1941
  */
1502
- async getCreatedFromForkTarget(repoPath, branch, createdFrom, defaultBranch, dependsOn, cancellation) {
1503
- const skipDefault = defaultBranch == null || createdFrom === defaultBranch || createdFrom === `refs/heads/${defaultBranch}` || createdFrom === `refs/remotes/${defaultBranch}`;
1504
- 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)]);
1505
1946
  if (created == null) return fallback != null ? {
1506
1947
  target: fallback,
1507
1948
  tier: "default"
@@ -1512,7 +1953,7 @@ var BranchesGitSubProvider = class {
1512
1953
  };
1513
1954
  return {
1514
1955
  target: created,
1515
- tier: "created"
1956
+ tier: createdFrom.tier
1516
1957
  };
1517
1958
  }
1518
1959
  /** Counts HEAD's commits past `target` for {@link getAheadOfBase} */
@@ -1538,8 +1979,8 @@ var BranchesGitSubProvider = class {
1538
1979
  };
1539
1980
  }
1540
1981
  /** Counts the default branch's own commits past its upstream (see {@link getSelfUpstreamTarget}) for {@link getAheadOfBase} */
1541
- async measureAheadOfSelfUpstream(repoPath, branch, dependsOn, cancellation) {
1542
- const upstream = await this.getSelfUpstreamTarget(repoPath, branch, dependsOn, cancellation);
1982
+ async measureAheadOfSelfUpstream(repoPath, upstream, reads, cancellation) {
1983
+ const { dependsOn } = reads;
1543
1984
  if (cancellation?.aborted) throw new CancellationError();
1544
1985
  if (upstream == null) return {
1545
1986
  base: void 0,
@@ -1557,7 +1998,7 @@ var BranchesGitSubProvider = class {
1557
1998
  reason: "self",
1558
1999
  dependsOn: [...dependsOn]
1559
2000
  };
1560
- 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);
1561
2002
  if (cancellation?.aborted) throw new CancellationError();
1562
2003
  return {
1563
2004
  base: {
@@ -1747,42 +2188,28 @@ var BranchesGitSubProvider = class {
1747
2188
  return [];
1748
2189
  }
1749
2190
  }
1750
- /** Whether `ref` names `branch` itself (see {@link isSelfRef}) and sits at HEAD's commit; tips are read only after the name matched */
1751
- async isSelfAtHead(repoPath, ref, branch, cancellation) {
1752
- if (!await this.isSelfRef(repoPath, ref, branch, cancellation)) return false;
1753
- const [refSha, headSha] = await Promise.all([this.provider.refs.validateReference(repoPath, ref, void 0, cancellation), this.provider.refs.validateReference(repoPath, "HEAD", void 0, cancellation)]);
1754
- return refSha != null && refSha === headSha;
1755
- }
1756
2191
  /**
1757
- * The bases recorded against a branch, most trusted first: the user's merge target, the detected one, then the
1758
- * base a launch recorded. Only the stored value of the latter — `getBaseBranchName` would fall back to a reflog
1759
- * 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
1760
2194
  */
1761
- async getRecordedBaseCandidates(repoPath, branch, skipUserTarget) {
1762
- const tolerate = async (read) => {
1763
- try {
1764
- return (await read())?.trim() || void 0;
1765
- } catch (ex) {
1766
- if (isCancellationError(ex)) throw ex;
1767
- return;
1768
- }
1769
- };
1770
- const reads = await Promise.all([
1771
- skipUserTarget ? void 0 : tolerate(() => this.getStoredUserMergeTargetBranchName(repoPath, branch)),
1772
- tolerate(() => this.getStoredDetectedMergeTargetBranchName(repoPath, branch)),
1773
- tolerate(() => this.provider.config.getGkConfig(repoPath, `branch.${branch}.gk-merge-base`))
1774
- ]);
1775
- const tiers = [
1776
- "user",
1777
- "detected",
1778
- "recorded"
1779
- ];
1780
- const candidates = [];
1781
- for (const [i, ref] of reads.entries()) if (ref && !candidates.some((c) => c.ref === ref)) candidates.push({
1782
- ref,
1783
- tier: tiers[i]
1784
- });
1785
- 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))];
1786
2213
  }
1787
2214
  /**
1788
2215
  * The repo's default branch from local refs only, never the network: the `HEAD` symref of the `upstream` remote,
@@ -1796,12 +2223,7 @@ var BranchesGitSubProvider = class {
1796
2223
  records ??= await this.provider.refs.getRefs(repoPath, cancellation);
1797
2224
  return await this.getRefTip(repoPath, records, ref, cancellation) != null;
1798
2225
  };
1799
- const remotes = await this.getRemoteNames(repoPath, cancellation);
1800
- const preferred = new Set([
1801
- remotes.find((r) => r === "upstream"),
1802
- remotes.find((r) => r === "origin"),
1803
- remotes.at(0)
1804
- ].filter((r) => r != null));
2226
+ const preferred = await this.getPreferredRemotes(repoPath, cancellation);
1805
2227
  for (const remote of preferred) {
1806
2228
  dependsOn.add(`refs/remotes/${remote}/HEAD`);
1807
2229
  const symref = await this.getDefaultBranchName(repoPath, remote, { local: true }, cancellation);
@@ -1840,7 +2262,7 @@ var BranchesGitSubProvider = class {
1840
2262
  * remote and ref lists; `undefined` when there is no `upstream` remote, it lacks the branch, or the base is that
1841
2263
  * ref itself.
1842
2264
  */
1843
- async getUpstreamContender(repoPath, base, dependsOn, cancellation) {
2265
+ async getUpstreamContender(repoPath, base, reads, cancellation) {
1844
2266
  if (!base) return void 0;
1845
2267
  const remotes = await this.getRemoteNames(repoPath, cancellation);
1846
2268
  if (!remotes.includes("upstream")) return void 0;
@@ -1857,33 +2279,37 @@ var BranchesGitSubProvider = class {
1857
2279
  }
1858
2280
  const contender = `upstream/${name}`;
1859
2281
  const remoteRef = `refs/remotes/${contender}`;
1860
- if (contender === base || remoteRef === base || findRefTip(records, remoteRef) == null) return void 0;
1861
- 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);
1862
2288
  return contender;
1863
2289
  }
1864
2290
  /** {@link getNearestForkTarget} with the `upstream` remote's copy of `candidate` as its contender */
1865
- async getUpstreamNearestForkTarget(repoPath, branch, candidate, dependsOn, cancellation) {
1866
- 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) });
1867
2293
  }
1868
2294
  /**
1869
- * A candidate base as the ref HEAD is measured against: the candidate or its tracking ref, whichever forked
1870
- * 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
1871
2297
  * `contender` other than the tracking ref is taken only when it forked strictly nearer than both, so a tie keeps
1872
- * the candidate's own pick. A candidate none forks from is used as-is if it names a commit. `undefined` when the
1873
- * 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.
1874
2300
  */
1875
- async getNearestForkTarget(repoPath, branch, candidate, dependsOn, cancellation, options) {
2301
+ async getNearestForkTarget(repoPath, branch, candidate, reads, cancellation, options) {
1876
2302
  if (!candidate || candidate.startsWith("-")) return void 0;
1877
2303
  if (candidate === branch || candidate === `refs/heads/${branch}`) return void 0;
1878
- addRefDependencies(dependsOn, candidate);
2304
+ addRefDependencies(reads.dependsOn, candidate);
1879
2305
  const upstreamName = this.getUpstreamName(repoPath, candidate, void 0, cancellation);
1880
2306
  const getUpstreamFork = async () => {
1881
2307
  const upstream = await upstreamName;
1882
2308
  if (!upstream || upstream.startsWith("-")) return void 0;
1883
- addRefDependencies(dependsOn, upstream);
2309
+ addRefDependencies(reads.dependsOn, upstream);
1884
2310
  return {
1885
2311
  ref: upstream,
1886
- mergeBase: await this.getForkPoint(repoPath, upstream, cancellation)
2312
+ mergeBase: await this.getForkPoint(repoPath, upstream, reads, cancellation)
1887
2313
  };
1888
2314
  };
1889
2315
  const contender = options?.contender;
@@ -1896,10 +2322,15 @@ var BranchesGitSubProvider = class {
1896
2322
  const upstream = await upstreamName;
1897
2323
  if (upstream && tip === findRefTip(records, upstream)) return void 0;
1898
2324
  }
1899
- 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
1900
2331
  };
1901
2332
  const [candidateMergeBase, upstreamFork, contenderMergeBase] = await Promise.all([
1902
- this.getForkPoint(repoPath, candidate, cancellation),
2333
+ this.getForkPoint(repoPath, candidate, candidateReads, cancellation),
1903
2334
  getUpstreamFork(),
1904
2335
  getContenderMergeBase()
1905
2336
  ]);
@@ -1918,18 +2349,33 @@ var BranchesGitSubProvider = class {
1918
2349
  ref: contender,
1919
2350
  mergeBase: contenderMergeBase
1920
2351
  };
1921
- if (nearest != null) return {
1922
- ref: nearest.ref,
1923
- forkPoint: nearest.mergeBase
1924
- };
1925
- 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 {
1926
2371
  ref: candidate,
1927
2372
  forkPoint: void 0
1928
- } : void 0;
2373
+ };
1929
2374
  }
1930
2375
  /**
1931
2376
  * `<name>@{u}`, without asking git when the cached ref list shows `name` can't have one: a failed lookup isn't
1932
- * 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.
1933
2379
  */
1934
2380
  async getUpstreamName(repoPath, name, options, cancellation) {
1935
2381
  if (!mayHaveUpstream(await this.provider.refs.getRefs(repoPath, cancellation), name)) return void 0;
@@ -1940,7 +2386,8 @@ var BranchesGitSubProvider = class {
1940
2386
  * when an `upstream` remote has it, else `<branch>@{u}`. Read from the cached ref and branch lists; only a
1941
2387
  * `<branch>` that isn't checked out spawns, for its `@{u}`. `undefined` when there is neither, or its ref is gone.
1942
2388
  */
1943
- async getSelfUpstreamTarget(repoPath, branch, dependsOn, cancellation) {
2389
+ async getSelfUpstreamTarget(repoPath, branch, reads, cancellation) {
2390
+ const { dependsOn } = reads;
1944
2391
  const records = await this.provider.refs.getRefs(repoPath, cancellation);
1945
2392
  let upstream;
1946
2393
  if ((await this.getRemoteNames(repoPath, cancellation)).includes("upstream")) {
@@ -1976,9 +2423,36 @@ var BranchesGitSubProvider = class {
1976
2423
  if (records.length) return findRefTip(records, name);
1977
2424
  return this.provider.refs.validateReference(repoPath, name, void 0, cancellation);
1978
2425
  }
1979
- /** The merge base of HEAD and `ref`, preferring git's fork-point reading of `ref`'s reflog when it has one */
1980
- async getForkPoint(repoPath, ref, cancellation) {
1981
- 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;
1982
2456
  }
1983
2457
  /**
1984
2458
  * The stored merge target — the user's pick, else the one detected from its PR — skipping any whose branch git
@@ -2025,6 +2499,7 @@ var BranchesGitSubProvider = class {
2025
2499
  forgetBranchCreatedFrom(repoPath, name) {
2026
2500
  const key = this.getBranchActivityKey(repoPath, name);
2027
2501
  this._branchCreatedFrom.delete(key);
2502
+ this._branchCheckedOutFrom.delete(key);
2028
2503
  this._branchCreatedFromGenerations.set(key, (this._branchCreatedFromGenerations.get(key) ?? 0) + 1);
2029
2504
  }
2030
2505
  /** Forgets this process's throttle marks for `name`, so its next activity re-reads the stored stamps */
@@ -2169,32 +2644,66 @@ var BranchesGitSubProvider = class {
2169
2644
  this.context.hooks?.repository?.onChanged?.(repoPath, ["starred"]);
2170
2645
  }
2171
2646
  };
2172
- __decorate([debug()], BranchesGitSubProvider.prototype, "getBranch", null);
2173
- __decorate([debug({ args: (repoPath) => ({ repoPath }) })], BranchesGitSubProvider.prototype, "getBranches", null);
2174
- __decorate([debug()], BranchesGitSubProvider.prototype, "getBranchContributionsOverview", null);
2175
- __decorate([debug()], BranchesGitSubProvider.prototype, "getBranchesWithCommits", null);
2176
- __decorate([debug()], BranchesGitSubProvider.prototype, "getCurrentBranchReference", null);
2177
- __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);
2178
2659
  __decorate([debug()], BranchesGitSubProvider.prototype, "setRemoteHeadIfMissing", null);
2179
2660
  __decorate([debug()], BranchesGitSubProvider.prototype, "createBranch", null);
2180
2661
  __decorate([debug()], BranchesGitSubProvider.prototype, "deleteLocalBranch", null);
2181
2662
  __decorate([debug()], BranchesGitSubProvider.prototype, "deleteRemoteBranch", null);
2182
- __decorate([debug()], BranchesGitSubProvider.prototype, "getBranchMergedStatus", null);
2183
- __decorate([debug()], BranchesGitSubProvider.prototype, "getLocalBranchByUpstream", null);
2184
- __decorate([trace({ exit: true })], BranchesGitSubProvider.prototype, "getPushTarget", null);
2185
- __decorate([debug()], BranchesGitSubProvider.prototype, "getPotentialApplyConflicts", null);
2186
- __decorate([debug()], BranchesGitSubProvider.prototype, "getPotentialMergeConflicts", null);
2187
- __decorate([debug()], BranchesGitSubProvider.prototype, "getPotentialStashReapplyConflicts", null);
2188
- __decorate([debug({ exit: true })], BranchesGitSubProvider.prototype, "getBaseBranchName", null);
2189
- __decorate([trace()], BranchesGitSubProvider.prototype, "getAheadOfBaseConfigKey", null);
2190
- __decorate([trace()], BranchesGitSubProvider.prototype, "getAheadOfBaseConfigEntries", null);
2191
- __decorate([debug({ exit: true })], BranchesGitSubProvider.prototype, "getAheadOfBase", null);
2192
- __decorate([debug({ exit: true })], BranchesGitSubProvider.prototype, "getAheadOfBaseAfterBaseMove", null);
2193
- __decorate([debug({ exit: true })], BranchesGitSubProvider.prototype, "getAheadBehindCounts", null);
2194
- __decorate([debug({ exit: true })], BranchesGitSubProvider.prototype, "getMergedBranches", null);
2195
- __decorate([debug({ exit: true })], BranchesGitSubProvider.prototype, "getStoredMergeTargetBranchName", null);
2196
- __decorate([trace({ exit: true })], BranchesGitSubProvider.prototype, "getStoredDetectedMergeTargetBranchName", null);
2197
- __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);
2198
2707
  __decorate([trace()], BranchesGitSubProvider.prototype, "onCurrentBranchAccessed", null);
2199
2708
  __decorate([trace()], BranchesGitSubProvider.prototype, "onCurrentBranchModified", null);
2200
2709
  __decorate([trace()], BranchesGitSubProvider.prototype, "onCurrentBranchAgentActivity", null);
@@ -2260,15 +2769,46 @@ function getRefRecordsByName(records) {
2260
2769
  }
2261
2770
  return byName;
2262
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
+ }
2263
2804
  /** The record for the full ref name `fullName` in `records`, the cached ref list; no short-name resolution */
2264
2805
  function findRefRecord(records, fullName) {
2265
2806
  return getRefRecordsByName(records).get(fullName);
2266
2807
  }
2267
2808
  function toPushTargetRef(records, ref) {
2268
- const record = findRefRecord(records, ref);
2269
2809
  return {
2270
2810
  ref,
2271
- sha: record != null ? record.peeledObjectname || record.objectname : void 0
2811
+ sha: getListedTip(records, ref)
2272
2812
  };
2273
2813
  }
2274
2814
  /**
@@ -2335,6 +2875,48 @@ function addRefDependencies(dependsOn, name) {
2335
2875
  dependsOn.add(`refs/heads/${name}`);
2336
2876
  dependsOn.add(`refs/remotes/${name}`);
2337
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
+ }
2338
2920
  //#endregion
2339
2921
  export { BranchesGitSubProvider };
2340
2922