@hominis/fireforge 0.44.4 → 0.46.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 (378) hide show
  1. package/CHANGELOG.md +120 -0
  2. package/dist/bin/fireforge.js +6 -0
  3. package/dist/build-info.json +4 -4
  4. package/dist/src/cli-usage-error.d.ts +46 -0
  5. package/dist/src/cli-usage-error.js +81 -0
  6. package/dist/src/cli.d.ts +5 -1
  7. package/dist/src/cli.js +20 -21
  8. package/dist/src/commands/bootstrap.js +1 -2
  9. package/dist/src/commands/build.js +15 -6
  10. package/dist/src/commands/destructive-decision.d.ts +19 -0
  11. package/dist/src/commands/destructive-decision.js +31 -0
  12. package/dist/src/commands/doctor-check-core.d.ts +0 -6
  13. package/dist/src/commands/doctor-check-core.js +0 -8
  14. package/dist/src/commands/doctor-orphaned-harness.d.ts +0 -6
  15. package/dist/src/commands/doctor-orphaned-harness.js +4 -12
  16. package/dist/src/commands/doctor-post-rebase-audit.js +1 -1
  17. package/dist/src/commands/doctor-source-pin.d.ts +29 -0
  18. package/dist/src/commands/doctor-source-pin.js +81 -0
  19. package/dist/src/commands/doctor.d.ts +2 -0
  20. package/dist/src/commands/doctor.js +9 -8
  21. package/dist/src/commands/download.js +35 -24
  22. package/dist/src/commands/export-all.js +21 -4
  23. package/dist/src/commands/export-flow.js +15 -60
  24. package/dist/src/commands/export-placement-conflicts.js +3 -3
  25. package/dist/src/commands/export-placement-gate.js +2 -8
  26. package/dist/src/commands/export-placement-policy.js +3 -5
  27. package/dist/src/commands/export-shared.d.ts +42 -23
  28. package/dist/src/commands/export-shared.js +4 -19
  29. package/dist/src/commands/export.js +10 -1
  30. package/dist/src/commands/furnace/authoring-config.d.ts +13 -0
  31. package/dist/src/commands/furnace/authoring-config.js +19 -0
  32. package/dist/src/commands/furnace/chrome-doc-remove.js +1 -1
  33. package/dist/src/commands/furnace/create-browser-test.js +3 -8
  34. package/dist/src/commands/furnace/create-dry-run.js +2 -5
  35. package/dist/src/commands/furnace/create-templates.d.ts +17 -1
  36. package/dist/src/commands/furnace/create-templates.js +2 -1
  37. package/dist/src/commands/furnace/create.js +25 -19
  38. package/dist/src/commands/furnace/deploy.js +11 -2
  39. package/dist/src/commands/furnace/init.js +2 -1
  40. package/dist/src/commands/furnace/override.js +28 -19
  41. package/dist/src/commands/furnace/refresh.js +9 -1
  42. package/dist/src/commands/furnace/remove-mutations.d.ts +6 -7
  43. package/dist/src/commands/furnace/remove-mutations.js +0 -1
  44. package/dist/src/commands/furnace/remove.js +2 -6
  45. package/dist/src/commands/furnace/rename-browser-test.js +1 -7
  46. package/dist/src/commands/furnace/rename.js +29 -29
  47. package/dist/src/commands/furnace/status.js +12 -9
  48. package/dist/src/commands/furnace/test-file-name.d.ts +28 -0
  49. package/dist/src/commands/furnace/test-file-name.js +39 -0
  50. package/dist/src/commands/furnace/validation-output.d.ts +21 -9
  51. package/dist/src/commands/furnace/validation-output.js +3 -8
  52. package/dist/src/commands/import.js +2 -9
  53. package/dist/src/commands/lint-per-patch.d.ts +2 -2
  54. package/dist/src/commands/lint-per-patch.js +19 -4
  55. package/dist/src/commands/lint-per-run-checkjs.d.ts +2 -2
  56. package/dist/src/commands/lint.d.ts +2 -1
  57. package/dist/src/commands/lint.js +1 -1
  58. package/dist/src/commands/manifest.js +1 -1
  59. package/dist/src/commands/package.js +2 -4
  60. package/dist/src/commands/patch/compact.js +14 -15
  61. package/dist/src/commands/patch/delete.js +15 -25
  62. package/dist/src/commands/patch/entry-projection.d.ts +13 -0
  63. package/dist/src/commands/patch/entry-projection.js +24 -0
  64. package/dist/src/commands/patch/lint-ignore.d.ts +0 -8
  65. package/dist/src/commands/patch/lint-ignore.js +19 -29
  66. package/dist/src/commands/patch/move-files-create.js +24 -10
  67. package/dist/src/commands/patch/move-files-into.js +38 -37
  68. package/dist/src/commands/patch/move-files.js +4 -13
  69. package/dist/src/commands/patch/rename.js +27 -41
  70. package/dist/src/commands/patch/reorder.js +25 -38
  71. package/dist/src/commands/patch/split-plan.d.ts +4 -5
  72. package/dist/src/commands/patch/split-plan.js +4 -11
  73. package/dist/src/commands/patch/split.js +45 -43
  74. package/dist/src/commands/patch/staged-dependency.js +16 -22
  75. package/dist/src/commands/patch/tier.js +15 -25
  76. package/dist/src/commands/patch-rename-map.d.ts +19 -0
  77. package/dist/src/commands/patch-rename-map.js +36 -0
  78. package/dist/src/commands/pending-resolution.d.ts +15 -0
  79. package/dist/src/commands/pending-resolution.js +25 -0
  80. package/dist/src/commands/re-export-adjacent.d.ts +32 -2
  81. package/dist/src/commands/re-export-adjacent.js +51 -10
  82. package/dist/src/commands/re-export-bulk-scan.js +6 -10
  83. package/dist/src/commands/re-export-drift.d.ts +8 -0
  84. package/dist/src/commands/re-export-drift.js +65 -4
  85. package/dist/src/commands/re-export-files.d.ts +2 -3
  86. package/dist/src/commands/re-export-files.js +38 -32
  87. package/dist/src/commands/re-export-lint.d.ts +3 -4
  88. package/dist/src/commands/re-export-lint.js +9 -2
  89. package/dist/src/commands/re-export-options.js +19 -5
  90. package/dist/src/commands/re-export-register.js +5 -1
  91. package/dist/src/commands/re-export-scan.js +8 -8
  92. package/dist/src/commands/re-export-single.d.ts +22 -3
  93. package/dist/src/commands/re-export-single.js +19 -11
  94. package/dist/src/commands/re-export.js +44 -27
  95. package/dist/src/commands/rebase/abort.js +3 -8
  96. package/dist/src/commands/rebase/continue.js +12 -12
  97. package/dist/src/commands/rebase/index.js +2 -2
  98. package/dist/src/commands/rebase/patch-loop.d.ts +2 -2
  99. package/dist/src/commands/rebase/patch-loop.js +2 -7
  100. package/dist/src/commands/resolve.js +18 -10
  101. package/dist/src/commands/run.d.ts +0 -13
  102. package/dist/src/commands/run.js +12 -25
  103. package/dist/src/commands/setup-support.d.ts +16 -4
  104. package/dist/src/commands/setup-support.js +16 -16
  105. package/dist/src/commands/setup.js +3 -9
  106. package/dist/src/commands/source.js +8 -1
  107. package/dist/src/commands/status-check.js +2 -2
  108. package/dist/src/commands/status-output.js +2 -2
  109. package/dist/src/commands/status.js +3 -4
  110. package/dist/src/commands/test-build-phase.d.ts +2 -2
  111. package/dist/src/commands/test-build-phase.js +33 -8
  112. package/dist/src/commands/test-diagnose.js +14 -5
  113. package/dist/src/commands/test-modes.d.ts +2 -2
  114. package/dist/src/commands/test-modes.js +0 -1
  115. package/dist/src/commands/test-register.js +6 -3
  116. package/dist/src/commands/test-run.js +29 -21
  117. package/dist/src/commands/test-stale-gate.js +8 -5
  118. package/dist/src/commands/test-verdict.d.ts +16 -2
  119. package/dist/src/commands/test-verdict.js +18 -3
  120. package/dist/src/commands/test.js +51 -7
  121. package/dist/src/commands/token-list.d.ts +26 -0
  122. package/dist/src/commands/token-list.js +153 -0
  123. package/dist/src/commands/token.js +17 -1
  124. package/dist/src/commands/tree.js +7 -2
  125. package/dist/src/commands/typecheck.js +9 -9
  126. package/dist/src/commands/wire.js +4 -3
  127. package/dist/src/core/branding.d.ts +0 -7
  128. package/dist/src/core/branding.js +3 -2
  129. package/dist/src/core/browser-wire.js +6 -3
  130. package/dist/src/core/build-audit-platform.d.ts +29 -0
  131. package/dist/src/core/build-audit-platform.js +140 -0
  132. package/dist/src/core/build-audit-registration.js +3 -2
  133. package/dist/src/core/build-audit-skip.d.ts +57 -0
  134. package/dist/src/core/build-audit-skip.js +112 -0
  135. package/dist/src/core/build-audit.d.ts +15 -2
  136. package/dist/src/core/build-audit.js +154 -12
  137. package/dist/src/core/build-baseline.d.ts +45 -28
  138. package/dist/src/core/build-baseline.js +6 -29
  139. package/dist/src/core/build-jar-escalation.d.ts +64 -0
  140. package/dist/src/core/build-jar-escalation.js +128 -0
  141. package/dist/src/core/build-overwrite-guard.js +1 -1
  142. package/dist/src/core/build-prepare.d.ts +5 -2
  143. package/dist/src/core/build-prepare.js +34 -6
  144. package/dist/src/core/config-paths.d.ts +2 -6
  145. package/dist/src/core/config-paths.js +4 -2
  146. package/dist/src/core/config-state.d.ts +0 -6
  147. package/dist/src/core/config-state.js +1 -19
  148. package/dist/src/core/config-validate-build-audit.d.ts +8 -0
  149. package/dist/src/core/config-validate-build-audit.js +80 -0
  150. package/dist/src/core/config-validate-file-size.d.ts +12 -0
  151. package/dist/src/core/config-validate-file-size.js +65 -0
  152. package/dist/src/core/config-validate-patch-policy.d.ts +2 -2
  153. package/dist/src/core/config-validate-patch-policy.js +3 -3
  154. package/dist/src/core/config-validate-test-toolchains.d.ts +2 -2
  155. package/dist/src/core/config-validate-test-toolchains.js +4 -5
  156. package/dist/src/core/config-validate.js +20 -3
  157. package/dist/src/core/config.d.ts +2 -2
  158. package/dist/src/core/config.js +11 -15
  159. package/dist/src/core/coverage-extend.js +4 -4
  160. package/dist/src/core/discard-baseline.js +7 -7
  161. package/dist/src/core/engine-precondition.d.ts +1 -9
  162. package/dist/src/core/engine-precondition.js +3 -8
  163. package/dist/src/core/engine-session-lock.js +13 -17
  164. package/dist/src/core/export-moved-code-shape.js +2 -2
  165. package/dist/src/core/file-lock-owner.d.ts +103 -0
  166. package/dist/src/core/file-lock-owner.js +259 -0
  167. package/dist/src/core/file-lock.d.ts +2 -7
  168. package/dist/src/core/file-lock.js +40 -91
  169. package/dist/src/core/firefox-cache.d.ts +13 -1
  170. package/dist/src/core/firefox-cache.js +59 -34
  171. package/dist/src/core/firefox-download.d.ts +15 -0
  172. package/dist/src/core/firefox-download.js +37 -21
  173. package/dist/src/core/firefox-extract.d.ts +6 -5
  174. package/dist/src/core/firefox-extract.js +36 -18
  175. package/dist/src/core/firefox.d.ts +30 -32
  176. package/dist/src/core/firefox.js +5 -36
  177. package/dist/src/core/furnace-apply-dry-run.d.ts +1 -6
  178. package/dist/src/core/furnace-apply-dry-run.js +1 -7
  179. package/dist/src/core/furnace-apply-helpers.d.ts +0 -11
  180. package/dist/src/core/furnace-apply-helpers.js +8 -19
  181. package/dist/src/core/furnace-apply-overwrite-warn.js +4 -2
  182. package/dist/src/core/furnace-apply.d.ts +1 -1
  183. package/dist/src/core/furnace-apply.js +30 -10
  184. package/dist/src/core/furnace-config.d.ts +1 -1
  185. package/dist/src/core/furnace-config.js +23 -36
  186. package/dist/src/core/furnace-constants.js +3 -2
  187. package/dist/src/core/furnace-dir-entry.d.ts +20 -0
  188. package/dist/src/core/furnace-dir-entry.js +21 -0
  189. package/dist/src/core/furnace-operation.d.ts +2 -0
  190. package/dist/src/core/furnace-operation.js +3 -1
  191. package/dist/src/core/furnace-refresh.d.ts +18 -6
  192. package/dist/src/core/furnace-refresh.js +3 -6
  193. package/dist/src/core/furnace-registration-ast.d.ts +0 -1
  194. package/dist/src/core/furnace-registration-ast.js +0 -3
  195. package/dist/src/core/furnace-registration.d.ts +2 -1
  196. package/dist/src/core/furnace-registration.js +22 -29
  197. package/dist/src/core/furnace-scanner.js +3 -6
  198. package/dist/src/core/furnace-state-persist.d.ts +2 -2
  199. package/dist/src/core/furnace-validate-registration.js +5 -7
  200. package/dist/src/core/git-base.d.ts +3 -1
  201. package/dist/src/core/git-base.js +25 -3
  202. package/dist/src/core/git-diff-new-file.d.ts +40 -0
  203. package/dist/src/core/git-diff-new-file.js +119 -0
  204. package/dist/src/core/git-diff.d.ts +9 -9
  205. package/dist/src/core/git-diff.js +88 -157
  206. package/dist/src/core/git-file-ops.d.ts +0 -7
  207. package/dist/src/core/git-file-ops.js +4 -28
  208. package/dist/src/core/git-readonly-index.d.ts +0 -2
  209. package/dist/src/core/git-readonly-index.js +0 -4
  210. package/dist/src/core/git-status.d.ts +7 -0
  211. package/dist/src/core/git-status.js +21 -9
  212. package/dist/src/core/git.d.ts +0 -6
  213. package/dist/src/core/git.js +14 -43
  214. package/dist/src/core/harness-orphans.d.ts +73 -0
  215. package/dist/src/core/harness-orphans.js +187 -0
  216. package/dist/src/core/history-log.d.ts +19 -0
  217. package/dist/src/core/history-log.js +29 -0
  218. package/dist/src/core/mach-error-hints.d.ts +0 -5
  219. package/dist/src/core/mach-error-hints.js +1 -1
  220. package/dist/src/core/mach-known-noise-filter.d.ts +25 -3
  221. package/dist/src/core/mach-known-noise-filter.js +31 -10
  222. package/dist/src/core/mach-mozconfig.js +2 -1
  223. package/dist/src/core/mach-python.d.ts +2 -0
  224. package/dist/src/core/mach-python.js +12 -7
  225. package/dist/src/core/mach-resource-shim.d.ts +13 -1
  226. package/dist/src/core/mach-resource-shim.js +112 -9
  227. package/dist/src/core/mach.d.ts +46 -53
  228. package/dist/src/core/mach.js +31 -83
  229. package/dist/src/core/marionette-port.d.ts +0 -17
  230. package/dist/src/core/marionette-port.js +12 -58
  231. package/dist/src/core/marionette-preflight.js +9 -27
  232. package/dist/src/core/mochitest-server-port.js +3 -3
  233. package/dist/src/core/moz-manifest-rules.js +37 -33
  234. package/dist/src/core/patch-apply-fuzz.js +4 -4
  235. package/dist/src/core/patch-apply.d.ts +4 -5
  236. package/dist/src/core/patch-apply.js +55 -32
  237. package/dist/src/core/patch-export-coverage.d.ts +0 -9
  238. package/dist/src/core/patch-export-coverage.js +1 -30
  239. package/dist/src/core/patch-export-update.d.ts +18 -1
  240. package/dist/src/core/patch-export-update.js +4 -3
  241. package/dist/src/core/patch-export.d.ts +11 -21
  242. package/dist/src/core/patch-export.js +44 -66
  243. package/dist/src/core/patch-files.d.ts +0 -4
  244. package/dist/src/core/patch-files.js +0 -11
  245. package/dist/src/core/patch-lint-binary.d.ts +6 -12
  246. package/dist/src/core/patch-lint-cache.d.ts +17 -1
  247. package/dist/src/core/patch-lint-cache.js +3 -6
  248. package/dist/src/core/patch-lint-checkjs.d.ts +33 -16
  249. package/dist/src/core/patch-lint-checkjs.js +43 -25
  250. package/dist/src/core/patch-lint-creators.d.ts +2 -6
  251. package/dist/src/core/patch-lint-cross.d.ts +16 -8
  252. package/dist/src/core/patch-lint-cross.js +49 -92
  253. package/dist/src/core/patch-lint-css.js +10 -6
  254. package/dist/src/core/patch-lint-file-size.d.ts +52 -0
  255. package/dist/src/core/patch-lint-file-size.js +53 -0
  256. package/dist/src/core/patch-lint-forward-registration.d.ts +58 -0
  257. package/dist/src/core/patch-lint-forward-registration.js +210 -0
  258. package/dist/src/core/patch-lint-jsdoc.js +36 -36
  259. package/dist/src/core/patch-lint-module-registration.d.ts +2 -8
  260. package/dist/src/core/patch-lint-prettier.d.ts +36 -0
  261. package/dist/src/core/patch-lint-prettier.js +126 -0
  262. package/dist/src/core/patch-lint-queue-types.d.ts +60 -0
  263. package/dist/src/core/patch-lint-queue-types.js +12 -0
  264. package/dist/src/core/patch-lint-staged-registration.d.ts +61 -0
  265. package/dist/src/core/patch-lint-staged-registration.js +90 -0
  266. package/dist/src/core/patch-lint-unmanaged-companion.d.ts +106 -0
  267. package/dist/src/core/patch-lint-unmanaged-companion.js +214 -0
  268. package/dist/src/core/patch-lint.d.ts +14 -9
  269. package/dist/src/core/patch-lint.js +27 -27
  270. package/dist/src/core/patch-lock.js +3 -3
  271. package/dist/src/core/patch-manifest-io.js +8 -7
  272. package/dist/src/core/patch-parse.d.ts +19 -8
  273. package/dist/src/core/patch-parse.js +25 -0
  274. package/dist/src/core/patch-policy.js +15 -27
  275. package/dist/src/core/patch-source-metadata.d.ts +1 -3
  276. package/dist/src/core/patch-source-metadata.js +0 -4
  277. package/dist/src/core/patch-transform.d.ts +21 -8
  278. package/dist/src/core/patch-transform.js +38 -10
  279. package/dist/src/core/process-cpu-time.js +1 -1
  280. package/dist/src/core/rebase-session.d.ts +2 -8
  281. package/dist/src/core/rebase-session.js +5 -17
  282. package/dist/src/core/register-browser-content.js +3 -2
  283. package/dist/src/core/register-browser-test.js +2 -1
  284. package/dist/src/core/register-module.js +2 -1
  285. package/dist/src/core/register-scaffold.js +5 -4
  286. package/dist/src/core/register-shared-css.js +2 -1
  287. package/dist/src/core/run-log-redact.d.ts +38 -0
  288. package/dist/src/core/run-log-redact.js +69 -0
  289. package/dist/src/core/run-log.d.ts +13 -2
  290. package/dist/src/core/run-log.js +67 -18
  291. package/dist/src/core/signal-critical.js +2 -4
  292. package/dist/src/core/smoke-patterns.d.ts +0 -1
  293. package/dist/src/core/smoke-patterns.js +18 -11
  294. package/dist/src/core/state-file.d.ts +12 -1
  295. package/dist/src/core/state-file.js +14 -1
  296. package/dist/src/core/status-classify.js +2 -1
  297. package/dist/src/core/test-changed-prefs.js +26 -11
  298. package/dist/src/core/test-harness-crash.js +61 -48
  299. package/dist/src/core/test-harness-verdict-notes.d.ts +55 -0
  300. package/dist/src/core/test-harness-verdict-notes.js +48 -0
  301. package/dist/src/core/test-stale-check.d.ts +23 -8
  302. package/dist/src/core/test-stale-check.js +77 -4
  303. package/dist/src/core/test-stall-triage.js +37 -19
  304. package/dist/src/core/test-xpcshell-retry.d.ts +26 -7
  305. package/dist/src/core/test-xpcshell-retry.js +14 -11
  306. package/dist/src/core/token-category.d.ts +33 -0
  307. package/dist/src/core/token-category.js +56 -48
  308. package/dist/src/core/token-dark-mode.js +2 -14
  309. package/dist/src/core/token-inventory.d.ts +56 -0
  310. package/dist/src/core/token-inventory.js +109 -0
  311. package/dist/src/core/token-manager.d.ts +1 -1
  312. package/dist/src/core/token-manager.js +1 -1
  313. package/dist/src/core/token-variant.js +21 -22
  314. package/dist/src/core/toolchain-preflight.js +1 -1
  315. package/dist/src/core/tree-cow.d.ts +0 -2
  316. package/dist/src/core/tree-cow.js +2 -2
  317. package/dist/src/core/tree-store.js +4 -5
  318. package/dist/src/core/typecheck.js +2 -2
  319. package/dist/src/core/wire-dom-fragment.js +6 -17
  320. package/dist/src/core/wire-utils.js +5 -17
  321. package/dist/src/core/xpcshell-appdir.js +2 -4
  322. package/dist/src/errors/base.d.ts +53 -1
  323. package/dist/src/errors/base.js +79 -11
  324. package/dist/src/errors/build.d.ts +28 -0
  325. package/dist/src/errors/build.js +51 -10
  326. package/dist/src/errors/config.d.ts +0 -14
  327. package/dist/src/errors/config.js +13 -34
  328. package/dist/src/errors/download.d.ts +13 -0
  329. package/dist/src/errors/download.js +53 -24
  330. package/dist/src/errors/furnace.js +8 -9
  331. package/dist/src/errors/git.d.ts +8 -8
  332. package/dist/src/errors/git.js +20 -30
  333. package/dist/src/errors/patch.js +8 -10
  334. package/dist/src/errors/rebase.js +7 -7
  335. package/dist/src/types/commands/options.d.ts +96 -123
  336. package/dist/src/types/commands/patches.d.ts +17 -6
  337. package/dist/src/types/config.d.ts +75 -2
  338. package/dist/src/types/index.d.ts +0 -1
  339. package/dist/src/utils/assert.d.ts +3 -15
  340. package/dist/src/utils/assert.js +1 -18
  341. package/dist/src/utils/build-info.d.ts +1 -1
  342. package/dist/src/utils/child-env.d.ts +2 -1
  343. package/dist/src/utils/elapsed.js +10 -1
  344. package/dist/src/utils/fs.d.ts +4 -2
  345. package/dist/src/utils/fs.js +11 -14
  346. package/dist/src/utils/hash.d.ts +12 -0
  347. package/dist/src/utils/hash.js +16 -0
  348. package/dist/src/utils/line-dispatch.d.ts +15 -0
  349. package/dist/src/utils/line-dispatch.js +48 -0
  350. package/dist/src/utils/options.d.ts +1 -7
  351. package/dist/src/utils/options.js +6 -7
  352. package/dist/src/utils/package-root.js +3 -12
  353. package/dist/src/utils/platform.d.ts +0 -4
  354. package/dist/src/utils/process-group.d.ts +2 -1
  355. package/dist/src/utils/process-group.js +20 -19
  356. package/dist/src/utils/process.d.ts +40 -26
  357. package/dist/src/utils/process.js +115 -166
  358. package/dist/src/utils/ps-duration.js +3 -3
  359. package/dist/src/utils/sleep.d.ts +26 -0
  360. package/dist/src/utils/sleep.js +31 -0
  361. package/dist/src/utils/validation.d.ts +2 -38
  362. package/dist/src/utils/validation.js +7 -16
  363. package/docs/configuration.md +136 -1
  364. package/docs/exit-codes.md +34 -20
  365. package/docs/lifecycle-invariants.md +18 -12
  366. package/docs/machine-output.md +31 -8
  367. package/docs/patch-workflow.md +2 -1
  368. package/docs/run-logs.md +44 -4
  369. package/docs/testing.md +70 -11
  370. package/package.json +8 -7
  371. package/dist/src/commands/rebase.d.ts +0 -4
  372. package/dist/src/commands/rebase.js +0 -5
  373. package/dist/src/core/patch-artifact-normalize.d.ts +0 -21
  374. package/dist/src/core/patch-artifact-normalize.js +0 -24
  375. package/dist/src/core/patch-lint-reexports.d.ts +0 -7
  376. package/dist/src/core/patch-lint-reexports.js +0 -8
  377. package/dist/src/core/wire-targets.d.ts +0 -7
  378. package/dist/src/core/wire-targets.js +0 -8
@@ -1,38 +1,15 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
- import { mkdtemp, rm, stat, writeFile } from 'node:fs/promises';
3
- import { tmpdir } from 'node:os';
4
- import { basename, join } from 'node:path';
2
+ import { stat } from 'node:fs/promises';
3
+ import { join } from 'node:path';
5
4
  import { GitError } from '../errors/git.js';
6
5
  import { toError } from '../utils/errors.js';
7
- import { pathExists, readText } from '../utils/fs.js';
6
+ import { pathExists } from '../utils/fs.js';
8
7
  import { verbose } from '../utils/logger.js';
9
- import { exec } from '../utils/process.js';
10
- import { chunkPathspecs, ensureGit, git } from './git-base.js';
11
- import { fileExistsInHead, hashObjectBatch, isBinaryFile, listTrackedInHead, } from './git-file-ops.js';
12
- import { mintDisposableGitIndex, readOnlyGitIndexEnv } from './git-readonly-index.js';
8
+ import { chunkPathspecs, git } from './git-base.js';
9
+ import { readNewFileContent, withForcedBinaryAttribute } from './git-diff-new-file.js';
10
+ import { fileExistsInHead, hashObjectBatch, listTrackedInHead } from './git-file-ops.js';
11
+ import { mintDisposableGitIndex } from './git-readonly-index.js';
13
12
  import { getUntrackedFiles, getUntrackedFilesInDir } from './git-status.js';
14
- async function execGitWithAllowedExitCodes(repoDir, args, allowedExitCodes = [0], envOverride) {
15
- // Honour a read-only command's private-index scope: the `--intent-to-add`
16
- // / `update-index` staging dance below is a genuine
17
- // index write, and inside a read-only command it must land on the
18
- // private index, not the primary checkout's.
19
- //
20
- // `envOverride` wins over that scope: a caller that minted an index for
21
- // one specific invocation has said something more specific than the
22
- // ambient scope did.
23
- const readOnlyIndexEnv = readOnlyGitIndexEnv(repoDir);
24
- const env = readOnlyIndexEnv !== undefined || envOverride !== undefined
25
- ? { ...readOnlyIndexEnv, ...envOverride }
26
- : undefined;
27
- const result = await exec('git', args, {
28
- cwd: repoDir,
29
- ...(env !== undefined ? { env } : {}),
30
- });
31
- if (allowedExitCodes.includes(result.exitCode)) {
32
- return result;
33
- }
34
- throw new GitError(result.stderr.trim() || 'Git command failed', args.join(' '));
35
- }
36
13
  /**
37
14
  * Gets the diff for a specific file.
38
15
  *
@@ -46,7 +23,6 @@ async function execGitWithAllowedExitCodes(repoDir, args, allowedExitCodes = [0]
46
23
  * @returns Diff content
47
24
  */
48
25
  export async function getFileDiff(repoDir, filePath) {
49
- await ensureGit();
50
26
  return git(['diff', '--binary', 'HEAD', '--', filePath], repoDir);
51
27
  }
52
28
  /**
@@ -113,22 +89,36 @@ function buildNewFileDiffBody(filePath, content, blobHash) {
113
89
  return diffLines.join('\n') + '\n';
114
90
  }
115
91
  /**
116
- * Generates a unified diff for a new (untracked) file.
92
+ * Defensive check shared by the new-file readers: a directory here means a
93
+ * caller bypassed the expansion layers and handed the leaf reader a path it
94
+ * cannot read. Surfaces an actionable message naming the offending path
95
+ * rather than the raw `EISDIR` the read would throw. This is a recurring bug
96
+ * class, hence the guard at every leaf.
97
+ * @param repoDir - Repository directory
98
+ * @param filePath - Path to the file (relative to repo)
99
+ */
100
+ async function rejectDirectory(repoDir, filePath) {
101
+ const fileStat = await stat(join(repoDir, filePath));
102
+ if (fileStat.isDirectory()) {
103
+ throw new GitError(`expected a file but found a directory at '${filePath}' — caller must expand directory entries before diffing`, `hash-object ${filePath}`);
104
+ }
105
+ }
106
+ /**
107
+ * Generates a unified diff for a new (untracked) file. A file that is binary
108
+ * or not valid UTF-8 (see {@link readNewFileContent}) is routed through
109
+ * {@link generateBinaryFilePatch} instead, because a text body cannot carry
110
+ * its bytes faithfully.
117
111
  * @param repoDir - Repository directory
118
112
  * @param filePath - Path to the file (relative to repo)
119
113
  * @returns Diff content in unified diff format
120
114
  */
121
115
  export async function generateNewFileDiff(repoDir, filePath) {
122
116
  const fullPath = join(repoDir, filePath);
123
- // Defensive check: a directory here means a caller bypassed the expansion
124
- // layers and handed the leaf reader a path it cannot read. Surface it with
125
- // an actionable message naming the offending path rather than the raw
126
- // `EISDIR` that `readText` would throw.
127
- const fileStat = await stat(fullPath);
128
- if (fileStat.isDirectory()) {
129
- throw new GitError(`expected a file but found a directory at '${filePath}' — caller must expand directory entries before diffing`, `hash-object ${filePath}`);
117
+ await rejectDirectory(repoDir, filePath);
118
+ const file = await readNewFileContent(repoDir, filePath);
119
+ if (file.binary) {
120
+ return generateBinaryFilePatch(repoDir, filePath);
130
121
  }
131
- const content = await readText(fullPath);
132
122
  // Compute the abbreviated git blob hash for the index line
133
123
  let blobHash = '0000000000';
134
124
  try {
@@ -138,7 +128,7 @@ export async function generateNewFileDiff(repoDir, filePath) {
138
128
  catch (error) {
139
129
  verbose(`git hash-object failed for ${filePath}; falling back to zero blob hash: ${toError(error).message}`);
140
130
  }
141
- return buildNewFileDiffBody(filePath, content, blobHash);
131
+ return buildNewFileDiffBody(filePath, file.content, blobHash);
142
132
  }
143
133
  /**
144
134
  * Generates a patch for a file.
@@ -150,7 +140,6 @@ export async function generateNewFileDiff(repoDir, filePath) {
150
140
  * @returns Diff content in unified diff format
151
141
  */
152
142
  export async function generateFullFilePatch(repoDir, filePath) {
153
- await ensureGit();
154
143
  // If file exists in HEAD, use standard git diff HEAD -- <file>
155
144
  // This generates a contextual diff that is safer for rebasing
156
145
  if (await fileExistsInHead(repoDir, filePath)) {
@@ -159,56 +148,6 @@ export async function generateFullFilePatch(repoDir, filePath) {
159
148
  // If file is new/untracked, use the full-file "new file" format
160
149
  return generateNewFileDiff(repoDir, filePath);
161
150
  }
162
- /**
163
- * Generates a unified diff between base content and current file content.
164
- * @param repoDir - Repository directory
165
- * @param filePath - Path to the file (relative to repo)
166
- * @param baseContent - The base content to diff against
167
- * @returns Unified diff in git format
168
- */
169
- export async function generateModificationDiff(repoDir, filePath, baseContent) {
170
- const fullPath = join(repoDir, filePath);
171
- const currentContent = await readText(fullPath);
172
- // If contents are identical, return empty diff
173
- if (baseContent === currentContent) {
174
- return '';
175
- }
176
- const tempDir = await mkdtemp(join(tmpdir(), 'fireforge-diff-'));
177
- const tempFile = join(tempDir, basename(filePath));
178
- try {
179
- await writeFile(tempFile, baseContent);
180
- // git diff --no-index exits code 1 when files differ — that's normal
181
- const result = await execGitWithAllowedExitCodes(repoDir, ['diff', '--no-index', '--', tempFile, fullPath], [0, 1]);
182
- const output = result.stdout;
183
- if (!output) {
184
- return '';
185
- }
186
- // Post-process: fix paths in the diff header only (before the first @@ hunk)
187
- const lines = output.split('\n');
188
- let pastHeader = false;
189
- const fixedLines = lines.map((line) => {
190
- if (!pastHeader && line.startsWith('@@')) {
191
- pastHeader = true;
192
- }
193
- if (!pastHeader) {
194
- if (line.startsWith('diff --git')) {
195
- return `diff --git a/${filePath} b/${filePath}`;
196
- }
197
- if (line.startsWith('--- ')) {
198
- return `--- a/${filePath}`;
199
- }
200
- if (line.startsWith('+++ ')) {
201
- return `+++ b/${filePath}`;
202
- }
203
- }
204
- return line;
205
- });
206
- return fixedLines.join('\n');
207
- }
208
- finally {
209
- await rm(tempDir, { recursive: true, force: true });
210
- }
211
- }
212
151
  /**
213
152
  * Gets the diff for all modified files, including untracked (new) files.
214
153
  *
@@ -219,16 +158,16 @@ export async function generateModificationDiff(repoDir, filePath, baseContent) {
219
158
  * @returns Diff content
220
159
  */
221
160
  export async function getAllDiff(repoDir) {
222
- await ensureGit();
223
161
  // Get diff for tracked files
224
162
  const trackedDiff = await git(['diff', '--binary', 'HEAD'], repoDir);
225
163
  // Get untracked files (properly expanded, not directories)
226
164
  const untrackedFiles = await getUntrackedFiles(repoDir);
227
- // Classify first. `isBinaryFile` reads the file's first 8 KB directly and
228
- // spawns nothing, so the only per-file process in this loop is the
165
+ // Classify first. `readNewFileContent` reads the file directly and spawns
166
+ // nothing, so the only per-file process in this loop would be the
229
167
  // `git hash-object` inside `generateNewFileDiff` — one spawn per untracked
230
168
  // TEXT file, uncapped, against a Firefox tree where "untracked" can mean a
231
- // very large number. Hence classify → one batched hash → render.
169
+ // very large number. Hence classify → one batched hash → render. The
170
+ // classification carries each text file's content so it is read once.
232
171
  //
233
172
  // Everything else here is deliberately untouched: `git diff HEAD` (with
234
173
  // renames, unlike the `--no-renames` scoped sibling), the `ls-files`
@@ -236,40 +175,33 @@ export async function getAllDiff(repoDir) {
236
175
  // sentinel are all observable. `getAllDiff`'s output is SHA-256'd into
237
176
  // every tree fingerprint (tree-store.ts) and written verbatim into `.patch`
238
177
  // files (export-all.ts), so its bytes are a contract.
239
- const binaryFiles = [];
240
- const textFiles = [];
178
+ const classified = new Map();
241
179
  for (const file of untrackedFiles) {
242
- if (await isBinaryFile(repoDir, file)) {
243
- binaryFiles.push(file);
244
- }
245
- else {
246
- textFiles.push(file);
247
- }
180
+ await rejectDirectory(repoDir, file);
181
+ classified.set(file, await readNewFileContent(repoDir, file));
248
182
  }
249
- const textHashes = await hashObjectBatch(repoDir, textFiles.map((file) => join(repoDir, file)));
183
+ const textHashes = await hashObjectBatch(repoDir, untrackedFiles
184
+ .filter((file) => classified.get(file)?.binary === false)
185
+ .map((file) => join(repoDir, file)));
250
186
  // Binary patches stage into the index and must stay serial; text patches
251
187
  // are pure formatting over the already-batched hashes.
252
188
  const binaryDiffs = new Map();
253
- for (const file of binaryFiles) {
254
- binaryDiffs.set(file, await generateBinaryFilePatch(repoDir, file));
189
+ for (const file of untrackedFiles) {
190
+ if (classified.get(file)?.binary === true) {
191
+ binaryDiffs.set(file, await generateBinaryFilePatch(repoDir, file));
192
+ }
255
193
  }
256
194
  const untrackedDiffs = [];
257
195
  for (const file of untrackedFiles) {
258
- const binary = binaryDiffs.get(file);
259
- if (binary !== undefined) {
260
- untrackedDiffs.push(binary);
196
+ const entry = classified.get(file);
197
+ if (entry === undefined || entry.binary) {
198
+ const binary = binaryDiffs.get(file);
199
+ if (binary !== undefined)
200
+ untrackedDiffs.push(binary);
261
201
  continue;
262
202
  }
263
203
  const fullPath = join(repoDir, file);
264
- // Same defensive directory check `generateNewFileDiff` carries: a
265
- // directory here means a caller bypassed the expansion layers, and its
266
- // comment records this as a recurring bug class.
267
- const fileStat = await stat(fullPath);
268
- if (fileStat.isDirectory()) {
269
- throw new GitError(`expected a file but found a directory at '${file}' — caller must expand directory entries before diffing`, `hash-object ${file}`);
270
- }
271
- const content = await readText(fullPath);
272
- untrackedDiffs.push(buildNewFileDiffBody(file, content, abbreviateBlobHash(textHashes.get(fullPath))));
204
+ untrackedDiffs.push(buildNewFileDiffBody(file, entry.content, abbreviateBlobHash(textHashes.get(fullPath))));
273
205
  }
274
206
  // Combine all diffs — each already ends with \n, so concatenate directly
275
207
  // to avoid inserting blank lines between diff sections.
@@ -376,7 +308,6 @@ async function buildTrackedSections(repoDir, trackedFiles) {
376
308
  * @returns Combined diff content
377
309
  */
378
310
  export async function getDiffForFilesAgainstHead(repoDir, files) {
379
- await ensureGit();
380
311
  // Expand any directory entries (paths ending with `/`) into their
381
312
  // individual untracked files before diffing. `git status --porcelain=v1`
382
313
  // reports collapsed untracked directories as `?? dir/`, and every caller
@@ -435,25 +366,27 @@ export async function getDiffForFilesAgainstHead(repoDir, files) {
435
366
  sectionByFile.set(file, innerDiff);
436
367
  continue;
437
368
  }
438
- if (await isBinaryFile(repoDir, file)) {
369
+ // Binary — by git's NUL heuristic or because the bytes are not valid
370
+ // UTF-8 — takes the byte-faithful binary-patch arm.
371
+ const newFile = await readNewFileContent(repoDir, file);
372
+ if (newFile.binary) {
439
373
  const diff = await generateBinaryFilePatch(repoDir, file);
440
374
  if (diff.trim())
441
375
  sectionByFile.set(file, diff);
442
376
  continue;
443
377
  }
444
- newTextFiles.push(file);
378
+ newTextFiles.push({ file, content: newFile.content });
445
379
  }
446
380
  // Batch 3: blob hashes for every new text file in one `git hash-object`.
447
381
  // A miss (rare: a path became unreadable after the stat above) falls back to
448
382
  // the zero hash with the same verbose log the per-file path emitted.
449
- const blobHashes = await hashObjectBatch(repoDir, newTextFiles.map((file) => join(repoDir, file)));
450
- for (const file of newTextFiles) {
451
- const fullPath = join(repoDir, file);
452
- const fullHash = blobHashes.get(fullPath);
383
+ const blobHashes = await hashObjectBatch(repoDir, newTextFiles.map(({ file }) => join(repoDir, file)));
384
+ for (const { file, content } of newTextFiles) {
385
+ const fullHash = blobHashes.get(join(repoDir, file));
453
386
  if (fullHash === undefined) {
454
387
  verbose(`git hash-object failed for ${file}; falling back to zero blob hash`);
455
388
  }
456
- const body = buildNewFileDiffBody(file, await readText(fullPath), abbreviateBlobHash(fullHash));
389
+ const body = buildNewFileDiffBody(file, content, abbreviateBlobHash(fullHash));
457
390
  if (body.trim())
458
391
  sectionByFile.set(file, body);
459
392
  }
@@ -484,7 +417,6 @@ export async function getDiffForFilesAgainstHead(repoDir, files) {
484
417
  * @returns Diff content for the staged files
485
418
  */
486
419
  export async function getStagedDiffForFiles(repoDir, files) {
487
- await ensureGit();
488
420
  return git(['diff', '--binary', '--cached', 'HEAD', '--', ...files], repoDir);
489
421
  }
490
422
  /**
@@ -507,22 +439,31 @@ function runWithBinaryStagingLock(task) {
507
439
  * Generates a GIT binary patch for a binary file.
508
440
  * For tracked files, uses `git diff --binary HEAD`.
509
441
  * For untracked files, temporarily stages with `--intent-to-add` to produce a diff.
442
+ *
443
+ * Every `git diff` here runs under {@link withForcedBinaryAttribute}: a
444
+ * caller sends a file down this arm because a text body cannot carry its
445
+ * bytes (NUL, or not valid UTF-8), and git's own NUL-only heuristic would
446
+ * otherwise still render the non-UTF-8 case as text.
510
447
  * @param repoDir - Repository directory
511
448
  * @param filePath - File path (relative to repo root)
512
449
  * @returns The binary diff string, or empty string if no diff
513
450
  */
514
451
  export async function generateBinaryFilePatch(repoDir, filePath) {
515
- await ensureGit();
452
+ return withForcedBinaryAttribute(filePath, (gitConfigArgs) => generateBinaryFilePatchWithConfig(repoDir, filePath, gitConfigArgs));
453
+ }
454
+ /**
455
+ * The body of {@link generateBinaryFilePatch}, with the forced-binary config
456
+ * arguments already resolved.
457
+ * @param repoDir - Repository directory
458
+ * @param filePath - File path (relative to repo root)
459
+ * @param gitConfigArgs - `-c` arguments spliced ahead of every `diff`
460
+ * @returns The binary diff string, or empty string if no diff
461
+ */
462
+ async function generateBinaryFilePatchWithConfig(repoDir, filePath, gitConfigArgs) {
516
463
  // Try tracked file diff first (read-only — no index lock needed)
517
- const result = await execGitWithAllowedExitCodes(repoDir, [
518
- 'diff',
519
- '--binary',
520
- 'HEAD',
521
- '--',
522
- filePath,
523
- ]);
524
- if (result.stdout.trim())
525
- return result.stdout;
464
+ const tracked = await git([...gitConfigArgs, 'diff', '--binary', 'HEAD', '--', filePath], repoDir);
465
+ if (tracked.trim())
466
+ return tracked;
526
467
  // The diff for an untracked file has to come from an index entry, so one
527
468
  // has to be written. Writing it to a PRIVATE index is what keeps that
528
469
  // write invisible: the primary `.git/index` never moves, so a concurrent
@@ -539,9 +480,10 @@ export async function generateBinaryFilePatch(repoDir, filePath) {
539
480
  const disposable = await mintDisposableGitIndex(repoDir);
540
481
  if (disposable !== undefined) {
541
482
  try {
542
- await execGitWithAllowedExitCodes(repoDir, ['add', '--intent-to-add', '--', filePath], [0], disposable.env);
543
- const diffResult = await execGitWithAllowedExitCodes(repoDir, ['diff', '--binary', '--', filePath], [0], disposable.env);
544
- return diffResult.stdout;
483
+ await git(['add', '--intent-to-add', '--', filePath], repoDir, { env: disposable.env });
484
+ return await git([...gitConfigArgs, 'diff', '--binary', '--', filePath], repoDir, {
485
+ env: disposable.env,
486
+ });
545
487
  }
546
488
  finally {
547
489
  await disposable.dispose();
@@ -562,16 +504,10 @@ export async function generateBinaryFilePatch(repoDir, filePath) {
562
504
  // between the read-only diff above and this staging block (a concurrent
563
505
  // writer, or a staged-add whose worktree file was deleted). Restoring
564
506
  // the captured entry is exact regardless of how the entry got there.
565
- const priorEntry = (await execGitWithAllowedExitCodes(repoDir, ['ls-files', '--stage', '--', filePath])).stdout;
507
+ const priorEntry = await git(['ls-files', '--stage', '--', filePath], repoDir);
566
508
  try {
567
- await execGitWithAllowedExitCodes(repoDir, ['add', '--intent-to-add', '--', filePath]);
568
- const diffResult = await execGitWithAllowedExitCodes(repoDir, [
569
- 'diff',
570
- '--binary',
571
- '--',
572
- filePath,
573
- ]);
574
- return diffResult.stdout;
509
+ await git(['add', '--intent-to-add', '--', filePath], repoDir);
510
+ return await git([...gitConfigArgs, 'diff', '--binary', '--', filePath], repoDir);
575
511
  }
576
512
  finally {
577
513
  await restoreIndexEntry(repoDir, filePath, priorEntry);
@@ -595,13 +531,8 @@ async function restoreIndexEntry(repoDir, filePath, priorEntry) {
595
531
  const mode = entryMatch?.[1];
596
532
  const oid = entryMatch?.[2];
597
533
  if (mode === undefined || oid === undefined) {
598
- await execGitWithAllowedExitCodes(repoDir, ['reset', 'HEAD', '--', filePath]);
534
+ await git(['reset', 'HEAD', '--', filePath], repoDir);
599
535
  return;
600
536
  }
601
- await execGitWithAllowedExitCodes(repoDir, [
602
- 'update-index',
603
- '--add',
604
- '--cacheinfo',
605
- `${mode},${oid},${filePath}`,
606
- ]);
537
+ await git(['update-index', '--add', '--cacheinfo', `${mode},${oid},${filePath}`], repoDir);
607
538
  }
@@ -77,13 +77,6 @@ export declare function hashObjectBatch(repoDir: string, fullPaths: string[]): P
77
77
  * @returns File content or null if file doesn't exist at that ref
78
78
  */
79
79
  export declare function getFileContentAtRef(repoDir: string, filePath: string, ref?: string): Promise<string | null>;
80
- /**
81
- * Gets the content of a file from HEAD commit.
82
- * @param repoDir - Repository directory
83
- * @param filePath - Path to the file (relative to repo)
84
- * @returns File content or null if file doesn't exist in HEAD
85
- */
86
- export declare function getFileContentFromHead(repoDir: string, filePath: string): Promise<string | null>;
87
80
  /**
88
81
  * Checks if a file is binary by looking for NUL bytes in the first 8KB.
89
82
  * Uses the same heuristic as git.
@@ -11,27 +11,16 @@ import { chunkPathspecs, ensureGit, git } from './git-base.js';
11
11
  * @param filePath - Path to the file (relative to repo)
12
12
  */
13
13
  export async function restoreTrackedPath(repoDir, filePath) {
14
- await ensureGit();
15
14
  await git(['restore', '--source', 'HEAD', '--staged', '--worktree', '--', filePath], repoDir);
16
15
  }
17
- /**
18
- * Removes an untracked path from disk.
19
- * @param repoDir - Repository directory
20
- * @param filePath - Path to the file (relative to repo)
21
- */
22
- async function removeUntrackedPath(repoDir, filePath) {
23
- const fullPath = join(repoDir, filePath);
24
- await removeFile(fullPath);
25
- }
26
16
  /**
27
17
  * Removes a path that is present only in the index/worktree and not in HEAD.
28
18
  * @param repoDir - Repository directory
29
19
  * @param filePath - Path to remove
30
20
  */
31
21
  async function removeAddedPath(repoDir, filePath) {
32
- await ensureGit();
33
22
  await git(['reset', 'HEAD', '--', filePath], repoDir);
34
- await removeUntrackedPath(repoDir, filePath);
23
+ await removeFile(join(repoDir, filePath));
35
24
  }
36
25
  /**
37
26
  * Discards a status entry according to its git state.
@@ -40,7 +29,7 @@ async function removeAddedPath(repoDir, filePath) {
40
29
  */
41
30
  export async function discardStatusEntry(repoDir, entry) {
42
31
  if (entry.isUntracked) {
43
- await removeUntrackedPath(repoDir, entry.file);
32
+ await removeFile(join(repoDir, entry.file));
44
33
  return;
45
34
  }
46
35
  if (entry.isRenameOrCopy && entry.originalPath) {
@@ -65,7 +54,6 @@ export async function discardStatusEntry(repoDir, entry) {
65
54
  * @param files - File paths to stage (relative to repo)
66
55
  */
67
56
  export async function stageFiles(repoDir, files) {
68
- await ensureGit();
69
57
  await git(['add', '--', ...files], repoDir);
70
58
  }
71
59
  /**
@@ -74,7 +62,6 @@ export async function stageFiles(repoDir, files) {
74
62
  * @param files - File paths to unstage (relative to repo)
75
63
  */
76
64
  export async function unstageFiles(repoDir, files) {
77
- await ensureGit();
78
65
  await git(['reset', 'HEAD', '--', ...files], repoDir);
79
66
  }
80
67
  /**
@@ -84,7 +71,6 @@ export async function unstageFiles(repoDir, files) {
84
71
  * @returns true if file exists in HEAD
85
72
  */
86
73
  export async function fileExistsInHead(repoDir, filePath) {
87
- await ensureGit();
88
74
  return (await git(['ls-tree', 'HEAD', '--', filePath], repoDir)).trim().length > 0;
89
75
  }
90
76
  /**
@@ -107,7 +93,6 @@ export async function listTrackedInHead(repoDir, files) {
107
93
  const tracked = new Set();
108
94
  if (files.length === 0)
109
95
  return tracked;
110
- await ensureGit();
111
96
  const wanted = new Set(files);
112
97
  for (const chunk of chunkPathspecs(files)) {
113
98
  const output = await git(['ls-tree', '-r', 'HEAD', '--name-only', '-z', '--', ...chunk], repoDir);
@@ -151,8 +136,8 @@ export async function hashObjectBatch(repoDir, fullPaths) {
151
136
  const result = await exec('git', ['hash-object', '--', ...chunk], { cwd: repoDir });
152
137
  const lines = result.stdout.split('\n').filter((line) => line.length > 0);
153
138
  if (result.exitCode === 0 && lines.length === chunk.length) {
154
- for (let i = 0; i < chunk.length; i++) {
155
- hashes.set(chunk[i], lines[i]);
139
+ for (const [i, path] of chunk.entries()) {
140
+ hashes.set(path, lines[i]);
156
141
  }
157
142
  continue;
158
143
  }
@@ -190,15 +175,6 @@ export async function getFileContentAtRef(repoDir, filePath, ref = 'HEAD') {
190
175
  }
191
176
  return result.stdout;
192
177
  }
193
- /**
194
- * Gets the content of a file from HEAD commit.
195
- * @param repoDir - Repository directory
196
- * @param filePath - Path to the file (relative to repo)
197
- * @returns File content or null if file doesn't exist in HEAD
198
- */
199
- export async function getFileContentFromHead(repoDir, filePath) {
200
- return getFileContentAtRef(repoDir, filePath, 'HEAD');
201
- }
202
178
  /**
203
179
  * Checks if a file is binary by looking for NUL bytes in the first 8KB.
204
180
  * Uses the same heuristic as git.
@@ -43,8 +43,6 @@
43
43
  * @returns `{ GIT_INDEX_FILE }` when scoped, otherwise undefined
44
44
  */
45
45
  export declare function readOnlyGitIndexEnv(cwd: string): Record<string, string> | undefined;
46
- /** True while a private-index scope is installed (diagnostics and tests). */
47
- export declare function hasReadOnlyGitIndexScope(): boolean;
48
46
  /**
49
47
  * Runs `operation` with FireForge's git plumbing pointed at a private
50
48
  * index seeded from `repoDir`'s own, so the primary checkout's
@@ -64,10 +64,6 @@ export function readOnlyGitIndexEnv(cwd) {
64
64
  return undefined;
65
65
  return { GIT_INDEX_FILE: activeScope.indexFile };
66
66
  }
67
- /** True while a private-index scope is installed (diagnostics and tests). */
68
- export function hasReadOnlyGitIndexScope() {
69
- return activeScope !== undefined;
70
- }
71
67
  /**
72
68
  * Resolves a repository's git directory. Returns undefined for anything
73
69
  * that is not a readable git checkout — `engine/` is not always one
@@ -1,6 +1,13 @@
1
1
  import type { GitStatusEntry } from './git-base.js';
2
2
  /**
3
3
  * Parses NUL-delimited porcelain status output.
4
+ *
5
+ * A rename or copy occupies TWO NUL records (`XY new\0old\0`), and the `R`/`C`
6
+ * may sit in EITHER column: git >= 2.18 detects renames in the worktree too
7
+ * (`status.renames`), emitting ` R new\0old\0` for an unstaged rename and
8
+ * `RM`/`R ` for a staged one. Only consuming the second record for an index
9
+ * `R`/`C` turned the worktree form's `old` field into a bogus extra entry
10
+ * whose status was whatever its first two path characters happened to be.
4
11
  * @param output - Raw git status output
5
12
  * @returns Parsed entries
6
13
  */
@@ -1,8 +1,25 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  import { warn } from '../utils/logger.js';
3
- import { ensureGit, git } from './git-base.js';
3
+ import { git } from './git-base.js';
4
+ /**
5
+ * True when a porcelain status column carries a rename or copy, i.e. the
6
+ * record is followed by a second NUL-terminated field holding the original
7
+ * path.
8
+ * @param column - A single status column character
9
+ * @returns Whether the column is `R` or `C`
10
+ */
11
+ function isRenameOrCopyColumn(column) {
12
+ return column === 'R' || column === 'C';
13
+ }
4
14
  /**
5
15
  * Parses NUL-delimited porcelain status output.
16
+ *
17
+ * A rename or copy occupies TWO NUL records (`XY new\0old\0`), and the `R`/`C`
18
+ * may sit in EITHER column: git >= 2.18 detects renames in the worktree too
19
+ * (`status.renames`), emitting ` R new\0old\0` for an unstaged rename and
20
+ * `RM`/`R ` for a staged one. Only consuming the second record for an index
21
+ * `R`/`C` turned the worktree form's `old` field into a bogus extra entry
22
+ * whose status was whatever its first two path characters happened to be.
6
23
  * @param output - Raw git status output
7
24
  * @returns Parsed entries
8
25
  */
@@ -18,7 +35,7 @@ export function parsePorcelainStatus(output) {
18
35
  const worktreeStatus = record[1] ?? ' ';
19
36
  const status = `${indexStatus}${worktreeStatus}`;
20
37
  const pathField = record.slice(3);
21
- const isRenameOrCopy = indexStatus === 'R' || indexStatus === 'C';
38
+ const isRenameOrCopy = isRenameOrCopyColumn(indexStatus) || isRenameOrCopyColumn(worktreeStatus);
22
39
  const originalPath = isRenameOrCopy ? records[i + 1] : undefined;
23
40
  if (isRenameOrCopy) {
24
41
  i++;
@@ -42,7 +59,6 @@ export function parsePorcelainStatus(output) {
42
59
  * @returns Parsed git status entries
43
60
  */
44
61
  export async function getWorkingTreeStatus(repoDir) {
45
- await ensureGit();
46
62
  return parsePorcelainStatus(await git(['status', '--porcelain=v1', '-z'], repoDir));
47
63
  }
48
64
  /** Default per-directory cap for untracked-directory expansion. */
@@ -57,7 +73,7 @@ export function resolveMaxUntrackedFilesPerDir() {
57
73
  return DEFAULT_MAX_UNTRACKED_FILES_PER_DIR;
58
74
  const parsed = Number.parseInt(raw, 10);
59
75
  if (!Number.isFinite(parsed) || parsed <= 0) {
60
- warn(`Ignoring FIREFORGE_MAX_UNTRACKED_FILES="${raw}" — expected a positive integer. Falling back to ${String(DEFAULT_MAX_UNTRACKED_FILES_PER_DIR)}.`);
76
+ warn(`Ignoring FIREFORGE_MAX_UNTRACKED_FILES="${raw}" — expected a positive integer. Falling back to ${DEFAULT_MAX_UNTRACKED_FILES_PER_DIR}.`);
61
77
  return DEFAULT_MAX_UNTRACKED_FILES_PER_DIR;
62
78
  }
63
79
  return parsed;
@@ -86,7 +102,7 @@ export async function expandUntrackedDirectoryEntries(repoDir, entries) {
86
102
  }
87
103
  const individualFiles = await getUntrackedFilesInDir(repoDir, entry.file);
88
104
  if (individualFiles.length > maxPerDir) {
89
- warn(`Untracked directory ${entry.file} contains ${String(individualFiles.length)} files — only the first ${String(maxPerDir)} are listed. Add a .gitignore entry or clean the directory.`);
105
+ warn(`Untracked directory ${entry.file} contains ${individualFiles.length} files — only the first ${maxPerDir} are listed. Add a .gitignore entry or clean the directory.`);
90
106
  }
91
107
  for (const file of individualFiles.slice(0, maxPerDir)) {
92
108
  expanded.push({
@@ -108,7 +124,6 @@ export async function expandUntrackedDirectoryEntries(repoDir, entries) {
108
124
  * @returns List of untracked file paths
109
125
  */
110
126
  export async function getUntrackedFiles(repoDir) {
111
- await ensureGit();
112
127
  // Use git ls-files to get all untracked files, which properly expands directories
113
128
  const output = await git(['ls-files', '--others', '--exclude-standard'], repoDir);
114
129
  return output.split('\n').filter((line) => line.trim().length > 0);
@@ -121,7 +136,6 @@ export async function getUntrackedFiles(repoDir) {
121
136
  * @returns List of untracked file paths relative to repo root
122
137
  */
123
138
  export async function getUntrackedFilesInDir(repoDir, dir) {
124
- await ensureGit();
125
139
  const output = await git(['ls-files', '--others', '--exclude-standard', '--', dir], repoDir);
126
140
  return output.split('\n').filter((line) => line.trim().length > 0);
127
141
  }
@@ -133,7 +147,6 @@ export async function getUntrackedFilesInDir(repoDir, dir) {
133
147
  * @returns List of modified file paths relative to repo root
134
148
  */
135
149
  export async function getModifiedFilesInDir(repoDir, dir) {
136
- await ensureGit();
137
150
  const output = await git(['diff', '--name-only', 'HEAD', '--', dir], repoDir);
138
151
  return output.split('\n').filter((line) => line.trim().length > 0);
139
152
  }
@@ -146,7 +159,6 @@ export async function getModifiedFilesInDir(repoDir, dir) {
146
159
  export async function getDirtyFiles(repoDir, files) {
147
160
  if (files.length === 0)
148
161
  return [];
149
- await ensureGit();
150
162
  // Check both staged and unstaged changes for the given files
151
163
  const trackedOutput = await git(['diff', '--name-only', 'HEAD', '--', ...files], repoDir);
152
164
  const tracked = trackedOutput.split('\n').filter((line) => line.trim().length > 0);
@@ -103,12 +103,6 @@ export declare function getCurrentBranch(repoDir: string): Promise<string>;
103
103
  * @param repoDir - Repository directory
104
104
  */
105
105
  export declare function resetChanges(repoDir: string): Promise<void>;
106
- /**
107
- * Creates a commit with all current changes.
108
- * @param repoDir - Repository directory
109
- * @param message - Commit message
110
- */
111
- export declare function commit(repoDir: string, message: string): Promise<void>;
112
106
  /**
113
107
  * Gets the status of files with their status codes.
114
108
  * @param repoDir - Repository directory