@workit-dev/core 0.0.1 → 1.0.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.
Files changed (234) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +22 -43
  3. package/package.json +56 -6
  4. package/schemas/config.json +74 -0
  5. package/schemas/vcs.json +79 -0
  6. package/schemas/workspaces.json +1029 -0
  7. package/schemas/youtrack.json +84 -0
  8. package/scripts/analyze-release-scope.ts +215 -0
  9. package/scripts/bundled-deps.json +131 -0
  10. package/scripts/fixtures/sample-plan.md +15 -0
  11. package/scripts/fixtures/sample-sdd/progress.md +2 -0
  12. package/scripts/install-codex-plugin.sh +28 -0
  13. package/scripts/install-cursor-plugin.sh +146 -0
  14. package/scripts/install-opencode-plugin.sh +79 -0
  15. package/scripts/install-pi-package.sh +56 -0
  16. package/scripts/lib/config-dir.sh +28 -0
  17. package/scripts/publish-changed-packages.ts +395 -0
  18. package/scripts/rewrite-workspace-deps.ts +53 -0
  19. package/scripts/run-cursor-mcp.sh +6 -0
  20. package/scripts/sync-release-manifests.ts +82 -0
  21. package/scripts/sync-runtime.sh +142 -0
  22. package/scripts/validate-cursor-marketplace.ts +171 -0
  23. package/scripts/verify-bundle-sources.ts +134 -0
  24. package/scripts/write-schemas.ts +16 -0
  25. package/skills/workit-architecture/SKILL.md +72 -0
  26. package/skills/workit-architecture/agents/openai.yaml +3 -0
  27. package/skills/workit-architecture/references/instruction-files.md +52 -0
  28. package/skills/workit-architecture/references/vocabulary.md +53 -0
  29. package/skills/workit-bdd/SKILL.md +53 -0
  30. package/skills/workit-continue/SKILL.md +54 -0
  31. package/skills/workit-debug/SKILL.md +45 -0
  32. package/skills/workit-deslop/SKILL.md +58 -0
  33. package/skills/workit-fanout/SKILL.md +71 -0
  34. package/skills/workit-fanout/references/brief.md +141 -0
  35. package/skills/workit-fanout/references/orchestration.md +169 -0
  36. package/skills/workit-implement/SKILL.md +71 -0
  37. package/skills/workit-retro/SKILL.md +72 -0
  38. package/skills/workit-retro/agents/openai.yaml +3 -0
  39. package/skills/workit-retro/references/sources.md +35 -0
  40. package/skills/workit-retro/references/steering.md +39 -0
  41. package/skills/workit-review/SKILL.md +66 -0
  42. package/skills/workit-review/references/impact.md +24 -0
  43. package/skills/workit-shape/SKILL.md +71 -0
  44. package/skills/workit-shape/references/diagrams.md +17 -0
  45. package/skills/workit-shape/references/knowledge.md +61 -0
  46. package/skills/workit-shape/references/mockups.md +15 -0
  47. package/skills/workit-shape/references/slicing.md +44 -0
  48. package/skills/workit-ship/SKILL.md +71 -0
  49. package/skills/workit-ship/references/changelog.md +44 -0
  50. package/skills/workit-ship/references/pr-body.md +32 -0
  51. package/skills/workit-ship/references/release.md +39 -0
  52. package/skills/workit-test-audit/SKILL.md +54 -0
  53. package/skills/workit-verify-app/SKILL.md +68 -0
  54. package/skills/workit-verify-app/references/template.md +51 -0
  55. package/src/autonomy.ts +910 -0
  56. package/src/check-config.ts +613 -0
  57. package/src/checks.ts +534 -0
  58. package/src/ci-waiter.ts +159 -0
  59. package/src/core/boundary.ts +55 -0
  60. package/src/core/branch-policy.ts +112 -0
  61. package/src/core/branch.ts +1192 -0
  62. package/src/core/changelog-gate.ts +349 -0
  63. package/src/core/changelog-merge.ts +214 -0
  64. package/src/core/changelog.ts +392 -0
  65. package/src/core/cleanup.ts +638 -0
  66. package/src/core/cli-locate.ts +173 -0
  67. package/src/core/commit-flavors.ts +65 -0
  68. package/src/core/config-schemas.ts +653 -0
  69. package/src/core/config.ts +351 -0
  70. package/src/core/context-read.ts +144 -0
  71. package/src/core/docs-gate.ts +69 -0
  72. package/src/core/finding-evidence.ts +89 -0
  73. package/src/core/finding-queue.ts +36 -0
  74. package/src/core/git.ts +73 -0
  75. package/src/core/gitignore.ts +52 -0
  76. package/src/core/heavy-eta.ts +170 -0
  77. package/src/core/heavy-lease.ts +290 -0
  78. package/src/core/heavy-rules.ts +373 -0
  79. package/src/core/heavy-scope.ts +131 -0
  80. package/src/core/heavy-words.ts +83 -0
  81. package/src/core/heavy.ts +535 -0
  82. package/src/core/hygiene.ts +149 -0
  83. package/src/core/init.ts +218 -0
  84. package/src/core/live-session.ts +333 -0
  85. package/src/core/logger.ts +310 -0
  86. package/src/core/methods.ts +185 -0
  87. package/src/core/model-fallback.ts +208 -0
  88. package/src/core/node-resolve.mjs +249 -0
  89. package/src/core/operation-input.ts +632 -0
  90. package/src/core/package-root.ts +28 -0
  91. package/src/core/policy/attributes.ts +97 -0
  92. package/src/core/policy/bookkeeping.ts +119 -0
  93. package/src/core/policy/carry.ts +122 -0
  94. package/src/core/policy/check-evidence.ts +115 -0
  95. package/src/core/policy/current-judgment.ts +20 -0
  96. package/src/core/policy/derive.ts +387 -0
  97. package/src/core/policy/judgment.ts +246 -0
  98. package/src/core/policy/lane.ts +562 -0
  99. package/src/core/policy/pr-lane.ts +32 -0
  100. package/src/core/policy/substance.ts +365 -0
  101. package/src/core/policy-resolver.ts +94 -0
  102. package/src/core/pr-create.ts +448 -0
  103. package/src/core/pr-template.ts +162 -0
  104. package/src/core/release-notes.ts +197 -0
  105. package/src/core/release-tracks.ts +779 -0
  106. package/src/core/remotes.ts +436 -0
  107. package/src/core/repo-context.ts +542 -0
  108. package/src/core/repo-paths.ts +29 -0
  109. package/src/core/resources.ts +1299 -0
  110. package/src/core/role-models.ts +603 -0
  111. package/src/core/route-intent.ts +305 -0
  112. package/src/core/rules.ts +65 -0
  113. package/src/core/runtime-identity.ts +63 -0
  114. package/src/core/safe-write.ts +87 -0
  115. package/src/core/scope-glob.ts +56 -0
  116. package/src/core/scripts.ts +6 -0
  117. package/src/core/skill-manifests.ts +173 -0
  118. package/src/core/store-lock.ts +374 -0
  119. package/src/core/support-matrix.ts +24 -0
  120. package/src/core/task-context.ts +247 -0
  121. package/src/core/task-contract.ts +1279 -0
  122. package/src/core/task-delivery.ts +109 -0
  123. package/src/core/task-engine.ts +2248 -0
  124. package/src/core/task-evaluation.ts +1147 -0
  125. package/src/core/task-store.ts +2649 -0
  126. package/src/core/task-view.ts +180 -0
  127. package/src/core/templates.ts +22 -0
  128. package/src/core/tracker-issues.ts +174 -0
  129. package/src/core/vcs-config.ts +540 -0
  130. package/src/core/workers.ts +62 -0
  131. package/src/core/workit-paths.mjs +242 -0
  132. package/src/core/workspace-branch-policy.ts +123 -0
  133. package/src/core/workspaces.ts +1745 -0
  134. package/src/core/youtrack-backend.ts +288 -0
  135. package/src/core/youtrack-tools.ts +260 -0
  136. package/src/core/youtrack-writes.ts +73 -0
  137. package/src/core/youtrack-ytrack.ts +217 -0
  138. package/src/core/youtrack.ts +454 -0
  139. package/src/core/ytrack-skill.ts +297 -0
  140. package/src/core.ts +171 -0
  141. package/src/deslop/lex.ts +271 -0
  142. package/src/deslop/scan.ts +232 -0
  143. package/src/duration.ts +27 -0
  144. package/src/fanout-brief.ts +486 -0
  145. package/src/fanout-check.ts +455 -0
  146. package/src/fanout-landed.ts +370 -0
  147. package/src/fanout-stack.ts +108 -0
  148. package/src/fanout-status.ts +362 -0
  149. package/src/fanout-worktree.ts +893 -0
  150. package/src/fanout.ts +1473 -0
  151. package/src/forge/exec.ts +385 -0
  152. package/src/forge/github.ts +831 -0
  153. package/src/forge/gitlab.ts +906 -0
  154. package/src/forge/issue-forge.ts +209 -0
  155. package/src/forge/issue-link.ts +468 -0
  156. package/src/forge/issue-ops.ts +314 -0
  157. package/src/forge/merge-gate-base-red.ts +72 -0
  158. package/src/forge/pr-body.ts +100 -0
  159. package/src/forge/pr-edit.ts +292 -0
  160. package/src/forge/pr-ops.ts +1109 -0
  161. package/src/forge/pr-status.ts +10 -0
  162. package/src/forge/redact.ts +195 -0
  163. package/src/forge/report.ts +994 -0
  164. package/src/forge/reruns.ts +132 -0
  165. package/src/forge/resolve.ts +509 -0
  166. package/src/forge/types.ts +242 -0
  167. package/src/forge/verify.ts +398 -0
  168. package/src/git/default-base.ts +130 -0
  169. package/src/git/env-redirect.ts +28 -0
  170. package/src/git/facts.ts +122 -0
  171. package/src/git/ops.ts +1823 -0
  172. package/src/git/refs-read.ts +332 -0
  173. package/src/git/release.ts +1020 -0
  174. package/src/git/rev.ts +1175 -0
  175. package/src/git/session-hook.ts +597 -0
  176. package/src/git/stage.ts +63 -0
  177. package/src/hooks/agent-model.ts +149 -0
  178. package/src/hooks/budget-signal.ts +284 -0
  179. package/src/hooks/context.ts +461 -0
  180. package/src/hooks/cwd-normalize.ts +79 -0
  181. package/src/hooks/deadline.ts +228 -0
  182. package/src/hooks/delivery-verbs.ts +182 -0
  183. package/src/hooks/descriptor.ts +124 -0
  184. package/src/hooks/handle.ts +433 -0
  185. package/src/hooks/heavy-gate.ts +99 -0
  186. package/src/hooks/hook-runs.ts +130 -0
  187. package/src/hooks/hosts/claude-code.ts +380 -0
  188. package/src/hooks/hosts/codex.ts +372 -0
  189. package/src/hooks/hosts/cursor.ts +419 -0
  190. package/src/hooks/hosts/fields.ts +98 -0
  191. package/src/hooks/hosts/opencode.ts +79 -0
  192. package/src/hooks/hosts/pi.ts +78 -0
  193. package/src/hooks/index.ts +60 -0
  194. package/src/hooks/model-limit.ts +378 -0
  195. package/src/hooks/model-switch.ts +35 -0
  196. package/src/hooks/policy.ts +29 -0
  197. package/src/hooks/process.ts +48 -0
  198. package/src/hooks/protocol.ts +135 -0
  199. package/src/hooks/raw-git.ts +2289 -0
  200. package/src/hooks/repeat-failure.ts +115 -0
  201. package/src/hooks/run.ts +54 -0
  202. package/src/hooks/session-marker.ts +151 -0
  203. package/src/hooks/shell-words.ts +532 -0
  204. package/src/hooks/skill-nudge.ts +295 -0
  205. package/src/hooks/spawn-gate.ts +72 -0
  206. package/src/hooks/standing-signal.ts +99 -0
  207. package/src/hooks/standing.ts +225 -0
  208. package/src/hooks/steering.ts +74 -0
  209. package/src/hooks/stop.ts +408 -0
  210. package/src/hooks/worktree-confine.ts +415 -0
  211. package/src/hooks/write-gate.ts +1174 -0
  212. package/src/host-session.ts +459 -0
  213. package/src/knowledge.ts +368 -0
  214. package/src/ledger.ts +2913 -0
  215. package/src/stack.ts +2570 -0
  216. package/src/store/event-log.ts +217 -0
  217. package/src/store/notes.ts +29 -0
  218. package/src/store/patch.ts +135 -0
  219. package/src/store/paths.ts +368 -0
  220. package/src/store/reduce.ts +203 -0
  221. package/src/test-audit-mocks.ts +309 -0
  222. package/src/verdict-cover.ts +151 -0
  223. package/src/worktree-deps.ts +142 -0
  224. package/templates/headers.md +3 -0
  225. package/templates/hygiene/.editorconfig +8 -0
  226. package/templates/hygiene/.gitattributes +3 -0
  227. package/templates/hygiene/CHANGELOG.md +14 -0
  228. package/templates/hygiene/CONTRIBUTING.md +3 -0
  229. package/templates/hygiene/LICENSE +21 -0
  230. package/templates/hygiene/README.md +3 -0
  231. package/templates/issue-update.md +5 -0
  232. package/templates/plan-template.md +26 -0
  233. package/templates/spec-template.md +64 -0
  234. package/templates/workit-contract.md +13 -0
@@ -0,0 +1,332 @@
1
+ // HEAD and branch tips read from the git files, without spawning git (#284 N6).
2
+ //
3
+ // Every read is fresh: HEAD and branch tips move during a workit call, and a
4
+ // stale HEAD would mis-key a SHA-keyed verdict, so nothing here is cached
5
+ // (decision mw3kbh1p). Each reader answers only the plain layouts it can be
6
+ // certain about, and returns null (ask git) otherwise:
7
+ //
8
+ // - an environment that changes where git looks (GIT_DIR, GIT_COMMON_DIR,
9
+ // GIT_INDEX_FILE, GIT_NAMESPACE, object or replace overrides, ...);
10
+ // - reftable ref storage (`extensions.refStorage`, or a `reftable` dir);
11
+ // - a HEAD that is not a regular file (a symlink HEAD is a symref), a HEAD
12
+ // symref outside refs/heads, a symbolic or non-regular branch ref;
13
+ // - a missing ref (unborn branch), a peeled packed entry, any line or name
14
+ // it does not fully understand;
15
+ // - a tip it cannot prove is a commit (`^{commit}`): an object outside the
16
+ // local loose objects and pack indexes, a delta in a pack, replace refs;
17
+ // - a short branch name another ref could shadow (`--short` would differ).
18
+ import fs from "node:fs";
19
+ import path from "node:path";
20
+ import zlib from "node:zlib";
21
+ import { gitDirs } from "./facts";
22
+
23
+ /** Environment that moves the repository, its refs or its objects away from the files read here. */
24
+ const REDIRECTING_ENV = [
25
+ "GIT_DIR",
26
+ "GIT_COMMON_DIR",
27
+ "GIT_WORK_TREE",
28
+ "GIT_INDEX_FILE",
29
+ "GIT_NAMESPACE",
30
+ "GIT_OBJECT_DIRECTORY",
31
+ "GIT_ALTERNATE_OBJECT_DIRECTORIES",
32
+ "GIT_REPLACE_REF_BASE",
33
+ "GIT_NO_REPLACE_OBJECTS",
34
+ "GIT_REF_PARANOIA",
35
+ "GIT_CEILING_DIRECTORIES",
36
+ "GIT_DISCOVERY_ACROSS_FILESYSTEM",
37
+ ];
38
+ // Command-line config (GIT_CONFIG_PARAMETERS, GIT_CONFIG_COUNT) is not here:
39
+ // git takes extensions only from the repository's own config, and the one
40
+ // config that changes what a ref names (core.useReplaceRefs) matters only
41
+ // with replace refs, which already send a read to git.
42
+
43
+ type Repo = { gitDir: string; commonDir: string; hexLength: 40 | 64 };
44
+
45
+ const readText = (file: string): string | null => {
46
+ try {
47
+ return fs.readFileSync(file, "utf8");
48
+ } catch {
49
+ return null;
50
+ }
51
+ };
52
+
53
+ /** The repository at `cwd` when its refs are plain files this module can read; null otherwise. */
54
+ const plainRepo = (cwd: string): Repo | null => {
55
+ if (REDIRECTING_ENV.some((name) => process.env[name] !== undefined)) return null;
56
+ const dirs = gitDirs(cwd);
57
+ if (!dirs) return null;
58
+ if (fs.existsSync(path.join(dirs.commonDir, "reftable"))) return null;
59
+ const config = readText(path.join(dirs.commonDir, "config"));
60
+ if (config === null) return null;
61
+ if (/^\s*refstorage\s*=/imu.test(config) && !/^\s*refstorage\s*=\s*files\s*$/imu.test(config))
62
+ return null;
63
+ const format = /^\s*objectformat\s*=\s*(\S+)\s*$/imu.exec(config)?.[1]?.toLowerCase() ?? "sha1";
64
+ if (format !== "sha1" && format !== "sha256") return null;
65
+ return {
66
+ gitDir: dirs.gitDir,
67
+ commonDir: dirs.commonDir,
68
+ hexLength: format === "sha256" ? 64 : 40,
69
+ };
70
+ };
71
+
72
+ /** A branch name git accepts and this module can map to a file path (a conservative subset of check-ref-format). */
73
+ const plainBranchName = (name: string): boolean =>
74
+ /^[A-Za-z0-9_][\w.+,=@-]*(?:\/[\w+,=@-][\w.+,=@-]*)*$/u.test(name) &&
75
+ !/\.\.|@\{|\.lock(?:\/|$)|\.$|\.\//u.test(name) &&
76
+ name !== "@" &&
77
+ name !== "HEAD";
78
+
79
+ /** A loose ref file's content: an object id, a symref, or null for anything else. */
80
+ type LooseRef = { kind: "id"; id: string } | { kind: "symref"; target: string };
81
+
82
+ const parseRefFile = (text: string, hexLength: number): LooseRef | null => {
83
+ const id = /^([0-9a-f]+)\n?$/u.exec(text);
84
+ if (id) return id[1].length === hexLength ? { kind: "id", id: id[1] } : null;
85
+ const symref = /^ref: (refs\/[^\s]+)\n?$/u.exec(text);
86
+ return symref ? { kind: "symref", target: symref[1] } : null;
87
+ };
88
+
89
+ /** Larger packed-refs files are left to git, which binary-searches them instead of parsing every line. */
90
+ const MAX_PACKED_BYTES = 1024 * 1024;
91
+
92
+ /**
93
+ * A regular file's content; undefined when nothing (or a missing parent) is
94
+ * there; null for anything git might read differently (a symlink, a
95
+ * directory) or a file over `maxBytes`.
96
+ */
97
+ const regularFile = (file: string, maxBytes = Infinity): string | undefined | null => {
98
+ let stat: fs.Stats;
99
+ try {
100
+ stat = fs.lstatSync(file);
101
+ } catch (error) {
102
+ const code = (error as NodeJS.ErrnoException).code;
103
+ return code === "ENOENT" || code === "ENOTDIR" ? undefined : null;
104
+ }
105
+ if (!stat.isFile() || stat.size > maxBytes) return null;
106
+ return readText(file);
107
+ };
108
+
109
+ type Packed = Map<string, { id: string; peeled: boolean }>;
110
+
111
+ /** packed-refs by name; an empty map when there is none; null on any line it does not understand, or a large file. */
112
+ const readPacked = (repo: Repo): Packed | null => {
113
+ const text = regularFile(path.join(repo.commonDir, "packed-refs"), MAX_PACKED_BYTES);
114
+ if (text === undefined) return new Map();
115
+ if (text === null || text.includes("\r")) return null;
116
+ const refs: Packed = new Map();
117
+ let last: string | null = null;
118
+ const lines = text.split("\n");
119
+ if (lines.at(-1) === "") lines.pop();
120
+ for (const [index, entry] of lines.entries()) {
121
+ if (index === 0 && entry.startsWith("# pack-refs with:")) continue;
122
+ const peeled = /^\^([0-9a-f]+)$/u.exec(entry);
123
+ if (peeled) {
124
+ const ref = last === null ? undefined : refs.get(last);
125
+ if (!ref || ref.peeled || peeled[1].length !== repo.hexLength) return null;
126
+ ref.peeled = true;
127
+ continue;
128
+ }
129
+ const line = /^([0-9a-f]+) (refs\/\S+)$/u.exec(entry);
130
+ if (!line || line[1].length !== repo.hexLength || refs.has(line[2])) return null;
131
+ refs.set(line[2], { id: line[1], peeled: false });
132
+ last = line[2];
133
+ }
134
+ return refs;
135
+ };
136
+
137
+ /** Whether a ref exists in the files backend; null when unsure. */
138
+ const refExists = (repo: Repo, packed: Packed, ref: string): boolean | null => {
139
+ for (const dir of new Set([repo.gitDir, repo.commonDir])) {
140
+ try {
141
+ fs.lstatSync(path.join(dir, ref));
142
+ return true;
143
+ } catch (error) {
144
+ const code = (error as NodeJS.ErrnoException).code;
145
+ if (code !== "ENOENT" && code !== "ENOTDIR") return null;
146
+ }
147
+ }
148
+ return packed.has(ref);
149
+ };
150
+
151
+ /** The object a branch names: loose ref first, then packed-refs, as git reads them; null when absent or unsure. */
152
+ const branchObject = (repo: Repo, branch: string, packed: () => Packed | null): string | null => {
153
+ if (!plainBranchName(branch)) return null;
154
+ const ref = `refs/heads/${branch}`;
155
+ const loose = regularFile(path.join(repo.commonDir, ref));
156
+ if (loose === null) return null;
157
+ if (loose !== undefined) {
158
+ const parsed = parseRefFile(loose, repo.hexLength);
159
+ return parsed?.kind === "id" ? parsed.id : null;
160
+ }
161
+ const entry = packed()?.get(ref);
162
+ return entry && !entry.peeled ? entry.id : null;
163
+ };
164
+
165
+ const readAt = (fd: number, length: number, position: number): Buffer | null => {
166
+ const buffer = Buffer.alloc(length);
167
+ return fs.readSync(fd, buffer, 0, length, position) === length ? buffer : null;
168
+ };
169
+
170
+ /**
171
+ * The pack entry type of `id` from one `.idx` (version 2) and its pack: 1 is a
172
+ * commit, 6 and 7 are deltas. Undefined when the index does not list the id.
173
+ */
174
+ const packedType = (pack: string, id: string): number | undefined | null => {
175
+ const want = Buffer.from(id, "hex");
176
+ const idx = fs.openSync(`${pack}.idx`, "r");
177
+ try {
178
+ const header = readAt(idx, 8 + 256 * 4, 0);
179
+ if (!header || header.readUInt32BE(0) !== 0xff744f63 || header.readUInt32BE(4) !== 2)
180
+ return null;
181
+ const count = header.readUInt32BE(8 + 255 * 4);
182
+ let low = want[0] === 0 ? 0 : header.readUInt32BE(8 + (want[0] - 1) * 4);
183
+ let high = header.readUInt32BE(8 + want[0] * 4);
184
+ const names = 8 + 256 * 4;
185
+ let found = -1;
186
+ while (low < high) {
187
+ const mid = (low + high) >>> 1;
188
+ const name = readAt(idx, want.length, names + mid * want.length);
189
+ if (!name) return null;
190
+ const order = Buffer.compare(name, want);
191
+ if (order === 0) {
192
+ found = mid;
193
+ break;
194
+ }
195
+ if (order < 0) low = mid + 1;
196
+ else high = mid;
197
+ }
198
+ if (found < 0) return undefined;
199
+ const offsets = names + count * want.length + count * 4;
200
+ const small = readAt(idx, 4, offsets + found * 4);
201
+ if (!small) return null;
202
+ let offset = small.readUInt32BE(0);
203
+ if (offset & 0x80000000) {
204
+ const large = readAt(idx, 8, offsets + count * 4 + (offset & 0x7fffffff) * 8);
205
+ if (!large) return null;
206
+ offset = Number(large.readBigUInt64BE(0));
207
+ }
208
+ const data = fs.openSync(`${pack}.pack`, "r");
209
+ try {
210
+ const entry = readAt(data, 1, offset);
211
+ return entry ? (entry[0] >> 4) & 7 : null;
212
+ } finally {
213
+ fs.closeSync(data);
214
+ }
215
+ } finally {
216
+ fs.closeSync(idx);
217
+ }
218
+ };
219
+
220
+ /**
221
+ * Whether `id` is a commit in the local object store: true only when proven
222
+ * (a loose object's header, or a pack entry's type). Null when unsure:
223
+ * missing here (alternates, a lazy fetch), a delta, replace refs.
224
+ */
225
+ const isCommit = (repo: Repo, id: string, packed: Packed): boolean | null => {
226
+ if (fs.existsSync(path.join(repo.commonDir, "refs", "replace"))) return null;
227
+ for (const name of packed.keys()) if (name.startsWith("refs/replace/")) return null;
228
+ const objects = path.join(repo.commonDir, "objects");
229
+ try {
230
+ const fd = fs.openSync(path.join(objects, id.slice(0, 2), id.slice(2)), "r");
231
+ let head: Buffer;
232
+ try {
233
+ head = Buffer.alloc(64);
234
+ head = head.subarray(0, fs.readSync(fd, head, 0, head.length, 0));
235
+ } finally {
236
+ fs.closeSync(fd);
237
+ }
238
+ const header = zlib.inflateSync(head, { finishFlush: zlib.constants.Z_SYNC_FLUSH });
239
+ return header.toString("latin1").startsWith("commit ");
240
+ } catch (error) {
241
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") return null;
242
+ }
243
+ try {
244
+ const packDir = path.join(objects, "pack");
245
+ for (const file of fs.readdirSync(packDir)) {
246
+ if (!file.endsWith(".idx")) continue;
247
+ const type = packedType(path.join(packDir, file.slice(0, -4)), id);
248
+ if (type === undefined) continue;
249
+ return type === 1 ? true : null;
250
+ }
251
+ } catch {}
252
+ return null;
253
+ };
254
+
255
+ /** `id` when it is proven to be a commit, else null (ask git). */
256
+ const commitOrNull = (
257
+ repo: Repo,
258
+ id: string | null,
259
+ packed: () => Packed | null,
260
+ ): string | null => {
261
+ if (!id) return null;
262
+ const refs = packed();
263
+ return refs && isCommit(repo, id, refs) === true ? id : null;
264
+ };
265
+
266
+ /** One read of packed-refs per call, and only when needed. */
267
+ const lazyPacked = (repo: Repo): (() => Packed | null) => {
268
+ let value: Packed | null | undefined;
269
+ return () => {
270
+ if (value === undefined) value = readPacked(repo);
271
+ return value;
272
+ };
273
+ };
274
+
275
+ type Head = { kind: "detached"; id: string } | { kind: "branch"; branch: string };
276
+
277
+ const readHead = (repo: Repo): Head | null => {
278
+ const text = regularFile(path.join(repo.gitDir, "HEAD"));
279
+ if (!text) return null;
280
+ const parsed = parseRefFile(text, repo.hexLength);
281
+ if (!parsed) return null;
282
+ if (parsed.kind === "id") return { kind: "detached", id: parsed.id };
283
+ const branch = /^refs\/heads\/(.+)$/u.exec(parsed.target)?.[1];
284
+ return branch && plainBranchName(branch) ? { kind: "branch", branch } : null;
285
+ };
286
+
287
+ /** The commit HEAD points at (`git rev-parse --verify HEAD^{commit}`), or null when git must answer. */
288
+ export function readHeadCommit(cwd: string): string | null {
289
+ const repo = plainRepo(cwd);
290
+ if (!repo) return null;
291
+ const head = readHead(repo);
292
+ if (!head) return null;
293
+ const packed = lazyPacked(repo);
294
+ const id = head.kind === "detached" ? head.id : branchObject(repo, head.branch, packed);
295
+ return commitOrNull(repo, id, packed);
296
+ }
297
+
298
+ /** The commit `refs/heads/<branch>` points at (`git rev-parse --verify refs/heads/<branch>^{commit}`), or null when git must answer. */
299
+ export function readBranchCommit(cwd: string, branch: string): string | null {
300
+ const repo = plainRepo(cwd);
301
+ if (!repo) return null;
302
+ const packed = lazyPacked(repo);
303
+ return commitOrNull(repo, branchObject(repo, branch, packed), packed);
304
+ }
305
+
306
+ /**
307
+ * `git symbolic-ref --short -q HEAD`: `{ branch }` with the short name (also
308
+ * on an unborn branch), `{ branch: null }` on a detached HEAD, or null when
309
+ * git must answer. The short name is the branch name only when no other ref
310
+ * could claim it (git then prints a longer form): `<name>` at the top of the
311
+ * git dir, `refs/<name>`, `refs/tags/<name>`, `refs/remotes/<name>` or
312
+ * `refs/remotes/<name>/HEAD`.
313
+ */
314
+ export function readHeadBranch(cwd: string): { branch: string | null } | null {
315
+ const repo = plainRepo(cwd);
316
+ if (!repo) return null;
317
+ const head = readHead(repo);
318
+ if (!head) return null;
319
+ if (head.kind === "detached") return { branch: null };
320
+ const packed = readPacked(repo);
321
+ if (!packed) return null;
322
+ const name = head.branch;
323
+ for (const shadow of [
324
+ name,
325
+ `refs/${name}`,
326
+ `refs/tags/${name}`,
327
+ `refs/remotes/${name}`,
328
+ `refs/remotes/${name}/HEAD`,
329
+ ])
330
+ if (refExists(repo, packed, shadow) !== false) return null;
331
+ return { branch: name };
332
+ }