@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.
- package/CHANGELOG.md +11 -1
- package/dist/git/cache.d.ts +13 -0
- package/dist/git/cache.js +43 -6
- package/dist/git/cache.js.map +1 -1
- package/dist/git/features.d.ts +1 -1
- package/dist/git/features.js +1 -0
- package/dist/git/features.js.map +1 -1
- package/dist/git/providers/branches.d.ts +29 -0
- package/dist/git/service.d.ts +4 -2
- package/dist/git/service.js +6 -4
- package/dist/git/service.js.map +1 -1
- package/dist/git/watching/worktreeSummary.d.ts +19 -4
- package/dist/git/watching/worktreeSummary.js +121 -36
- package/dist/git/watching/worktreeSummary.js.map +1 -1
- package/dist/git-cli/cliGitProvider.js +1 -1
- package/dist/git-cli/providers/branches.d.ts +50 -0
- package/dist/git-cli/providers/branches.js +280 -21
- package/dist/git-cli/providers/branches.js.map +1 -1
- package/dist/git-cli/providers/config.js +3 -2
- package/dist/git-cli/providers/config.js.map +1 -1
- package/dist/git-cli/providers/revision.js +1 -1
- package/dist/git-cli/providers/stash.js +1 -1
- package/dist/git-cli/providers/worktrees.js +23 -6
- package/dist/git-cli/providers/worktrees.js.map +1 -1
- package/dist/git-cli/service.d.ts +3 -0
- package/dist/git-cli/service.js +1 -0
- package/dist/git-cli/service.js.map +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|