@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.
Files changed (55) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/dist/git/cache.d.ts +13 -0
  3. package/dist/git/cache.js +45 -8
  4. package/dist/git/cache.js.map +1 -1
  5. package/dist/git/features.d.ts +1 -1
  6. package/dist/git/features.js +1 -0
  7. package/dist/git/features.js.map +1 -1
  8. package/dist/git/providers/branches.d.ts +29 -0
  9. package/dist/git/run.types.d.ts +3 -0
  10. package/dist/git/service.d.ts +4 -2
  11. package/dist/git/service.js +6 -4
  12. package/dist/git/service.js.map +1 -1
  13. package/dist/git/watching/classifyChange.js +3 -0
  14. package/dist/git/watching/classifyChange.js.map +1 -1
  15. package/dist/git/watching/watchSession.d.ts +2 -0
  16. package/dist/git/watching/watchSession.js +9 -1
  17. package/dist/git/watching/watchSession.js.map +1 -1
  18. package/dist/git/watching/worktreeSummary.d.ts +19 -4
  19. package/dist/git/watching/worktreeSummary.js +121 -36
  20. package/dist/git/watching/worktreeSummary.js.map +1 -1
  21. package/dist/git-cli/cliGitProvider.d.ts +1 -1
  22. package/dist/git-cli/cliGitProvider.js +8 -5
  23. package/dist/git-cli/cliGitProvider.js.map +1 -1
  24. package/dist/git-cli/exec/commandClassifier.d.ts +18 -3
  25. package/dist/git-cli/exec/commandClassifier.js +562 -16
  26. package/dist/git-cli/exec/commandClassifier.js.map +1 -1
  27. package/dist/git-cli/exec/exec.d.ts +2 -2
  28. package/dist/git-cli/exec/git.d.ts +31 -4
  29. package/dist/git-cli/exec/git.js +105 -43
  30. package/dist/git-cli/exec/git.js.map +1 -1
  31. package/dist/git-cli/exec/headState.d.ts +32 -0
  32. package/dist/git-cli/exec/headState.js +78 -0
  33. package/dist/git-cli/exec/headState.js.map +1 -0
  34. package/dist/git-cli/providers/branches.d.ts +50 -0
  35. package/dist/git-cli/providers/branches.js +294 -25
  36. package/dist/git-cli/providers/branches.js.map +1 -1
  37. package/dist/git-cli/providers/config.js +3 -2
  38. package/dist/git-cli/providers/config.js.map +1 -1
  39. package/dist/git-cli/providers/operations.d.ts +6 -7
  40. package/dist/git-cli/providers/operations.js +46 -64
  41. package/dist/git-cli/providers/operations.js.map +1 -1
  42. package/dist/git-cli/providers/refs.d.ts +4 -1
  43. package/dist/git-cli/providers/refs.js +13 -9
  44. package/dist/git-cli/providers/refs.js.map +1 -1
  45. package/dist/git-cli/providers/revision.js +1 -1
  46. package/dist/git-cli/providers/stash.js +1 -1
  47. package/dist/git-cli/providers/worktrees.js +23 -6
  48. package/dist/git-cli/providers/worktrees.js.map +1 -1
  49. package/dist/git-cli/service.d.ts +3 -0
  50. package/dist/git-cli/service.js +1 -0
  51. package/dist/git-cli/service.js.map +1 -1
  52. package/dist/utils/env/node/exec.d.ts +38 -1
  53. package/dist/utils/env/node/exec.js +14 -6
  54. package/dist/utils/env/node/exec.js.map +1 -1
  55. 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 current = await this.getCurrentBranchReference(repoPath, cancellation);
746
- if (current == null) return { known: false };
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 = current.name;
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 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;
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
- return result.stdout;
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
- if (branch != null && defaultBranch === branch) return this.measureAheadOfSelfUpstream(repoPath, branch, dependsOn, cancellation);
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
- for (const candidate of [
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