@gitkraken/core-gitlens 0.15.7 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,6 +17,13 @@ export declare class BranchesGitSubProvider implements GitBranchesSubProvider {
17
17
  private readonly provider;
18
18
  /** Ahead/behind counts by `<baseSha>...<tipSha>` range, from the per-tip fallback; a pair of commits always counts the same */
19
19
  private readonly aheadBehindBySha;
20
+ /**
21
+ * For a base that moved only forward, the commits its reflog reaches off the new tip's history (else `false`), by
22
+ * `<ref>\0<oldSha>\0<newSha>`, for each cached ref list it was read against: a ref write replaces that list, along
23
+ * with the reflog the answer came from. `undefined` when git couldn't say, which isn't kept. A fetch moves one base
24
+ * under every worktree at once, so they share the one check, joining it while it runs
25
+ */
26
+ private readonly baseMovedForward;
20
27
  /**
21
28
  * When each `commonPath\0branch\0key` was last stamped by this process. Bounded: a dropped entry only costs a
22
29
  * read of the stored stamp, which still holds off a write under 5 minutes old
@@ -32,6 +39,12 @@ export declare class BranchesGitSubProvider implements GitBranchesSubProvider {
32
39
  private readonly _branchCreatedFrom;
33
40
  /** Bumped each time a key's created-from is forgotten, so a reflog read in flight across that doesn't store it */
34
41
  private readonly _branchCreatedFromGenerations;
42
+ /**
43
+ * The repositories (by common path) whose last config listing showed config that can differ by checkout, so a linked
44
+ * checkout reads its own listing without first waiting on the main checkout's. Only a shortcut: a listing that shows
45
+ * otherwise drops the repository again
46
+ */
47
+ private readonly _configDiffersByCheckout;
35
48
  constructor(context: GitServiceContext, git: Git, cache: Cache, provider: CliGitProviderInternal);
36
49
  getBranch(repoPath: string, name?: string, options?: {
37
50
  force?: boolean;
@@ -137,10 +150,47 @@ export declare class BranchesGitSubProvider implements GitBranchesSubProvider {
137
150
  */
138
151
  private getCreatedFromTarget;
139
152
  getAheadOfBaseConfigKey(repoPath: string, cancellation?: AbortSignal): Promise<string | undefined>;
153
+ getAheadOfBaseConfigEntries(repoPath: string, cancellation?: AbortSignal): Promise<readonly (readonly [key: string, value: string | undefined])[] | undefined>;
154
+ /**
155
+ * A checkout's listing of the config inputs to {@link getAheadOfBase}. Every checkout of a repository shares the main
156
+ * checkout's, unless it shows config that can differ by checkout (see `mayConfigDifferByCheckout`); then
157
+ * each linked checkout reads its own, and keeps doing so until one of its own reads shows config no longer can.
158
+ */
159
+ private getAheadOfBaseConfigListing;
160
+ private readAheadOfBaseConfigListing;
140
161
  getAheadOfBase(repoPath: string, branch: string | undefined, options?: {
141
162
  skipUserTarget?: boolean;
142
163
  preferredTarget?: string;
143
164
  }, cancellation?: AbortSignal): Promise<AheadOfBase>;
165
+ getAheadOfBaseAfterBaseMove(repoPath: string, branch: string | undefined, previous: {
166
+ readonly head: string;
167
+ readonly base: NonNullable<AheadOfBase['base']>;
168
+ readonly dependsOn: readonly string[];
169
+ readonly defaultBranch?: string | null;
170
+ }, moved: readonly string[], cancellation?: AbortSignal): Promise<AheadOfBase | undefined>;
171
+ /**
172
+ * Whether the repository is shallow, by its common git directory's `shallow` file, which spawns nothing; `undefined`
173
+ * when it can't be read
174
+ */
175
+ private isShallowRepository;
176
+ /** Whether `ref` is a `refs/remotes/<remote>/HEAD` symref pointing at `target`, from the cached local symref */
177
+ private isRemoteHeadOf;
178
+ /**
179
+ * For `ref` (a full name in `records`, the cached ref list) moving from `oldTip` to `newTip`, the commits its reflog
180
+ * reaches off `newTip`'s history when it moved only forward: `oldTip` and every value its reflog took after it are in
181
+ * `newTip`'s history. `--fork-point` reads every reflog entry, so one in between that shares more of HEAD's history
182
+ * than `newTip` does (a branch's commit that landed and was force-pushed away) moves the fork point while the count
183
+ * stands; an older one off `newTip`'s history may hold such a commit too, which each worktree's count checks for.
184
+ * Without a reflog it reads only the tip. `undefined` when it didn't move only forward, or git can't say.
185
+ */
186
+ private getMovedForward;
187
+ /**
188
+ * Reads {@link getMovedForward} from git, in `commonPath`, which holds the refs, their reflogs and the objects for
189
+ * every worktree. Not bound to one caller's cancellation, since every worktree the move reached rides this one
190
+ * read. `false` when the ref didn't move only forward, or reaches too much history off `newTip`'s to tell;
191
+ * `undefined` when git couldn't say.
192
+ */
193
+ private readMovedForward;
144
194
  /**
145
195
  * Resolves the base from the recorded candidates, then the branch it was created from or the default branch, for
146
196
  * {@link getAheadOfBase}
@@ -6,6 +6,7 @@ import { getScopedLogger } from "../../utils/logger.scoped.js";
6
6
  import { getSettledValue, mapBounded } from "../../utils/promise.js";
7
7
  import { debug, trace } from "../../utils/decorators/log.js";
8
8
  import { getBranchId } from "../../utils/gitRefs.js";
9
+ import { joinUriPath } from "../../utils/uri.js";
9
10
  import { normalizePath } from "../../utils/path.js";
10
11
  import { createRevisionRange, shortenRevision } from "../../git/utils/revision.utils.js";
11
12
  import { formatDetachedHeadName, getLocalBranchByUpstream, isDetachedHead, isRemoteHEAD, parseBranchMetadataGkConfigKey, parseRefName, parseUpstream, setBranchMetadataValue, splitRemoteBranchName } from "../../git/utils/branch.utils.js";
@@ -24,6 +25,7 @@ import { LruMap } from "../../utils/lruMap.js";
24
25
  import { PageableResult, emptyPagedResult } from "../../utils/paging.js";
25
26
  import { parseMergeTreeConflict } from "../parsers/mergeTreeParser.js";
26
27
  import { resolveBranchRemote } from "./branchRemote.js";
28
+ import { isGitBooleanFalse } from "./config.js";
27
29
  //#region ../git-cli/src/providers/branches.ts
28
30
  /** Minimum time between writes to the config for last accessed/modified dates */
29
31
  const dateMetadataStaleThresholdMs = 3e5;
@@ -47,6 +49,12 @@ const branchActivityKeys = [
47
49
  "gk-last-modified",
48
50
  "gk-agent-last-activity"
49
51
  ];
52
+ /**
53
+ * The most commits a base's reflog may reach off its new tip's history for a re-measure: every worktree's count
54
+ * excludes each of them on its command line, and more come only from a long history force-pushed away, where a full
55
+ * read costs less
56
+ */
57
+ const maxOffHistoryCommits = 64;
50
58
  var BranchesGitSubProvider = class {
51
59
  context;
52
60
  git;
@@ -55,6 +63,13 @@ var BranchesGitSubProvider = class {
55
63
  /** Ahead/behind counts by `<baseSha>...<tipSha>` range, from the per-tip fallback; a pair of commits always counts the same */
56
64
  aheadBehindBySha = new LruMap(4096);
57
65
  /**
66
+ * For a base that moved only forward, the commits its reflog reaches off the new tip's history (else `false`), by
67
+ * `<ref>\0<oldSha>\0<newSha>`, for each cached ref list it was read against: a ref write replaces that list, along
68
+ * with the reflog the answer came from. `undefined` when git couldn't say, which isn't kept. A fetch moves one base
69
+ * under every worktree at once, so they share the one check, joining it while it runs
70
+ */
71
+ baseMovedForward = /* @__PURE__ */ new WeakMap();
72
+ /**
58
73
  * When each `commonPath\0branch\0key` was last stamped by this process. Bounded: a dropped entry only costs a
59
74
  * read of the stored stamp, which still holds off a write under 5 minutes old
60
75
  */
@@ -69,6 +84,12 @@ var BranchesGitSubProvider = class {
69
84
  _branchCreatedFrom = new LruMap(1024);
70
85
  /** Bumped each time a key's created-from is forgotten, so a reflog read in flight across that doesn't store it */
71
86
  _branchCreatedFromGenerations = new LruMap(1024);
87
+ /**
88
+ * The repositories (by common path) whose last config listing showed config that can differ by checkout, so a linked
89
+ * checkout reads its own listing without first waiting on the main checkout's. Only a shortcut: a listing that shows
90
+ * otherwise drops the repository again
91
+ */
92
+ _configDiffersByCheckout = /* @__PURE__ */ new Set();
72
93
  constructor(context, git, cache, provider) {
73
94
  this.context = context;
74
95
  this.git = git;
@@ -1129,20 +1150,50 @@ var BranchesGitSubProvider = class {
1129
1150
  return name;
1130
1151
  }
1131
1152
  getAheadOfBaseConfigKey(repoPath, cancellation) {
1153
+ return this.getAheadOfBaseConfigListing(repoPath, cancellation);
1154
+ }
1155
+ async getAheadOfBaseConfigEntries(repoPath, cancellation) {
1156
+ const listing = await this.getAheadOfBaseConfigListing(repoPath, cancellation);
1157
+ return listing != null ? parseConfigListing(listing) : void 0;
1158
+ }
1159
+ /**
1160
+ * A checkout's listing of the config inputs to {@link getAheadOfBase}. Every checkout of a repository shares the main
1161
+ * checkout's, unless it shows config that can differ by checkout (see `mayConfigDifferByCheckout`); then
1162
+ * each linked checkout reads its own, and keeps doing so until one of its own reads shows config no longer can.
1163
+ */
1164
+ getAheadOfBaseConfigListing(repoPath, cancellation) {
1132
1165
  return this.cache.getAheadOfBaseConfigKey(repoPath, async (checkoutPath, cacheable, signal) => {
1133
- const result = await this.git.run({
1134
- cwd: checkoutPath,
1135
- cancellation: signal,
1136
- expectedExitCodes: [1]
1137
- }, "config", "--null", "--get-regexp", "^(branch\\..*\\.(remote|merge)|remote\\..*|init\\.defaultbranch)$");
1138
- if (signal?.aborted) throw new CancellationError();
1139
- if (result.completion.status !== "exited") {
1140
- cacheable.invalidate();
1141
- return;
1166
+ const commonPath = this.cache.getCommonPath(checkoutPath);
1167
+ const isMain = checkoutPath === commonPath;
1168
+ if (isMain || !this._configDiffersByCheckout.has(commonPath)) {
1169
+ const shared = await this.cache.getAheadOfBaseConfigListing(checkoutPath, (path, sharedCacheable, sharedSignal) => this.readAheadOfBaseConfigListing(path, sharedCacheable, sharedSignal), signal);
1170
+ if (isMain) {
1171
+ if (shared == null) cacheable.invalidate();
1172
+ return shared;
1173
+ }
1174
+ if (shared != null) {
1175
+ if (!mayConfigDifferByCheckout(shared)) return shared;
1176
+ this._configDiffersByCheckout.add(commonPath);
1177
+ }
1142
1178
  }
1143
- return result.stdout;
1179
+ const own = await this.readAheadOfBaseConfigListing(checkoutPath, cacheable, signal);
1180
+ if (own != null && !mayConfigDifferByCheckout(own)) this._configDiffersByCheckout.delete(commonPath);
1181
+ return own;
1144
1182
  }, cancellation);
1145
1183
  }
1184
+ async readAheadOfBaseConfigListing(cwd, cacheable, cancellation) {
1185
+ const result = await this.git.run({
1186
+ cwd,
1187
+ cancellation,
1188
+ expectedExitCodes: [1]
1189
+ }, "config", "--null", "--get-regexp", "^(branch\\..*\\.(remote|merge)|remote\\..*|init\\.defaultbranch|includeif\\..*|extensions\\.worktreeconfig)$");
1190
+ if (cancellation?.aborted) throw new CancellationError();
1191
+ if (result.completion.status !== "exited") {
1192
+ cacheable.invalidate();
1193
+ return;
1194
+ }
1195
+ return result.stdout;
1196
+ }
1146
1197
  async getAheadOfBase(repoPath, branch, options, cancellation) {
1147
1198
  if (branch != null && isDetachedHead(branch)) branch = void 0;
1148
1199
  const dependsOn = new Set(branch != null ? [`refs/heads/${branch}`] : []);
@@ -1154,6 +1205,152 @@ var BranchesGitSubProvider = class {
1154
1205
  if (target != null) return this.measureAheadOfBase(repoPath, target, "preferred", dependsOn, cancellation);
1155
1206
  return this.getAheadOfBaseInOrder(repoPath, branch, options?.skipUserTarget, dependsOn, cancellation);
1156
1207
  }
1208
+ async getAheadOfBaseAfterBaseMove(repoPath, branch, previous, moved, cancellation) {
1209
+ const { head, base } = previous;
1210
+ const oldTip = base.sha;
1211
+ if (oldTip == null || !moved.length) return void 0;
1212
+ if (base.tier === "preferred" || base.tier === "upstream" && base.forkPoint === oldTip) return void 0;
1213
+ try {
1214
+ const records = await this.provider.refs.getRefs(repoPath, cancellation);
1215
+ const record = records.length ? resolveRefRecord(records, base.ref) : void 0;
1216
+ if (record == null || !moved.includes(record.name)) return void 0;
1217
+ if (isAmbiguousRefName(records, base.ref)) return void 0;
1218
+ const newTip = record.peeledObjectname || record.objectname;
1219
+ if (!newTip || newTip === oldTip) return void 0;
1220
+ if (branch != null && !isDetachedHead(branch) && findRefTip(records, `refs/heads/${branch}`) !== head) return;
1221
+ let remoteHeadMoved = false;
1222
+ for (const ref of moved) {
1223
+ if (ref === record.name) continue;
1224
+ if (findRefTip(records, ref) !== newTip || !await this.isRemoteHeadOf(repoPath, ref, record.name, cancellation)) return;
1225
+ remoteHeadMoved = true;
1226
+ }
1227
+ if (remoteHeadMoved && previous.defaultBranch === void 0) return void 0;
1228
+ if (previous.defaultBranch !== void 0) {
1229
+ const defaultBranch = await this.getLocalDefaultBranchName(repoPath, /* @__PURE__ */ new Set(), cancellation);
1230
+ if (cancellation?.aborted) throw new CancellationError();
1231
+ if ((defaultBranch ?? null) !== previous.defaultBranch) return void 0;
1232
+ }
1233
+ if (previous.dependsOn.some((r) => r.startsWith("refs/remotes/upstream/") && !r.endsWith("/HEAD"))) for (const ref of previous.dependsOn) {
1234
+ if (ref === record.name || ref.endsWith("/HEAD")) continue;
1235
+ const tip = findRefTip(records, ref);
1236
+ if (tip === oldTip || tip === newTip) return void 0;
1237
+ }
1238
+ if (await this.isShallowRepository(repoPath) !== false) return void 0;
1239
+ const offHistory = await this.getMovedForward(repoPath, records, record.name, oldTip, newTip);
1240
+ if (cancellation?.aborted) throw new CancellationError();
1241
+ if (!offHistory) return void 0;
1242
+ const result = await this.git.run({
1243
+ cwd: repoPath,
1244
+ cancellation,
1245
+ errors: "ignore"
1246
+ }, "rev-list", "--count", head, "--not", newTip, ...base.forkPoint != null ? [base.forkPoint] : [], ...offHistory, "--");
1247
+ if (cancellation?.aborted) throw new CancellationError();
1248
+ if (result.completion.status !== "exited" || result.exitCode !== 0) return void 0;
1249
+ if (parseInt(result.stdout.trim(), 10) !== base.ahead) return void 0;
1250
+ return withDefaultBranch({
1251
+ base: {
1252
+ ...base,
1253
+ sha: newTip
1254
+ },
1255
+ dependsOn: [...previous.dependsOn]
1256
+ }, previous.defaultBranch);
1257
+ } catch (ex) {
1258
+ if (isCancellationError(ex)) throw ex;
1259
+ return;
1260
+ }
1261
+ }
1262
+ /**
1263
+ * Whether the repository is shallow, by its common git directory's `shallow` file, which spawns nothing; `undefined`
1264
+ * when it can't be read
1265
+ */
1266
+ async isShallowRepository(repoPath) {
1267
+ try {
1268
+ const gitDir = await this.provider.config.getGitDir(repoPath);
1269
+ return await this.context.fs.stat(joinUriPath(gitDir.commonUri ?? gitDir.uri, "shallow")) != null;
1270
+ } catch {
1271
+ return;
1272
+ }
1273
+ }
1274
+ /** Whether `ref` is a `refs/remotes/<remote>/HEAD` symref pointing at `target`, from the cached local symref */
1275
+ async isRemoteHeadOf(repoPath, ref, target, cancellation) {
1276
+ if (!ref.startsWith("refs/remotes/") || !ref.endsWith("/HEAD")) return false;
1277
+ const remote = ref.slice(13, -5);
1278
+ if (!remote) return false;
1279
+ const symref = await this.getDefaultBranchName(repoPath, remote, { local: true }, cancellation);
1280
+ return symref != null && `refs/remotes/${symref}` === target;
1281
+ }
1282
+ /**
1283
+ * For `ref` (a full name in `records`, the cached ref list) moving from `oldTip` to `newTip`, the commits its reflog
1284
+ * reaches off `newTip`'s history when it moved only forward: `oldTip` and every value its reflog took after it are in
1285
+ * `newTip`'s history. `--fork-point` reads every reflog entry, so one in between that shares more of HEAD's history
1286
+ * than `newTip` does (a branch's commit that landed and was force-pushed away) moves the fork point while the count
1287
+ * stands; an older one off `newTip`'s history may hold such a commit too, which each worktree's count checks for.
1288
+ * Without a reflog it reads only the tip. `undefined` when it didn't move only forward, or git can't say.
1289
+ */
1290
+ async getMovedForward(repoPath, records, ref, oldTip, newTip) {
1291
+ let checks = this.baseMovedForward.get(records);
1292
+ if (checks == null) {
1293
+ checks = /* @__PURE__ */ new Map();
1294
+ this.baseMovedForward.set(records, checks);
1295
+ }
1296
+ const key = `${ref}\0${oldTip}\0${newTip}`;
1297
+ let check = checks.get(key);
1298
+ if (check == null) {
1299
+ check = this.readMovedForward(this.cache.getCommonPath(repoPath), ref, oldTip, newTip);
1300
+ checks.set(key, check);
1301
+ }
1302
+ const offHistory = await check;
1303
+ if (offHistory == null && checks.get(key) === check) checks.delete(key);
1304
+ return offHistory || void 0;
1305
+ }
1306
+ /**
1307
+ * Reads {@link getMovedForward} from git, in `commonPath`, which holds the refs, their reflogs and the objects for
1308
+ * every worktree. Not bound to one caller's cancellation, since every worktree the move reached rides this one
1309
+ * read. `false` when the ref didn't move only forward, or reaches too much history off `newTip`'s to tell;
1310
+ * `undefined` when git couldn't say.
1311
+ */
1312
+ async readMovedForward(commonPath, ref, oldTip, newTip) {
1313
+ let since = /* @__PURE__ */ new Set([oldTip]);
1314
+ let logged = /* @__PURE__ */ new Set([oldTip]);
1315
+ try {
1316
+ const entries = await this.provider.refs.getReflogEntries(commonPath, ref);
1317
+ if (entries.length) {
1318
+ if (entries[0].sha !== newTip) return false;
1319
+ const oldest = entries.findLastIndex((e) => e.sha === oldTip);
1320
+ if (oldest < 0) return false;
1321
+ since = new Set(entries.slice(1, oldest + 1).map((e) => e.sha));
1322
+ logged = new Set(entries.map((e) => e.sha));
1323
+ const before = await this.git.run({
1324
+ cwd: commonPath,
1325
+ errors: "ignore"
1326
+ }, "rev-parse", "--verify", `${ref}@{${entries.length}}`);
1327
+ if (before.completion.status !== "exited") return void 0;
1328
+ if (before.exitCode === 0) {
1329
+ const sha = before.stdout.trim();
1330
+ if (!sha) return void 0;
1331
+ logged.add(sha);
1332
+ } else if (!before.stderr?.includes(" only has ")) return;
1333
+ else if (!await this.git.supports("git:rev-parse:reflog-oldest-old-value")) return false;
1334
+ logged.delete(newTip);
1335
+ }
1336
+ } catch {
1337
+ return;
1338
+ }
1339
+ try {
1340
+ const result = await this.git.run({
1341
+ cwd: commonPath,
1342
+ errors: "ignore",
1343
+ stdin: `${[...logged, `^${newTip}`].join("\n")}\n`
1344
+ }, "rev-list", `--max-count=65`, "--stdin");
1345
+ if (result.completion.status !== "exited" || result.exitCode !== 0) return void 0;
1346
+ const off = result.stdout.split("\n").filter((sha) => sha.length > 0);
1347
+ if (off.length > maxOffHistoryCommits) return false;
1348
+ if (off.some((sha) => since.has(sha))) return false;
1349
+ return off;
1350
+ } catch {
1351
+ return;
1352
+ }
1353
+ }
1157
1354
  /**
1158
1355
  * Resolves the base from the recorded candidates, then the branch it was created from or the default branch, for
1159
1356
  * {@link getAheadOfBase}
@@ -1161,6 +1358,7 @@ var BranchesGitSubProvider = class {
1161
1358
  async getAheadOfBaseInOrder(repoPath, branch, skipUserTarget, dependsOn, cancellation) {
1162
1359
  let target;
1163
1360
  let tier = "default";
1361
+ let resolvedDefault;
1164
1362
  if (branch != null) for (const candidate of await this.getRecordedBaseCandidates(repoPath, branch, skipUserTarget)) {
1165
1363
  target = await this.getNearestForkTarget(repoPath, branch, candidate.ref, dependsOn, cancellation);
1166
1364
  if (target != null) {
@@ -1170,7 +1368,8 @@ var BranchesGitSubProvider = class {
1170
1368
  }
1171
1369
  if (target == null) {
1172
1370
  const defaultBranch = await this.getLocalDefaultBranchName(repoPath, dependsOn, cancellation);
1173
- if (branch != null && defaultBranch === branch) return this.measureAheadOfSelfUpstream(repoPath, branch, dependsOn, cancellation);
1371
+ resolvedDefault = defaultBranch ?? null;
1372
+ if (branch != null && defaultBranch === branch) return withDefaultBranch(await this.measureAheadOfSelfUpstream(repoPath, branch, dependsOn, cancellation), resolvedDefault);
1174
1373
  const createdFrom = branch != null ? await this.getCreatedFromTarget(repoPath, branch, cancellation) : void 0;
1175
1374
  if (createdFrom != null) {
1176
1375
  const created = await this.getCreatedFromForkTarget(repoPath, branch, createdFrom, defaultBranch, dependsOn, cancellation);
@@ -1185,18 +1384,18 @@ var BranchesGitSubProvider = class {
1185
1384
  } catch (ex) {
1186
1385
  if (isCancellationError(ex)) throw ex;
1187
1386
  }
1188
- return {
1387
+ return withDefaultBranch({
1189
1388
  base: void 0,
1190
1389
  reason: unborn ? "unborn" : "none",
1191
1390
  dependsOn: [...dependsOn]
1192
- };
1391
+ }, resolvedDefault);
1193
1392
  }
1194
- if (branch != null && await this.isSelfAtHead(repoPath, target.ref, branch, cancellation)) return {
1393
+ if (branch != null && await this.isSelfAtHead(repoPath, target.ref, branch, cancellation)) return withDefaultBranch({
1195
1394
  base: void 0,
1196
1395
  reason: "self",
1197
1396
  dependsOn: [...dependsOn]
1198
- };
1199
- return this.measureAheadOfBase(repoPath, target, tier, dependsOn, cancellation);
1397
+ }, resolvedDefault);
1398
+ return withDefaultBranch(await this.measureAheadOfBase(repoPath, target, tier, dependsOn, cancellation), resolvedDefault);
1200
1399
  }
1201
1400
  /**
1202
1401
  * The base for a branch created from `createdFrom`: that branch, unless the default branch forked strictly nearer
@@ -1895,7 +2094,9 @@ __decorate([debug()], BranchesGitSubProvider.prototype, "getPotentialMergeConfli
1895
2094
  __decorate([debug()], BranchesGitSubProvider.prototype, "getPotentialStashReapplyConflicts", null);
1896
2095
  __decorate([debug({ exit: true })], BranchesGitSubProvider.prototype, "getBaseBranchName", null);
1897
2096
  __decorate([trace()], BranchesGitSubProvider.prototype, "getAheadOfBaseConfigKey", null);
2097
+ __decorate([trace()], BranchesGitSubProvider.prototype, "getAheadOfBaseConfigEntries", null);
1898
2098
  __decorate([debug({ exit: true })], BranchesGitSubProvider.prototype, "getAheadOfBase", null);
2099
+ __decorate([debug({ exit: true })], BranchesGitSubProvider.prototype, "getAheadOfBaseAfterBaseMove", null);
1899
2100
  __decorate([debug({ exit: true })], BranchesGitSubProvider.prototype, "getAheadBehindCounts", null);
1900
2101
  __decorate([debug({ exit: true })], BranchesGitSubProvider.prototype, "getMergedBranches", null);
1901
2102
  __decorate([debug({ exit: true })], BranchesGitSubProvider.prototype, "getStoredMergeTargetBranchName", null);
@@ -1924,18 +2125,38 @@ const refRecordsByName = /* @__PURE__ */ new WeakMap();
1924
2125
  * `refs/heads/` before `refs/remotes/`); annotated tags peel to their commit. `undefined` when no listed ref matches.
1925
2126
  */
1926
2127
  function findRefTip(records, name) {
2128
+ const record = resolveRefRecord(records, name);
2129
+ return record != null ? record.peeledObjectname || record.objectname : void 0;
2130
+ }
2131
+ /** The listed ref `name` resolves to, in {@link findRefTip}'s order */
2132
+ function resolveRefRecord(records, name) {
1927
2133
  const byName = getRefRecordsByName(records);
1928
- for (const candidate of [
2134
+ for (const candidate of getRefNameCandidates(name)) {
2135
+ const record = byName.get(candidate);
2136
+ if (record != null) return record;
2137
+ }
2138
+ }
2139
+ /** Whether more than one listed ref answers to `name`, which git calls ambiguous and resolves in {@link findRefTip}'s order */
2140
+ function isAmbiguousRefName(records, name) {
2141
+ const byName = getRefRecordsByName(records);
2142
+ let found = false;
2143
+ for (const candidate of getRefNameCandidates(name)) {
2144
+ if (!byName.has(candidate)) continue;
2145
+ if (found) return true;
2146
+ found = true;
2147
+ }
2148
+ return false;
2149
+ }
2150
+ /** The full ref names `name` may stand for, in git's own order for a short name */
2151
+ function getRefNameCandidates(name) {
2152
+ return [
1929
2153
  name,
1930
2154
  `refs/${name}`,
1931
2155
  `refs/tags/${name}`,
1932
2156
  `refs/heads/${name}`,
1933
2157
  `refs/remotes/${name}`,
1934
2158
  `refs/remotes/${name}/HEAD`
1935
- ]) {
1936
- const record = byName.get(candidate);
1937
- if (record != null) return record.peeledObjectname || record.objectname;
1938
- }
2159
+ ];
1939
2160
  }
1940
2161
  function getRefRecordsByName(records) {
1941
2162
  let byName = refRecordsByName.get(records);
@@ -1969,6 +2190,44 @@ function mayHaveUpstream(records, name) {
1969
2190
  if (findRefTip(records, `refs/heads/${name}`) != null) return true;
1970
2191
  return findRefTip(records, name) == null;
1971
2192
  }
2193
+ /** `answer` with the default branch its read resolved, when it consulted one (see `AheadOfBase.defaultBranch`) */
2194
+ function withDefaultBranch(answer, defaultBranch) {
2195
+ return defaultBranch !== void 0 ? {
2196
+ ...answer,
2197
+ defaultBranch
2198
+ } : answer;
2199
+ }
2200
+ /** Splits `git config --null --get-regexp` output into its entries, in order; a key set without a value has none */
2201
+ function parseConfigListing(listing) {
2202
+ const entries = [];
2203
+ for (const entry of listing.split("\0")) {
2204
+ if (!entry) continue;
2205
+ const separator = entry.indexOf("\n");
2206
+ entries.push(separator < 0 ? [entry, void 0] : [entry.substring(0, separator), entry.substring(separator + 1)]);
2207
+ }
2208
+ return entries;
2209
+ }
2210
+ /**
2211
+ * Whether `listing` shows config that can differ by checkout over time: `extensions.worktreeConfig` (each checkout's
2212
+ * `config.worktree`), or a conditional include that is neither `hasconfig:` nor `gitdir:`, since `onbranch:` follows
2213
+ * each checkout's HEAD and an unknown condition might follow anything. A `gitdir:` include matching only some
2214
+ * checkouts is accepted as shared: it is the common identity setup, and treating it as differing would end sharing for
2215
+ * nearly everyone. The cost is a known gap: an edit to a file only those checkouts include goes unseen there, since
2216
+ * nothing watches it, until a change the shared listing does see.
2217
+ */
2218
+ function mayConfigDifferByCheckout(listing) {
2219
+ for (const [key, value] of parseConfigListing(listing)) {
2220
+ if (key === "extensions.worktreeconfig") {
2221
+ if (value == null || !isGitBooleanFalse(value)) return true;
2222
+ continue;
2223
+ }
2224
+ if (!key.startsWith("includeif.")) continue;
2225
+ const condition = key.substring(10, key.lastIndexOf("."));
2226
+ if (condition.startsWith("hasconfig:") || condition.startsWith("gitdir:") || condition.startsWith("gitdir/i:")) continue;
2227
+ return true;
2228
+ }
2229
+ return false;
2230
+ }
1972
2231
  /**
1973
2232
  * Adds the full ref names a `.git/refs` watcher would report for `name`. A short name can't be told tag from local
1974
2233
  * from remote without a spawn, so it adds every form, including the tag git resolves it to first; a spurious one