@gitkraken/core-gitlens 0.15.7 → 0.16.1
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 +25 -1
- package/dist/git/cache.d.ts +13 -0
- package/dist/git/cache.js +45 -8
- 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/run.types.d.ts +3 -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/classifyChange.js +3 -0
- package/dist/git/watching/classifyChange.js.map +1 -1
- package/dist/git/watching/watchSession.d.ts +2 -0
- package/dist/git/watching/watchSession.js +9 -1
- package/dist/git/watching/watchSession.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.d.ts +1 -1
- package/dist/git-cli/cliGitProvider.js +8 -5
- package/dist/git-cli/cliGitProvider.js.map +1 -1
- package/dist/git-cli/exec/commandClassifier.d.ts +18 -3
- package/dist/git-cli/exec/commandClassifier.js +562 -16
- package/dist/git-cli/exec/commandClassifier.js.map +1 -1
- package/dist/git-cli/exec/exec.d.ts +2 -2
- package/dist/git-cli/exec/git.d.ts +31 -4
- package/dist/git-cli/exec/git.js +105 -43
- package/dist/git-cli/exec/git.js.map +1 -1
- package/dist/git-cli/exec/headState.d.ts +32 -0
- package/dist/git-cli/exec/headState.js +78 -0
- package/dist/git-cli/exec/headState.js.map +1 -0
- package/dist/git-cli/providers/branches.d.ts +50 -0
- package/dist/git-cli/providers/branches.js +294 -25
- 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/operations.d.ts +6 -7
- package/dist/git-cli/providers/operations.js +46 -64
- package/dist/git-cli/providers/operations.js.map +1 -1
- package/dist/git-cli/providers/refs.d.ts +4 -1
- package/dist/git-cli/providers/refs.js +13 -9
- package/dist/git-cli/providers/refs.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/dist/utils/env/node/exec.d.ts +38 -1
- package/dist/utils/env/node/exec.js +14 -6
- package/dist/utils/env/node/exec.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { fileUri, joinUriPath } from "../../utils/uri.js";
|
|
2
|
+
import { FileType } from "../../git/context.js";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
//#region ../git-cli/src/exec/headState.ts
|
|
5
|
+
const headBranchRegex = /^ref:\s*(refs\/heads\/\S+)\s*$/;
|
|
6
|
+
const headShaRegex = /^(?:[0-9a-f]{40}|[0-9a-f]{64})\s*$/;
|
|
7
|
+
const gitFileRegex = /^gitdir:\s*(.+?)\s*$/m;
|
|
8
|
+
/**
|
|
9
|
+
* Reads `repoPath`'s HEAD from the worktree's own `HEAD` file, without running git: read around a write, it says which
|
|
10
|
+
* branch that write moved, whatever a switch outside core left in the caches. The git dir is the cached one, else the
|
|
11
|
+
* one `<repoPath>/.git` is or points to, so a worktree core never registered is read without spawning git either.
|
|
12
|
+
* A reftable repository's `HEAD` file holds the placeholder `refs/heads/.invalid` (its real HEAD is in the tables),
|
|
13
|
+
* which is no branch name git allows, so it reads as unknown, as does a branch that is a symbolic ref to another (a
|
|
14
|
+
* `master` kept pointing at `main`), since git follows it and a write moves the target. An object id is detached only
|
|
15
|
+
* when the git dir's listing doesn't flag `HEAD` as a symlink (a host's `stat` may follow the link): read as detached,
|
|
16
|
+
* a symlinked HEAD would say a write on its branch moved no branch. Never rejects.
|
|
17
|
+
*/
|
|
18
|
+
async function readHeadState(repoPath, fs, cache) {
|
|
19
|
+
try {
|
|
20
|
+
const cached = cache.gitDir.get(repoPath);
|
|
21
|
+
const gitDir = cached != null ? {
|
|
22
|
+
uri: cached.uri,
|
|
23
|
+
commonUri: cached.commonUri
|
|
24
|
+
} : await findGitDir(repoPath, fs);
|
|
25
|
+
if (gitDir == null) return void 0;
|
|
26
|
+
const decoder = new TextDecoder();
|
|
27
|
+
const head = decoder.decode(await fs.readFile(joinUriPath(gitDir.uri, "HEAD")));
|
|
28
|
+
if (headShaRegex.test(head)) return (await fs.readDirectory(gitDir.uri)).some(([name, type]) => name === "HEAD" && (type & FileType.SymbolicLink) !== 0) ? void 0 : "detached";
|
|
29
|
+
const ref = headBranchRegex.exec(head)?.[1];
|
|
30
|
+
if (ref == null || ref.split("/").some((part) => part.startsWith("."))) return void 0;
|
|
31
|
+
let target;
|
|
32
|
+
try {
|
|
33
|
+
target = decoder.decode(await fs.readFile(joinUriPath(gitDir.commonUri ?? gitDir.uri, ref)));
|
|
34
|
+
} catch {}
|
|
35
|
+
return target?.startsWith("ref:") ? void 0 : { branch: ref };
|
|
36
|
+
} catch {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The git dir `<repoPath>/.git` is: the folder itself, or the one a linked worktree's `gitdir:` file names, whose
|
|
42
|
+
* `commondir` file names the repository's shared one
|
|
43
|
+
*/
|
|
44
|
+
async function findGitDir(repoPath, fs) {
|
|
45
|
+
const dotGit = joinUriPath(fileUri(repoPath), ".git");
|
|
46
|
+
const stat = await fs.stat(dotGit);
|
|
47
|
+
if (stat == null) return void 0;
|
|
48
|
+
if (stat.type & FileType.Directory) return {
|
|
49
|
+
uri: dotGit,
|
|
50
|
+
commonUri: void 0
|
|
51
|
+
};
|
|
52
|
+
const dir = gitFileRegex.exec(new TextDecoder().decode(await fs.readFile(dotGit)))?.[1];
|
|
53
|
+
if (dir == null) return void 0;
|
|
54
|
+
const gitDir = resolve(repoPath, dir);
|
|
55
|
+
let commonDir;
|
|
56
|
+
try {
|
|
57
|
+
commonDir = new TextDecoder().decode(await fs.readFile(joinUriPath(fileUri(gitDir), "commondir"))).trim();
|
|
58
|
+
} catch {}
|
|
59
|
+
return {
|
|
60
|
+
uri: fileUri(gitDir),
|
|
61
|
+
commonUri: commonDir ? fileUri(resolve(gitDir, commonDir)) : void 0
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Which branch a write that moves only HEAD's branch moved, from HEAD read before and after it: that branch when HEAD
|
|
66
|
+
* was on the same one both times, `'detached'` (no branch) when it was detached both times, otherwise `undefined` — a
|
|
67
|
+
* hook or an outside switch moved HEAD meanwhile, or a read failed, so the moved branch is unknown.
|
|
68
|
+
*/
|
|
69
|
+
function getMovedHead(before, after) {
|
|
70
|
+
if (before == null || after == null) return void 0;
|
|
71
|
+
if (before === "detached") return after === "detached" ? "detached" : void 0;
|
|
72
|
+
if (after === "detached") return void 0;
|
|
73
|
+
return before.branch === after.branch ? before : void 0;
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
76
|
+
export { getMovedHead, readHeadState };
|
|
77
|
+
|
|
78
|
+
//# sourceMappingURL=headState.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"headState.js","names":["resolvePath"],"sources":["../../../../git-cli/src/exec/headState.ts"],"sourcesContent":["import { resolve as resolvePath } from 'node:path';\nimport type { Cache } from '@gitlens/git/cache.js';\nimport type { FileSystemProvider } from '@gitlens/git/context.js';\nimport { FileType } from '@gitlens/git/context.js';\nimport type { Uri } from '@gitlens/utils/uri.js';\nimport { fileUri, joinUriPath } from '@gitlens/utils/uri.js';\n\n/**\n * What a worktree's HEAD is on: the full name of its branch (`refs/heads/main`), `'detached'`, or `undefined` when\n * unknown — unreadable, not a branch name git allows, or a HEAD kept as a symlink to its branch\n * (`core.preferSymlinkRefs`). Reading such a HEAD follows the link to the branch's object id, as a detached HEAD\n * reads, so it is unknown when the git dir's listing flags it as a link, and `'detached'` on a host whose listing\n * doesn't.\n */\nexport type HeadState = { readonly branch: string } | 'detached' | undefined;\n\nconst headBranchRegex = /^ref:\\s*(refs\\/heads\\/\\S+)\\s*$/;\nconst headShaRegex = /^(?:[0-9a-f]{40}|[0-9a-f]{64})\\s*$/;\nconst gitFileRegex = /^gitdir:\\s*(.+?)\\s*$/m;\n\n/**\n * Reads `repoPath`'s HEAD from the worktree's own `HEAD` file, without running git: read around a write, it says which\n * branch that write moved, whatever a switch outside core left in the caches. The git dir is the cached one, else the\n * one `<repoPath>/.git` is or points to, so a worktree core never registered is read without spawning git either.\n * A reftable repository's `HEAD` file holds the placeholder `refs/heads/.invalid` (its real HEAD is in the tables),\n * which is no branch name git allows, so it reads as unknown, as does a branch that is a symbolic ref to another (a\n * `master` kept pointing at `main`), since git follows it and a write moves the target. An object id is detached only\n * when the git dir's listing doesn't flag `HEAD` as a symlink (a host's `stat` may follow the link): read as detached,\n * a symlinked HEAD would say a write on its branch moved no branch. Never rejects.\n */\nexport async function readHeadState(repoPath: string, fs: FileSystemProvider, cache: Cache): Promise<HeadState> {\n\ttry {\n\t\tconst cached = cache.gitDir.get(repoPath);\n\t\tconst gitDir =\n\t\t\tcached != null ? { uri: cached.uri, commonUri: cached.commonUri } : await findGitDir(repoPath, fs);\n\t\tif (gitDir == null) return undefined;\n\n\t\tconst decoder = new TextDecoder();\n\t\tconst head = decoder.decode(await fs.readFile(joinUriPath(gitDir.uri, 'HEAD')));\n\t\tif (headShaRegex.test(head)) {\n\t\t\tconst entries = await fs.readDirectory(gitDir.uri);\n\t\t\tconst linked = entries.some(([name, type]) => name === 'HEAD' && (type & FileType.SymbolicLink) !== 0);\n\t\t\treturn linked ? undefined : 'detached';\n\t\t}\n\n\t\tconst ref = headBranchRegex.exec(head)?.[1];\n\t\tif (ref == null || ref.split('/').some(part => part.startsWith('.'))) return undefined;\n\n\t\t// Packed refs can't hold a symbolic ref, so a missing loose file is a plain branch\n\t\tlet target: string | undefined;\n\t\ttry {\n\t\t\ttarget = decoder.decode(await fs.readFile(joinUriPath(gitDir.commonUri ?? gitDir.uri, ref)));\n\t\t} catch {}\n\t\treturn target?.startsWith('ref:') ? undefined : { branch: ref };\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/**\n * The git dir `<repoPath>/.git` is: the folder itself, or the one a linked worktree's `gitdir:` file names, whose\n * `commondir` file names the repository's shared one\n */\nasync function findGitDir(\n\trepoPath: string,\n\tfs: FileSystemProvider,\n): Promise<{ readonly uri: Uri; readonly commonUri: Uri | undefined } | undefined> {\n\tconst dotGit = joinUriPath(fileUri(repoPath), '.git');\n\tconst stat = await fs.stat(dotGit);\n\tif (stat == null) return undefined;\n\tif (stat.type & FileType.Directory) return { uri: dotGit, commonUri: undefined };\n\n\tconst dir = gitFileRegex.exec(new TextDecoder().decode(await fs.readFile(dotGit)))?.[1];\n\tif (dir == null) return undefined;\n\n\tconst gitDir = resolvePath(repoPath, dir);\n\tlet commonDir: string | undefined;\n\ttry {\n\t\tcommonDir = new TextDecoder().decode(await fs.readFile(joinUriPath(fileUri(gitDir), 'commondir'))).trim();\n\t} catch {}\n\treturn { uri: fileUri(gitDir), commonUri: commonDir ? fileUri(resolvePath(gitDir, commonDir)) : undefined };\n}\n\n/**\n * Which branch a write that moves only HEAD's branch moved, from HEAD read before and after it: that branch when HEAD\n * was on the same one both times, `'detached'` (no branch) when it was detached both times, otherwise `undefined` — a\n * hook or an outside switch moved HEAD meanwhile, or a read failed, so the moved branch is unknown.\n */\nexport function getMovedHead(before: HeadState, after: HeadState): HeadState {\n\tif (before == null || after == null) return undefined;\n\tif (before === 'detached') return after === 'detached' ? 'detached' : undefined;\n\tif (after === 'detached') return undefined;\n\n\treturn before.branch === after.branch ? before : undefined;\n}\n"],"mappings":";;;;AAgBA,MAAM,kBAAkB;AACxB,MAAM,eAAe;AACrB,MAAM,eAAe;;;;;;;;;;;AAYrB,eAAsB,cAAc,UAAkB,IAAwB,OAAkC;CAC/G,IAAI;EACH,MAAM,SAAS,MAAM,OAAO,IAAI,QAAQ;EACxC,MAAM,SACL,UAAU,OAAO;GAAE,KAAK,OAAO;GAAK,WAAW,OAAO;EAAU,IAAI,MAAM,WAAW,UAAU,EAAE;EAClG,IAAI,UAAU,MAAM,OAAO,KAAA;EAE3B,MAAM,UAAU,IAAI,YAAY;EAChC,MAAM,OAAO,QAAQ,OAAO,MAAM,GAAG,SAAS,YAAY,OAAO,KAAK,MAAM,CAAC,CAAC;EAC9E,IAAI,aAAa,KAAK,IAAI,GAGzB,QADe,MADO,GAAG,cAAc,OAAO,GAAG,EAAA,CAC1B,MAAM,CAAC,MAAM,UAAU,SAAS,WAAW,OAAO,SAAS,kBAAkB,CACxF,IAAI,KAAA,IAAY;EAG7B,MAAM,MAAM,gBAAgB,KAAK,IAAI,CAAC,GAAG;EACzC,IAAI,OAAO,QAAQ,IAAI,MAAM,GAAG,CAAC,CAAC,MAAK,SAAQ,KAAK,WAAW,GAAG,CAAC,GAAG,OAAO,KAAA;EAG7E,IAAI;EACJ,IAAI;GACH,SAAS,QAAQ,OAAO,MAAM,GAAG,SAAS,YAAY,OAAO,aAAa,OAAO,KAAK,GAAG,CAAC,CAAC;EAC5F,QAAQ,CAAC;EACT,OAAO,QAAQ,WAAW,MAAM,IAAI,KAAA,IAAY,EAAE,QAAQ,IAAI;CAC/D,QAAQ;EACP;CACD;AACD;;;;;AAMA,eAAe,WACd,UACA,IACkF;CAClF,MAAM,SAAS,YAAY,QAAQ,QAAQ,GAAG,MAAM;CACpD,MAAM,OAAO,MAAM,GAAG,KAAK,MAAM;CACjC,IAAI,QAAQ,MAAM,OAAO,KAAA;CACzB,IAAI,KAAK,OAAO,SAAS,WAAW,OAAO;EAAE,KAAK;EAAQ,WAAW,KAAA;CAAU;CAE/E,MAAM,MAAM,aAAa,KAAK,IAAI,YAAY,CAAC,CAAC,OAAO,MAAM,GAAG,SAAS,MAAM,CAAC,CAAC,CAAC,GAAG;CACrF,IAAI,OAAO,MAAM,OAAO,KAAA;CAExB,MAAM,SAASA,QAAY,UAAU,GAAG;CACxC,IAAI;CACJ,IAAI;EACH,YAAY,IAAI,YAAY,CAAC,CAAC,OAAO,MAAM,GAAG,SAAS,YAAY,QAAQ,MAAM,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC,KAAK;CACzG,QAAQ,CAAC;CACT,OAAO;EAAE,KAAK,QAAQ,MAAM;EAAG,WAAW,YAAY,QAAQA,QAAY,QAAQ,SAAS,CAAC,IAAI,KAAA;CAAU;AAC3G;;;;;;AAOA,SAAgB,aAAa,QAAmB,OAA6B;CAC5E,IAAI,UAAU,QAAQ,SAAS,MAAM,OAAO,KAAA;CAC5C,IAAI,WAAW,YAAY,OAAO,UAAU,aAAa,aAAa,KAAA;CACtE,IAAI,UAAU,YAAY,OAAO,KAAA;CAEjC,OAAO,OAAO,WAAW,MAAM,SAAS,SAAS,KAAA;AAClD"}
|
|
@@ -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";
|
|
@@ -13,6 +14,7 @@ import { createReference } from "../../git/utils/reference.utils.js";
|
|
|
13
14
|
import __decorate from "../../_virtual/@oxc-project+runtime/helpers/esm/decorate.js";
|
|
14
15
|
import { BranchError, GitWarnings } from "../../git/errors.js";
|
|
15
16
|
import { mayWriteTracking } from "../exec/commandClassifier.js";
|
|
17
|
+
import { readHeadState } from "../exec/headState.js";
|
|
16
18
|
import { KeyedSequencer } from "../exec/indexLock.js";
|
|
17
19
|
import { GitError, GitErrors, getGitCommandError, gitConfigsBranch, gitConfigsLog } from "../exec/git.js";
|
|
18
20
|
import { maybeStopWatch } from "../../utils/stopwatch.js";
|
|
@@ -24,6 +26,7 @@ import { LruMap } from "../../utils/lruMap.js";
|
|
|
24
26
|
import { PageableResult, emptyPagedResult } from "../../utils/paging.js";
|
|
25
27
|
import { parseMergeTreeConflict } from "../parsers/mergeTreeParser.js";
|
|
26
28
|
import { resolveBranchRemote } from "./branchRemote.js";
|
|
29
|
+
import { isGitBooleanFalse } from "./config.js";
|
|
27
30
|
//#region ../git-cli/src/providers/branches.ts
|
|
28
31
|
/** Minimum time between writes to the config for last accessed/modified dates */
|
|
29
32
|
const dateMetadataStaleThresholdMs = 3e5;
|
|
@@ -47,6 +50,12 @@ const branchActivityKeys = [
|
|
|
47
50
|
"gk-last-modified",
|
|
48
51
|
"gk-agent-last-activity"
|
|
49
52
|
];
|
|
53
|
+
/**
|
|
54
|
+
* The most commits a base's reflog may reach off its new tip's history for a re-measure: every worktree's count
|
|
55
|
+
* excludes each of them on its command line, and more come only from a long history force-pushed away, where a full
|
|
56
|
+
* read costs less
|
|
57
|
+
*/
|
|
58
|
+
const maxOffHistoryCommits = 64;
|
|
50
59
|
var BranchesGitSubProvider = class {
|
|
51
60
|
context;
|
|
52
61
|
git;
|
|
@@ -55,6 +64,13 @@ var BranchesGitSubProvider = class {
|
|
|
55
64
|
/** Ahead/behind counts by `<baseSha>...<tipSha>` range, from the per-tip fallback; a pair of commits always counts the same */
|
|
56
65
|
aheadBehindBySha = new LruMap(4096);
|
|
57
66
|
/**
|
|
67
|
+
* For a base that moved only forward, the commits its reflog reaches off the new tip's history (else `false`), by
|
|
68
|
+
* `<ref>\0<oldSha>\0<newSha>`, for each cached ref list it was read against: a ref write replaces that list, along
|
|
69
|
+
* with the reflog the answer came from. `undefined` when git couldn't say, which isn't kept. A fetch moves one base
|
|
70
|
+
* under every worktree at once, so they share the one check, joining it while it runs
|
|
71
|
+
*/
|
|
72
|
+
baseMovedForward = /* @__PURE__ */ new WeakMap();
|
|
73
|
+
/**
|
|
58
74
|
* When each `commonPath\0branch\0key` was last stamped by this process. Bounded: a dropped entry only costs a
|
|
59
75
|
* read of the stored stamp, which still holds off a write under 5 minutes old
|
|
60
76
|
*/
|
|
@@ -69,6 +85,12 @@ var BranchesGitSubProvider = class {
|
|
|
69
85
|
_branchCreatedFrom = new LruMap(1024);
|
|
70
86
|
/** Bumped each time a key's created-from is forgotten, so a reflog read in flight across that doesn't store it */
|
|
71
87
|
_branchCreatedFromGenerations = new LruMap(1024);
|
|
88
|
+
/**
|
|
89
|
+
* The repositories (by common path) whose last config listing showed config that can differ by checkout, so a linked
|
|
90
|
+
* checkout reads its own listing without first waiting on the main checkout's. Only a shortcut: a listing that shows
|
|
91
|
+
* otherwise drops the repository again
|
|
92
|
+
*/
|
|
93
|
+
_configDiffersByCheckout = /* @__PURE__ */ new Set();
|
|
72
94
|
constructor(context, git, cache, provider) {
|
|
73
95
|
this.context = context;
|
|
74
96
|
this.git = git;
|
|
@@ -742,14 +764,23 @@ var BranchesGitSubProvider = class {
|
|
|
742
764
|
const records = await this.provider.refs.getRefs(repoPath, cancellation);
|
|
743
765
|
if (!records.length) return { known: false };
|
|
744
766
|
if (branch == null) {
|
|
745
|
-
const
|
|
746
|
-
if (
|
|
747
|
-
if (current.remote || isDetachedHead(current.name)) return {
|
|
767
|
+
const head = await readHeadState(repoPath, this.context.fs, this.cache);
|
|
768
|
+
if (head === "detached") return {
|
|
748
769
|
known: true,
|
|
749
770
|
push: void 0,
|
|
750
771
|
upstream: void 0
|
|
751
772
|
};
|
|
752
|
-
branch =
|
|
773
|
+
if (head != null) branch = head.branch;
|
|
774
|
+
else {
|
|
775
|
+
const current = await this.getCurrentBranchReference(repoPath, cancellation);
|
|
776
|
+
if (current == null) return { known: false };
|
|
777
|
+
if (current.remote || isDetachedHead(current.name)) return {
|
|
778
|
+
known: true,
|
|
779
|
+
push: void 0,
|
|
780
|
+
upstream: void 0
|
|
781
|
+
};
|
|
782
|
+
branch = current.name;
|
|
783
|
+
}
|
|
753
784
|
}
|
|
754
785
|
const record = findRefRecord(records, branch.startsWith("refs/heads/") ? branch : `refs/heads/${branch}`);
|
|
755
786
|
return {
|
|
@@ -1129,20 +1160,50 @@ var BranchesGitSubProvider = class {
|
|
|
1129
1160
|
return name;
|
|
1130
1161
|
}
|
|
1131
1162
|
getAheadOfBaseConfigKey(repoPath, cancellation) {
|
|
1163
|
+
return this.getAheadOfBaseConfigListing(repoPath, cancellation);
|
|
1164
|
+
}
|
|
1165
|
+
async getAheadOfBaseConfigEntries(repoPath, cancellation) {
|
|
1166
|
+
const listing = await this.getAheadOfBaseConfigListing(repoPath, cancellation);
|
|
1167
|
+
return listing != null ? parseConfigListing(listing) : void 0;
|
|
1168
|
+
}
|
|
1169
|
+
/**
|
|
1170
|
+
* A checkout's listing of the config inputs to {@link getAheadOfBase}. Every checkout of a repository shares the main
|
|
1171
|
+
* checkout's, unless it shows config that can differ by checkout (see `mayConfigDifferByCheckout`); then
|
|
1172
|
+
* each linked checkout reads its own, and keeps doing so until one of its own reads shows config no longer can.
|
|
1173
|
+
*/
|
|
1174
|
+
getAheadOfBaseConfigListing(repoPath, cancellation) {
|
|
1132
1175
|
return this.cache.getAheadOfBaseConfigKey(repoPath, async (checkoutPath, cacheable, signal) => {
|
|
1133
|
-
const
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1176
|
+
const commonPath = this.cache.getCommonPath(checkoutPath);
|
|
1177
|
+
const isMain = checkoutPath === commonPath;
|
|
1178
|
+
if (isMain || !this._configDiffersByCheckout.has(commonPath)) {
|
|
1179
|
+
const shared = await this.cache.getAheadOfBaseConfigListing(checkoutPath, (path, sharedCacheable, sharedSignal) => this.readAheadOfBaseConfigListing(path, sharedCacheable, sharedSignal), signal);
|
|
1180
|
+
if (isMain) {
|
|
1181
|
+
if (shared == null) cacheable.invalidate();
|
|
1182
|
+
return shared;
|
|
1183
|
+
}
|
|
1184
|
+
if (shared != null) {
|
|
1185
|
+
if (!mayConfigDifferByCheckout(shared)) return shared;
|
|
1186
|
+
this._configDiffersByCheckout.add(commonPath);
|
|
1187
|
+
}
|
|
1142
1188
|
}
|
|
1143
|
-
|
|
1189
|
+
const own = await this.readAheadOfBaseConfigListing(checkoutPath, cacheable, signal);
|
|
1190
|
+
if (own != null && !mayConfigDifferByCheckout(own)) this._configDiffersByCheckout.delete(commonPath);
|
|
1191
|
+
return own;
|
|
1144
1192
|
}, cancellation);
|
|
1145
1193
|
}
|
|
1194
|
+
async readAheadOfBaseConfigListing(cwd, cacheable, cancellation) {
|
|
1195
|
+
const result = await this.git.run({
|
|
1196
|
+
cwd,
|
|
1197
|
+
cancellation,
|
|
1198
|
+
expectedExitCodes: [1]
|
|
1199
|
+
}, "config", "--null", "--get-regexp", "^(branch\\..*\\.(remote|merge)|remote\\..*|init\\.defaultbranch|includeif\\..*|extensions\\.worktreeconfig)$");
|
|
1200
|
+
if (cancellation?.aborted) throw new CancellationError();
|
|
1201
|
+
if (result.completion.status !== "exited") {
|
|
1202
|
+
cacheable.invalidate();
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
return result.stdout;
|
|
1206
|
+
}
|
|
1146
1207
|
async getAheadOfBase(repoPath, branch, options, cancellation) {
|
|
1147
1208
|
if (branch != null && isDetachedHead(branch)) branch = void 0;
|
|
1148
1209
|
const dependsOn = new Set(branch != null ? [`refs/heads/${branch}`] : []);
|
|
@@ -1154,6 +1215,152 @@ var BranchesGitSubProvider = class {
|
|
|
1154
1215
|
if (target != null) return this.measureAheadOfBase(repoPath, target, "preferred", dependsOn, cancellation);
|
|
1155
1216
|
return this.getAheadOfBaseInOrder(repoPath, branch, options?.skipUserTarget, dependsOn, cancellation);
|
|
1156
1217
|
}
|
|
1218
|
+
async getAheadOfBaseAfterBaseMove(repoPath, branch, previous, moved, cancellation) {
|
|
1219
|
+
const { head, base } = previous;
|
|
1220
|
+
const oldTip = base.sha;
|
|
1221
|
+
if (oldTip == null || !moved.length) return void 0;
|
|
1222
|
+
if (base.tier === "preferred" || base.tier === "upstream" && base.forkPoint === oldTip) return void 0;
|
|
1223
|
+
try {
|
|
1224
|
+
const records = await this.provider.refs.getRefs(repoPath, cancellation);
|
|
1225
|
+
const record = records.length ? resolveRefRecord(records, base.ref) : void 0;
|
|
1226
|
+
if (record == null || !moved.includes(record.name)) return void 0;
|
|
1227
|
+
if (isAmbiguousRefName(records, base.ref)) return void 0;
|
|
1228
|
+
const newTip = record.peeledObjectname || record.objectname;
|
|
1229
|
+
if (!newTip || newTip === oldTip) return void 0;
|
|
1230
|
+
if (branch != null && !isDetachedHead(branch) && findRefTip(records, `refs/heads/${branch}`) !== head) return;
|
|
1231
|
+
let remoteHeadMoved = false;
|
|
1232
|
+
for (const ref of moved) {
|
|
1233
|
+
if (ref === record.name) continue;
|
|
1234
|
+
if (findRefTip(records, ref) !== newTip || !await this.isRemoteHeadOf(repoPath, ref, record.name, cancellation)) return;
|
|
1235
|
+
remoteHeadMoved = true;
|
|
1236
|
+
}
|
|
1237
|
+
if (remoteHeadMoved && previous.defaultBranch === void 0) return void 0;
|
|
1238
|
+
if (previous.defaultBranch !== void 0) {
|
|
1239
|
+
const defaultBranch = await this.getLocalDefaultBranchName(repoPath, /* @__PURE__ */ new Set(), cancellation);
|
|
1240
|
+
if (cancellation?.aborted) throw new CancellationError();
|
|
1241
|
+
if ((defaultBranch ?? null) !== previous.defaultBranch) return void 0;
|
|
1242
|
+
}
|
|
1243
|
+
if (previous.dependsOn.some((r) => r.startsWith("refs/remotes/upstream/") && !r.endsWith("/HEAD"))) for (const ref of previous.dependsOn) {
|
|
1244
|
+
if (ref === record.name || ref.endsWith("/HEAD")) continue;
|
|
1245
|
+
const tip = findRefTip(records, ref);
|
|
1246
|
+
if (tip === oldTip || tip === newTip) return void 0;
|
|
1247
|
+
}
|
|
1248
|
+
if (await this.isShallowRepository(repoPath) !== false) return void 0;
|
|
1249
|
+
const offHistory = await this.getMovedForward(repoPath, records, record.name, oldTip, newTip);
|
|
1250
|
+
if (cancellation?.aborted) throw new CancellationError();
|
|
1251
|
+
if (!offHistory) return void 0;
|
|
1252
|
+
const result = await this.git.run({
|
|
1253
|
+
cwd: repoPath,
|
|
1254
|
+
cancellation,
|
|
1255
|
+
errors: "ignore"
|
|
1256
|
+
}, "rev-list", "--count", head, "--not", newTip, ...base.forkPoint != null ? [base.forkPoint] : [], ...offHistory, "--");
|
|
1257
|
+
if (cancellation?.aborted) throw new CancellationError();
|
|
1258
|
+
if (result.completion.status !== "exited" || result.exitCode !== 0) return void 0;
|
|
1259
|
+
if (parseInt(result.stdout.trim(), 10) !== base.ahead) return void 0;
|
|
1260
|
+
return withDefaultBranch({
|
|
1261
|
+
base: {
|
|
1262
|
+
...base,
|
|
1263
|
+
sha: newTip
|
|
1264
|
+
},
|
|
1265
|
+
dependsOn: [...previous.dependsOn]
|
|
1266
|
+
}, previous.defaultBranch);
|
|
1267
|
+
} catch (ex) {
|
|
1268
|
+
if (isCancellationError(ex)) throw ex;
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
/**
|
|
1273
|
+
* Whether the repository is shallow, by its common git directory's `shallow` file, which spawns nothing; `undefined`
|
|
1274
|
+
* when it can't be read
|
|
1275
|
+
*/
|
|
1276
|
+
async isShallowRepository(repoPath) {
|
|
1277
|
+
try {
|
|
1278
|
+
const gitDir = await this.provider.config.getGitDir(repoPath);
|
|
1279
|
+
return await this.context.fs.stat(joinUriPath(gitDir.commonUri ?? gitDir.uri, "shallow")) != null;
|
|
1280
|
+
} catch {
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
/** Whether `ref` is a `refs/remotes/<remote>/HEAD` symref pointing at `target`, from the cached local symref */
|
|
1285
|
+
async isRemoteHeadOf(repoPath, ref, target, cancellation) {
|
|
1286
|
+
if (!ref.startsWith("refs/remotes/") || !ref.endsWith("/HEAD")) return false;
|
|
1287
|
+
const remote = ref.slice(13, -5);
|
|
1288
|
+
if (!remote) return false;
|
|
1289
|
+
const symref = await this.getDefaultBranchName(repoPath, remote, { local: true }, cancellation);
|
|
1290
|
+
return symref != null && `refs/remotes/${symref}` === target;
|
|
1291
|
+
}
|
|
1292
|
+
/**
|
|
1293
|
+
* For `ref` (a full name in `records`, the cached ref list) moving from `oldTip` to `newTip`, the commits its reflog
|
|
1294
|
+
* reaches off `newTip`'s history when it moved only forward: `oldTip` and every value its reflog took after it are in
|
|
1295
|
+
* `newTip`'s history. `--fork-point` reads every reflog entry, so one in between that shares more of HEAD's history
|
|
1296
|
+
* than `newTip` does (a branch's commit that landed and was force-pushed away) moves the fork point while the count
|
|
1297
|
+
* stands; an older one off `newTip`'s history may hold such a commit too, which each worktree's count checks for.
|
|
1298
|
+
* Without a reflog it reads only the tip. `undefined` when it didn't move only forward, or git can't say.
|
|
1299
|
+
*/
|
|
1300
|
+
async getMovedForward(repoPath, records, ref, oldTip, newTip) {
|
|
1301
|
+
let checks = this.baseMovedForward.get(records);
|
|
1302
|
+
if (checks == null) {
|
|
1303
|
+
checks = /* @__PURE__ */ new Map();
|
|
1304
|
+
this.baseMovedForward.set(records, checks);
|
|
1305
|
+
}
|
|
1306
|
+
const key = `${ref}\0${oldTip}\0${newTip}`;
|
|
1307
|
+
let check = checks.get(key);
|
|
1308
|
+
if (check == null) {
|
|
1309
|
+
check = this.readMovedForward(this.cache.getCommonPath(repoPath), ref, oldTip, newTip);
|
|
1310
|
+
checks.set(key, check);
|
|
1311
|
+
}
|
|
1312
|
+
const offHistory = await check;
|
|
1313
|
+
if (offHistory == null && checks.get(key) === check) checks.delete(key);
|
|
1314
|
+
return offHistory || void 0;
|
|
1315
|
+
}
|
|
1316
|
+
/**
|
|
1317
|
+
* Reads {@link getMovedForward} from git, in `commonPath`, which holds the refs, their reflogs and the objects for
|
|
1318
|
+
* every worktree. Not bound to one caller's cancellation, since every worktree the move reached rides this one
|
|
1319
|
+
* read. `false` when the ref didn't move only forward, or reaches too much history off `newTip`'s to tell;
|
|
1320
|
+
* `undefined` when git couldn't say.
|
|
1321
|
+
*/
|
|
1322
|
+
async readMovedForward(commonPath, ref, oldTip, newTip) {
|
|
1323
|
+
let since = /* @__PURE__ */ new Set([oldTip]);
|
|
1324
|
+
let logged = /* @__PURE__ */ new Set([oldTip]);
|
|
1325
|
+
try {
|
|
1326
|
+
const entries = await this.provider.refs.getReflogEntries(commonPath, ref);
|
|
1327
|
+
if (entries.length) {
|
|
1328
|
+
if (entries[0].sha !== newTip) return false;
|
|
1329
|
+
const oldest = entries.findLastIndex((e) => e.sha === oldTip);
|
|
1330
|
+
if (oldest < 0) return false;
|
|
1331
|
+
since = new Set(entries.slice(1, oldest + 1).map((e) => e.sha));
|
|
1332
|
+
logged = new Set(entries.map((e) => e.sha));
|
|
1333
|
+
const before = await this.git.run({
|
|
1334
|
+
cwd: commonPath,
|
|
1335
|
+
errors: "ignore"
|
|
1336
|
+
}, "rev-parse", "--verify", `${ref}@{${entries.length}}`);
|
|
1337
|
+
if (before.completion.status !== "exited") return void 0;
|
|
1338
|
+
if (before.exitCode === 0) {
|
|
1339
|
+
const sha = before.stdout.trim();
|
|
1340
|
+
if (!sha) return void 0;
|
|
1341
|
+
logged.add(sha);
|
|
1342
|
+
} else if (!before.stderr?.includes(" only has ")) return;
|
|
1343
|
+
else if (!await this.git.supports("git:rev-parse:reflog-oldest-old-value")) return false;
|
|
1344
|
+
logged.delete(newTip);
|
|
1345
|
+
}
|
|
1346
|
+
} catch {
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
try {
|
|
1350
|
+
const result = await this.git.run({
|
|
1351
|
+
cwd: commonPath,
|
|
1352
|
+
errors: "ignore",
|
|
1353
|
+
stdin: `${[...logged, `^${newTip}`].join("\n")}\n`
|
|
1354
|
+
}, "rev-list", `--max-count=65`, "--stdin");
|
|
1355
|
+
if (result.completion.status !== "exited" || result.exitCode !== 0) return void 0;
|
|
1356
|
+
const off = result.stdout.split("\n").filter((sha) => sha.length > 0);
|
|
1357
|
+
if (off.length > maxOffHistoryCommits) return false;
|
|
1358
|
+
if (off.some((sha) => since.has(sha))) return false;
|
|
1359
|
+
return off;
|
|
1360
|
+
} catch {
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1157
1364
|
/**
|
|
1158
1365
|
* Resolves the base from the recorded candidates, then the branch it was created from or the default branch, for
|
|
1159
1366
|
* {@link getAheadOfBase}
|
|
@@ -1161,6 +1368,7 @@ var BranchesGitSubProvider = class {
|
|
|
1161
1368
|
async getAheadOfBaseInOrder(repoPath, branch, skipUserTarget, dependsOn, cancellation) {
|
|
1162
1369
|
let target;
|
|
1163
1370
|
let tier = "default";
|
|
1371
|
+
let resolvedDefault;
|
|
1164
1372
|
if (branch != null) for (const candidate of await this.getRecordedBaseCandidates(repoPath, branch, skipUserTarget)) {
|
|
1165
1373
|
target = await this.getNearestForkTarget(repoPath, branch, candidate.ref, dependsOn, cancellation);
|
|
1166
1374
|
if (target != null) {
|
|
@@ -1170,7 +1378,8 @@ var BranchesGitSubProvider = class {
|
|
|
1170
1378
|
}
|
|
1171
1379
|
if (target == null) {
|
|
1172
1380
|
const defaultBranch = await this.getLocalDefaultBranchName(repoPath, dependsOn, cancellation);
|
|
1173
|
-
|
|
1381
|
+
resolvedDefault = defaultBranch ?? null;
|
|
1382
|
+
if (branch != null && defaultBranch === branch) return withDefaultBranch(await this.measureAheadOfSelfUpstream(repoPath, branch, dependsOn, cancellation), resolvedDefault);
|
|
1174
1383
|
const createdFrom = branch != null ? await this.getCreatedFromTarget(repoPath, branch, cancellation) : void 0;
|
|
1175
1384
|
if (createdFrom != null) {
|
|
1176
1385
|
const created = await this.getCreatedFromForkTarget(repoPath, branch, createdFrom, defaultBranch, dependsOn, cancellation);
|
|
@@ -1185,18 +1394,18 @@ var BranchesGitSubProvider = class {
|
|
|
1185
1394
|
} catch (ex) {
|
|
1186
1395
|
if (isCancellationError(ex)) throw ex;
|
|
1187
1396
|
}
|
|
1188
|
-
return {
|
|
1397
|
+
return withDefaultBranch({
|
|
1189
1398
|
base: void 0,
|
|
1190
1399
|
reason: unborn ? "unborn" : "none",
|
|
1191
1400
|
dependsOn: [...dependsOn]
|
|
1192
|
-
};
|
|
1401
|
+
}, resolvedDefault);
|
|
1193
1402
|
}
|
|
1194
|
-
if (branch != null && await this.isSelfAtHead(repoPath, target.ref, branch, cancellation)) return {
|
|
1403
|
+
if (branch != null && await this.isSelfAtHead(repoPath, target.ref, branch, cancellation)) return withDefaultBranch({
|
|
1195
1404
|
base: void 0,
|
|
1196
1405
|
reason: "self",
|
|
1197
1406
|
dependsOn: [...dependsOn]
|
|
1198
|
-
};
|
|
1199
|
-
return this.measureAheadOfBase(repoPath, target, tier, dependsOn, cancellation);
|
|
1407
|
+
}, resolvedDefault);
|
|
1408
|
+
return withDefaultBranch(await this.measureAheadOfBase(repoPath, target, tier, dependsOn, cancellation), resolvedDefault);
|
|
1200
1409
|
}
|
|
1201
1410
|
/**
|
|
1202
1411
|
* The base for a branch created from `createdFrom`: that branch, unless the default branch forked strictly nearer
|
|
@@ -1895,7 +2104,9 @@ __decorate([debug()], BranchesGitSubProvider.prototype, "getPotentialMergeConfli
|
|
|
1895
2104
|
__decorate([debug()], BranchesGitSubProvider.prototype, "getPotentialStashReapplyConflicts", null);
|
|
1896
2105
|
__decorate([debug({ exit: true })], BranchesGitSubProvider.prototype, "getBaseBranchName", null);
|
|
1897
2106
|
__decorate([trace()], BranchesGitSubProvider.prototype, "getAheadOfBaseConfigKey", null);
|
|
2107
|
+
__decorate([trace()], BranchesGitSubProvider.prototype, "getAheadOfBaseConfigEntries", null);
|
|
1898
2108
|
__decorate([debug({ exit: true })], BranchesGitSubProvider.prototype, "getAheadOfBase", null);
|
|
2109
|
+
__decorate([debug({ exit: true })], BranchesGitSubProvider.prototype, "getAheadOfBaseAfterBaseMove", null);
|
|
1899
2110
|
__decorate([debug({ exit: true })], BranchesGitSubProvider.prototype, "getAheadBehindCounts", null);
|
|
1900
2111
|
__decorate([debug({ exit: true })], BranchesGitSubProvider.prototype, "getMergedBranches", null);
|
|
1901
2112
|
__decorate([debug({ exit: true })], BranchesGitSubProvider.prototype, "getStoredMergeTargetBranchName", null);
|
|
@@ -1924,18 +2135,38 @@ const refRecordsByName = /* @__PURE__ */ new WeakMap();
|
|
|
1924
2135
|
* `refs/heads/` before `refs/remotes/`); annotated tags peel to their commit. `undefined` when no listed ref matches.
|
|
1925
2136
|
*/
|
|
1926
2137
|
function findRefTip(records, name) {
|
|
2138
|
+
const record = resolveRefRecord(records, name);
|
|
2139
|
+
return record != null ? record.peeledObjectname || record.objectname : void 0;
|
|
2140
|
+
}
|
|
2141
|
+
/** The listed ref `name` resolves to, in {@link findRefTip}'s order */
|
|
2142
|
+
function resolveRefRecord(records, name) {
|
|
2143
|
+
const byName = getRefRecordsByName(records);
|
|
2144
|
+
for (const candidate of getRefNameCandidates(name)) {
|
|
2145
|
+
const record = byName.get(candidate);
|
|
2146
|
+
if (record != null) return record;
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
/** Whether more than one listed ref answers to `name`, which git calls ambiguous and resolves in {@link findRefTip}'s order */
|
|
2150
|
+
function isAmbiguousRefName(records, name) {
|
|
1927
2151
|
const byName = getRefRecordsByName(records);
|
|
1928
|
-
|
|
2152
|
+
let found = false;
|
|
2153
|
+
for (const candidate of getRefNameCandidates(name)) {
|
|
2154
|
+
if (!byName.has(candidate)) continue;
|
|
2155
|
+
if (found) return true;
|
|
2156
|
+
found = true;
|
|
2157
|
+
}
|
|
2158
|
+
return false;
|
|
2159
|
+
}
|
|
2160
|
+
/** The full ref names `name` may stand for, in git's own order for a short name */
|
|
2161
|
+
function getRefNameCandidates(name) {
|
|
2162
|
+
return [
|
|
1929
2163
|
name,
|
|
1930
2164
|
`refs/${name}`,
|
|
1931
2165
|
`refs/tags/${name}`,
|
|
1932
2166
|
`refs/heads/${name}`,
|
|
1933
2167
|
`refs/remotes/${name}`,
|
|
1934
2168
|
`refs/remotes/${name}/HEAD`
|
|
1935
|
-
]
|
|
1936
|
-
const record = byName.get(candidate);
|
|
1937
|
-
if (record != null) return record.peeledObjectname || record.objectname;
|
|
1938
|
-
}
|
|
2169
|
+
];
|
|
1939
2170
|
}
|
|
1940
2171
|
function getRefRecordsByName(records) {
|
|
1941
2172
|
let byName = refRecordsByName.get(records);
|
|
@@ -1969,6 +2200,44 @@ function mayHaveUpstream(records, name) {
|
|
|
1969
2200
|
if (findRefTip(records, `refs/heads/${name}`) != null) return true;
|
|
1970
2201
|
return findRefTip(records, name) == null;
|
|
1971
2202
|
}
|
|
2203
|
+
/** `answer` with the default branch its read resolved, when it consulted one (see `AheadOfBase.defaultBranch`) */
|
|
2204
|
+
function withDefaultBranch(answer, defaultBranch) {
|
|
2205
|
+
return defaultBranch !== void 0 ? {
|
|
2206
|
+
...answer,
|
|
2207
|
+
defaultBranch
|
|
2208
|
+
} : answer;
|
|
2209
|
+
}
|
|
2210
|
+
/** Splits `git config --null --get-regexp` output into its entries, in order; a key set without a value has none */
|
|
2211
|
+
function parseConfigListing(listing) {
|
|
2212
|
+
const entries = [];
|
|
2213
|
+
for (const entry of listing.split("\0")) {
|
|
2214
|
+
if (!entry) continue;
|
|
2215
|
+
const separator = entry.indexOf("\n");
|
|
2216
|
+
entries.push(separator < 0 ? [entry, void 0] : [entry.substring(0, separator), entry.substring(separator + 1)]);
|
|
2217
|
+
}
|
|
2218
|
+
return entries;
|
|
2219
|
+
}
|
|
2220
|
+
/**
|
|
2221
|
+
* Whether `listing` shows config that can differ by checkout over time: `extensions.worktreeConfig` (each checkout's
|
|
2222
|
+
* `config.worktree`), or a conditional include that is neither `hasconfig:` nor `gitdir:`, since `onbranch:` follows
|
|
2223
|
+
* each checkout's HEAD and an unknown condition might follow anything. A `gitdir:` include matching only some
|
|
2224
|
+
* checkouts is accepted as shared: it is the common identity setup, and treating it as differing would end sharing for
|
|
2225
|
+
* nearly everyone. The cost is a known gap: an edit to a file only those checkouts include goes unseen there, since
|
|
2226
|
+
* nothing watches it, until a change the shared listing does see.
|
|
2227
|
+
*/
|
|
2228
|
+
function mayConfigDifferByCheckout(listing) {
|
|
2229
|
+
for (const [key, value] of parseConfigListing(listing)) {
|
|
2230
|
+
if (key === "extensions.worktreeconfig") {
|
|
2231
|
+
if (value == null || !isGitBooleanFalse(value)) return true;
|
|
2232
|
+
continue;
|
|
2233
|
+
}
|
|
2234
|
+
if (!key.startsWith("includeif.")) continue;
|
|
2235
|
+
const condition = key.substring(10, key.lastIndexOf("."));
|
|
2236
|
+
if (condition.startsWith("hasconfig:") || condition.startsWith("gitdir:") || condition.startsWith("gitdir/i:")) continue;
|
|
2237
|
+
return true;
|
|
2238
|
+
}
|
|
2239
|
+
return false;
|
|
2240
|
+
}
|
|
1972
2241
|
/**
|
|
1973
2242
|
* Adds the full ref names a `.git/refs` watcher would report for `name`. A short name can't be told tag from local
|
|
1974
2243
|
* from remote without a spawn, so it adds every form, including the tag git resolves it to first; a spurious one
|