gitx.do 0.1.0 → 0.1.2

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 (344) hide show
  1. package/README.md +40 -353
  2. package/dist/do/logger.d.ts +50 -0
  3. package/dist/do/logger.d.ts.map +1 -0
  4. package/dist/do/logger.js +122 -0
  5. package/dist/do/logger.js.map +1 -0
  6. package/dist/{durable-object → do}/schema.d.ts +3 -3
  7. package/dist/do/schema.d.ts.map +1 -0
  8. package/dist/{durable-object → do}/schema.js +4 -3
  9. package/dist/do/schema.js.map +1 -0
  10. package/dist/do/types.d.ts +267 -0
  11. package/dist/do/types.d.ts.map +1 -0
  12. package/dist/do/types.js +62 -0
  13. package/dist/do/types.js.map +1 -0
  14. package/dist/index.d.ts +15 -415
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +31 -483
  17. package/dist/index.js.map +1 -1
  18. package/package.json +13 -21
  19. package/dist/cli/commands/add.d.ts +0 -174
  20. package/dist/cli/commands/add.d.ts.map +0 -1
  21. package/dist/cli/commands/add.js +0 -131
  22. package/dist/cli/commands/add.js.map +0 -1
  23. package/dist/cli/commands/blame.d.ts +0 -259
  24. package/dist/cli/commands/blame.d.ts.map +0 -1
  25. package/dist/cli/commands/blame.js +0 -609
  26. package/dist/cli/commands/blame.js.map +0 -1
  27. package/dist/cli/commands/branch.d.ts +0 -249
  28. package/dist/cli/commands/branch.d.ts.map +0 -1
  29. package/dist/cli/commands/branch.js +0 -693
  30. package/dist/cli/commands/branch.js.map +0 -1
  31. package/dist/cli/commands/commit.d.ts +0 -182
  32. package/dist/cli/commands/commit.d.ts.map +0 -1
  33. package/dist/cli/commands/commit.js +0 -437
  34. package/dist/cli/commands/commit.js.map +0 -1
  35. package/dist/cli/commands/diff.d.ts +0 -464
  36. package/dist/cli/commands/diff.d.ts.map +0 -1
  37. package/dist/cli/commands/diff.js +0 -958
  38. package/dist/cli/commands/diff.js.map +0 -1
  39. package/dist/cli/commands/log.d.ts +0 -239
  40. package/dist/cli/commands/log.d.ts.map +0 -1
  41. package/dist/cli/commands/log.js +0 -535
  42. package/dist/cli/commands/log.js.map +0 -1
  43. package/dist/cli/commands/merge.d.ts +0 -106
  44. package/dist/cli/commands/merge.d.ts.map +0 -1
  45. package/dist/cli/commands/merge.js +0 -55
  46. package/dist/cli/commands/merge.js.map +0 -1
  47. package/dist/cli/commands/review.d.ts +0 -457
  48. package/dist/cli/commands/review.d.ts.map +0 -1
  49. package/dist/cli/commands/review.js +0 -533
  50. package/dist/cli/commands/review.js.map +0 -1
  51. package/dist/cli/commands/status.d.ts +0 -269
  52. package/dist/cli/commands/status.d.ts.map +0 -1
  53. package/dist/cli/commands/status.js +0 -493
  54. package/dist/cli/commands/status.js.map +0 -1
  55. package/dist/cli/commands/web.d.ts +0 -199
  56. package/dist/cli/commands/web.d.ts.map +0 -1
  57. package/dist/cli/commands/web.js +0 -696
  58. package/dist/cli/commands/web.js.map +0 -1
  59. package/dist/cli/fs-adapter.d.ts +0 -656
  60. package/dist/cli/fs-adapter.d.ts.map +0 -1
  61. package/dist/cli/fs-adapter.js +0 -1179
  62. package/dist/cli/fs-adapter.js.map +0 -1
  63. package/dist/cli/fsx-cli-adapter.d.ts +0 -359
  64. package/dist/cli/fsx-cli-adapter.d.ts.map +0 -1
  65. package/dist/cli/fsx-cli-adapter.js +0 -619
  66. package/dist/cli/fsx-cli-adapter.js.map +0 -1
  67. package/dist/cli/index.d.ts +0 -387
  68. package/dist/cli/index.d.ts.map +0 -1
  69. package/dist/cli/index.js +0 -523
  70. package/dist/cli/index.js.map +0 -1
  71. package/dist/cli/ui/components/DiffView.d.ts +0 -7
  72. package/dist/cli/ui/components/DiffView.d.ts.map +0 -1
  73. package/dist/cli/ui/components/DiffView.js +0 -11
  74. package/dist/cli/ui/components/DiffView.js.map +0 -1
  75. package/dist/cli/ui/components/ErrorDisplay.d.ts +0 -6
  76. package/dist/cli/ui/components/ErrorDisplay.d.ts.map +0 -1
  77. package/dist/cli/ui/components/ErrorDisplay.js +0 -11
  78. package/dist/cli/ui/components/ErrorDisplay.js.map +0 -1
  79. package/dist/cli/ui/components/FuzzySearch.d.ts +0 -9
  80. package/dist/cli/ui/components/FuzzySearch.d.ts.map +0 -1
  81. package/dist/cli/ui/components/FuzzySearch.js +0 -12
  82. package/dist/cli/ui/components/FuzzySearch.js.map +0 -1
  83. package/dist/cli/ui/components/LoadingSpinner.d.ts +0 -6
  84. package/dist/cli/ui/components/LoadingSpinner.d.ts.map +0 -1
  85. package/dist/cli/ui/components/LoadingSpinner.js +0 -10
  86. package/dist/cli/ui/components/LoadingSpinner.js.map +0 -1
  87. package/dist/cli/ui/components/NavigationList.d.ts +0 -9
  88. package/dist/cli/ui/components/NavigationList.d.ts.map +0 -1
  89. package/dist/cli/ui/components/NavigationList.js +0 -11
  90. package/dist/cli/ui/components/NavigationList.js.map +0 -1
  91. package/dist/cli/ui/components/ScrollableContent.d.ts +0 -8
  92. package/dist/cli/ui/components/ScrollableContent.d.ts.map +0 -1
  93. package/dist/cli/ui/components/ScrollableContent.js +0 -11
  94. package/dist/cli/ui/components/ScrollableContent.js.map +0 -1
  95. package/dist/cli/ui/components/index.d.ts +0 -7
  96. package/dist/cli/ui/components/index.d.ts.map +0 -1
  97. package/dist/cli/ui/components/index.js +0 -9
  98. package/dist/cli/ui/components/index.js.map +0 -1
  99. package/dist/cli/ui/terminal-ui.d.ts +0 -52
  100. package/dist/cli/ui/terminal-ui.d.ts.map +0 -1
  101. package/dist/cli/ui/terminal-ui.js +0 -121
  102. package/dist/cli/ui/terminal-ui.js.map +0 -1
  103. package/dist/do/BashModule.d.ts +0 -871
  104. package/dist/do/BashModule.d.ts.map +0 -1
  105. package/dist/do/BashModule.js +0 -1143
  106. package/dist/do/BashModule.js.map +0 -1
  107. package/dist/do/FsModule.d.ts +0 -601
  108. package/dist/do/FsModule.d.ts.map +0 -1
  109. package/dist/do/FsModule.js +0 -1120
  110. package/dist/do/FsModule.js.map +0 -1
  111. package/dist/do/GitModule.d.ts +0 -635
  112. package/dist/do/GitModule.d.ts.map +0 -1
  113. package/dist/do/GitModule.js +0 -781
  114. package/dist/do/GitModule.js.map +0 -1
  115. package/dist/do/GitRepoDO.d.ts +0 -281
  116. package/dist/do/GitRepoDO.d.ts.map +0 -1
  117. package/dist/do/GitRepoDO.js +0 -479
  118. package/dist/do/GitRepoDO.js.map +0 -1
  119. package/dist/do/bash-ast.d.ts +0 -246
  120. package/dist/do/bash-ast.d.ts.map +0 -1
  121. package/dist/do/bash-ast.js +0 -888
  122. package/dist/do/bash-ast.js.map +0 -1
  123. package/dist/do/container-executor.d.ts +0 -491
  124. package/dist/do/container-executor.d.ts.map +0 -1
  125. package/dist/do/container-executor.js +0 -730
  126. package/dist/do/container-executor.js.map +0 -1
  127. package/dist/do/index.d.ts +0 -53
  128. package/dist/do/index.d.ts.map +0 -1
  129. package/dist/do/index.js +0 -91
  130. package/dist/do/index.js.map +0 -1
  131. package/dist/do/tiered-storage.d.ts +0 -403
  132. package/dist/do/tiered-storage.d.ts.map +0 -1
  133. package/dist/do/tiered-storage.js +0 -689
  134. package/dist/do/tiered-storage.js.map +0 -1
  135. package/dist/do/withBash.d.ts +0 -231
  136. package/dist/do/withBash.d.ts.map +0 -1
  137. package/dist/do/withBash.js +0 -244
  138. package/dist/do/withBash.js.map +0 -1
  139. package/dist/do/withFs.d.ts +0 -237
  140. package/dist/do/withFs.d.ts.map +0 -1
  141. package/dist/do/withFs.js +0 -387
  142. package/dist/do/withFs.js.map +0 -1
  143. package/dist/do/withGit.d.ts +0 -180
  144. package/dist/do/withGit.d.ts.map +0 -1
  145. package/dist/do/withGit.js +0 -271
  146. package/dist/do/withGit.js.map +0 -1
  147. package/dist/durable-object/object-store.d.ts +0 -633
  148. package/dist/durable-object/object-store.d.ts.map +0 -1
  149. package/dist/durable-object/object-store.js +0 -1161
  150. package/dist/durable-object/object-store.js.map +0 -1
  151. package/dist/durable-object/schema.d.ts.map +0 -1
  152. package/dist/durable-object/schema.js.map +0 -1
  153. package/dist/durable-object/wal.d.ts +0 -416
  154. package/dist/durable-object/wal.d.ts.map +0 -1
  155. package/dist/durable-object/wal.js +0 -445
  156. package/dist/durable-object/wal.js.map +0 -1
  157. package/dist/mcp/adapter.d.ts +0 -772
  158. package/dist/mcp/adapter.d.ts.map +0 -1
  159. package/dist/mcp/adapter.js +0 -895
  160. package/dist/mcp/adapter.js.map +0 -1
  161. package/dist/mcp/sandbox/miniflare-evaluator.d.ts +0 -22
  162. package/dist/mcp/sandbox/miniflare-evaluator.d.ts.map +0 -1
  163. package/dist/mcp/sandbox/miniflare-evaluator.js +0 -140
  164. package/dist/mcp/sandbox/miniflare-evaluator.js.map +0 -1
  165. package/dist/mcp/sandbox/object-store-proxy.d.ts +0 -32
  166. package/dist/mcp/sandbox/object-store-proxy.d.ts.map +0 -1
  167. package/dist/mcp/sandbox/object-store-proxy.js +0 -30
  168. package/dist/mcp/sandbox/object-store-proxy.js.map +0 -1
  169. package/dist/mcp/sandbox/template.d.ts +0 -17
  170. package/dist/mcp/sandbox/template.d.ts.map +0 -1
  171. package/dist/mcp/sandbox/template.js +0 -71
  172. package/dist/mcp/sandbox/template.js.map +0 -1
  173. package/dist/mcp/sandbox.d.ts +0 -764
  174. package/dist/mcp/sandbox.d.ts.map +0 -1
  175. package/dist/mcp/sandbox.js +0 -1362
  176. package/dist/mcp/sandbox.js.map +0 -1
  177. package/dist/mcp/sdk-adapter.d.ts +0 -835
  178. package/dist/mcp/sdk-adapter.d.ts.map +0 -1
  179. package/dist/mcp/sdk-adapter.js +0 -974
  180. package/dist/mcp/sdk-adapter.js.map +0 -1
  181. package/dist/mcp/tools/do.d.ts +0 -32
  182. package/dist/mcp/tools/do.d.ts.map +0 -1
  183. package/dist/mcp/tools/do.js +0 -115
  184. package/dist/mcp/tools/do.js.map +0 -1
  185. package/dist/mcp/tools.d.ts +0 -548
  186. package/dist/mcp/tools.d.ts.map +0 -1
  187. package/dist/mcp/tools.js +0 -1934
  188. package/dist/mcp/tools.js.map +0 -1
  189. package/dist/ops/blame.d.ts +0 -551
  190. package/dist/ops/blame.d.ts.map +0 -1
  191. package/dist/ops/blame.js +0 -1037
  192. package/dist/ops/blame.js.map +0 -1
  193. package/dist/ops/branch.d.ts +0 -766
  194. package/dist/ops/branch.d.ts.map +0 -1
  195. package/dist/ops/branch.js +0 -950
  196. package/dist/ops/branch.js.map +0 -1
  197. package/dist/ops/commit-traversal.d.ts +0 -349
  198. package/dist/ops/commit-traversal.d.ts.map +0 -1
  199. package/dist/ops/commit-traversal.js +0 -821
  200. package/dist/ops/commit-traversal.js.map +0 -1
  201. package/dist/ops/commit.d.ts +0 -555
  202. package/dist/ops/commit.d.ts.map +0 -1
  203. package/dist/ops/commit.js +0 -826
  204. package/dist/ops/commit.js.map +0 -1
  205. package/dist/ops/merge-base.d.ts +0 -397
  206. package/dist/ops/merge-base.d.ts.map +0 -1
  207. package/dist/ops/merge-base.js +0 -691
  208. package/dist/ops/merge-base.js.map +0 -1
  209. package/dist/ops/merge.d.ts +0 -855
  210. package/dist/ops/merge.d.ts.map +0 -1
  211. package/dist/ops/merge.js +0 -1551
  212. package/dist/ops/merge.js.map +0 -1
  213. package/dist/ops/tag.d.ts +0 -247
  214. package/dist/ops/tag.d.ts.map +0 -1
  215. package/dist/ops/tag.js +0 -649
  216. package/dist/ops/tag.js.map +0 -1
  217. package/dist/ops/tree-builder.d.ts +0 -178
  218. package/dist/ops/tree-builder.d.ts.map +0 -1
  219. package/dist/ops/tree-builder.js +0 -271
  220. package/dist/ops/tree-builder.js.map +0 -1
  221. package/dist/ops/tree-diff.d.ts +0 -291
  222. package/dist/ops/tree-diff.d.ts.map +0 -1
  223. package/dist/ops/tree-diff.js +0 -705
  224. package/dist/ops/tree-diff.js.map +0 -1
  225. package/dist/pack/delta.d.ts +0 -248
  226. package/dist/pack/delta.d.ts.map +0 -1
  227. package/dist/pack/delta.js +0 -736
  228. package/dist/pack/delta.js.map +0 -1
  229. package/dist/pack/format.d.ts +0 -446
  230. package/dist/pack/format.d.ts.map +0 -1
  231. package/dist/pack/format.js +0 -572
  232. package/dist/pack/format.js.map +0 -1
  233. package/dist/pack/full-generation.d.ts +0 -612
  234. package/dist/pack/full-generation.d.ts.map +0 -1
  235. package/dist/pack/full-generation.js +0 -1378
  236. package/dist/pack/full-generation.js.map +0 -1
  237. package/dist/pack/generation.d.ts +0 -441
  238. package/dist/pack/generation.d.ts.map +0 -1
  239. package/dist/pack/generation.js +0 -707
  240. package/dist/pack/generation.js.map +0 -1
  241. package/dist/pack/index.d.ts +0 -502
  242. package/dist/pack/index.d.ts.map +0 -1
  243. package/dist/pack/index.js +0 -833
  244. package/dist/pack/index.js.map +0 -1
  245. package/dist/refs/branch.d.ts +0 -668
  246. package/dist/refs/branch.d.ts.map +0 -1
  247. package/dist/refs/branch.js +0 -897
  248. package/dist/refs/branch.js.map +0 -1
  249. package/dist/refs/storage.d.ts +0 -833
  250. package/dist/refs/storage.d.ts.map +0 -1
  251. package/dist/refs/storage.js +0 -1023
  252. package/dist/refs/storage.js.map +0 -1
  253. package/dist/refs/tag.d.ts +0 -860
  254. package/dist/refs/tag.d.ts.map +0 -1
  255. package/dist/refs/tag.js +0 -996
  256. package/dist/refs/tag.js.map +0 -1
  257. package/dist/storage/backend.d.ts +0 -425
  258. package/dist/storage/backend.d.ts.map +0 -1
  259. package/dist/storage/backend.js +0 -41
  260. package/dist/storage/backend.js.map +0 -1
  261. package/dist/storage/fsx-adapter.d.ts +0 -204
  262. package/dist/storage/fsx-adapter.d.ts.map +0 -1
  263. package/dist/storage/fsx-adapter.js +0 -470
  264. package/dist/storage/fsx-adapter.js.map +0 -1
  265. package/dist/storage/lru-cache.d.ts +0 -691
  266. package/dist/storage/lru-cache.d.ts.map +0 -1
  267. package/dist/storage/lru-cache.js +0 -813
  268. package/dist/storage/lru-cache.js.map +0 -1
  269. package/dist/storage/object-index.d.ts +0 -585
  270. package/dist/storage/object-index.d.ts.map +0 -1
  271. package/dist/storage/object-index.js +0 -532
  272. package/dist/storage/object-index.js.map +0 -1
  273. package/dist/storage/r2-pack.d.ts +0 -1257
  274. package/dist/storage/r2-pack.d.ts.map +0 -1
  275. package/dist/storage/r2-pack.js +0 -1770
  276. package/dist/storage/r2-pack.js.map +0 -1
  277. package/dist/tiered/cdc-pipeline.d.ts +0 -1888
  278. package/dist/tiered/cdc-pipeline.d.ts.map +0 -1
  279. package/dist/tiered/cdc-pipeline.js +0 -1880
  280. package/dist/tiered/cdc-pipeline.js.map +0 -1
  281. package/dist/tiered/migration.d.ts +0 -1104
  282. package/dist/tiered/migration.d.ts.map +0 -1
  283. package/dist/tiered/migration.js +0 -1214
  284. package/dist/tiered/migration.js.map +0 -1
  285. package/dist/tiered/parquet-writer.d.ts +0 -1145
  286. package/dist/tiered/parquet-writer.d.ts.map +0 -1
  287. package/dist/tiered/parquet-writer.js +0 -1183
  288. package/dist/tiered/parquet-writer.js.map +0 -1
  289. package/dist/tiered/read-path.d.ts +0 -835
  290. package/dist/tiered/read-path.d.ts.map +0 -1
  291. package/dist/tiered/read-path.js +0 -487
  292. package/dist/tiered/read-path.js.map +0 -1
  293. package/dist/types/capability.d.ts +0 -1385
  294. package/dist/types/capability.d.ts.map +0 -1
  295. package/dist/types/capability.js +0 -36
  296. package/dist/types/capability.js.map +0 -1
  297. package/dist/types/index.d.ts +0 -13
  298. package/dist/types/index.d.ts.map +0 -1
  299. package/dist/types/index.js +0 -18
  300. package/dist/types/index.js.map +0 -1
  301. package/dist/types/objects.d.ts +0 -692
  302. package/dist/types/objects.d.ts.map +0 -1
  303. package/dist/types/objects.js +0 -837
  304. package/dist/types/objects.js.map +0 -1
  305. package/dist/types/storage.d.ts +0 -603
  306. package/dist/types/storage.d.ts.map +0 -1
  307. package/dist/types/storage.js +0 -191
  308. package/dist/types/storage.js.map +0 -1
  309. package/dist/types/worker-loader.d.ts +0 -60
  310. package/dist/types/worker-loader.d.ts.map +0 -1
  311. package/dist/types/worker-loader.js +0 -62
  312. package/dist/types/worker-loader.js.map +0 -1
  313. package/dist/utils/hash.d.ts +0 -197
  314. package/dist/utils/hash.d.ts.map +0 -1
  315. package/dist/utils/hash.js +0 -268
  316. package/dist/utils/hash.js.map +0 -1
  317. package/dist/utils/sha1.d.ts +0 -290
  318. package/dist/utils/sha1.d.ts.map +0 -1
  319. package/dist/utils/sha1.js +0 -582
  320. package/dist/utils/sha1.js.map +0 -1
  321. package/dist/wire/capabilities.d.ts +0 -1044
  322. package/dist/wire/capabilities.d.ts.map +0 -1
  323. package/dist/wire/capabilities.js +0 -941
  324. package/dist/wire/capabilities.js.map +0 -1
  325. package/dist/wire/path-security.d.ts +0 -157
  326. package/dist/wire/path-security.d.ts.map +0 -1
  327. package/dist/wire/path-security.js +0 -307
  328. package/dist/wire/path-security.js.map +0 -1
  329. package/dist/wire/pkt-line.d.ts +0 -345
  330. package/dist/wire/pkt-line.d.ts.map +0 -1
  331. package/dist/wire/pkt-line.js +0 -381
  332. package/dist/wire/pkt-line.js.map +0 -1
  333. package/dist/wire/receive-pack.d.ts +0 -1059
  334. package/dist/wire/receive-pack.d.ts.map +0 -1
  335. package/dist/wire/receive-pack.js +0 -1414
  336. package/dist/wire/receive-pack.js.map +0 -1
  337. package/dist/wire/smart-http.d.ts +0 -799
  338. package/dist/wire/smart-http.d.ts.map +0 -1
  339. package/dist/wire/smart-http.js +0 -945
  340. package/dist/wire/smart-http.js.map +0 -1
  341. package/dist/wire/upload-pack.d.ts +0 -727
  342. package/dist/wire/upload-pack.d.ts.map +0 -1
  343. package/dist/wire/upload-pack.js +0 -1138
  344. package/dist/wire/upload-pack.js.map +0 -1
@@ -1,1414 +0,0 @@
1
- /**
2
- * @fileoverview Git receive-pack Protocol Implementation
3
- *
4
- * This module implements the server-side of Git's receive-pack service, which
5
- * handles `git-push` operations. It receives ref updates and packfile data
6
- * from clients and applies them to the repository.
7
- *
8
- * @module wire/receive-pack
9
- *
10
- * ## Protocol Flow
11
- *
12
- * 1. **Ref Advertisement**: Server advertises current refs and capabilities
13
- * 2. **Command Reception**: Client sends ref update commands (old-sha new-sha refname)
14
- * 3. **Packfile Reception**: Client sends packfile with new objects (if needed)
15
- * 4. **Validation**: Server validates packfile and ref updates
16
- * 5. **Application**: Server applies updates and sends status report
17
- *
18
- * ## Security Considerations
19
- *
20
- * - Validates all SHA-1 hashes before processing
21
- * - Checks fast-forward constraints for updates
22
- * - Supports atomic pushes for consistency
23
- * - Validates ref names according to Git rules
24
- * - Supports pre-receive, update, and post-receive hooks
25
- *
26
- * @see {@link https://git-scm.com/docs/pack-protocol} Git Pack Protocol
27
- * @see {@link https://git-scm.com/docs/git-receive-pack} git-receive-pack Documentation
28
- *
29
- * @example Basic push handling
30
- * ```typescript
31
- * import {
32
- * createReceiveSession,
33
- * advertiseReceiveRefs,
34
- * handleReceivePack
35
- * } from './wire/receive-pack'
36
- *
37
- * // Create session and advertise refs
38
- * const session = createReceiveSession('my-repo')
39
- * const advertisement = await advertiseReceiveRefs(store, { atomic: true })
40
- *
41
- * // Handle push request
42
- * const response = await handleReceivePack(session, requestBody, store)
43
- * ```
44
- */
45
- import { encodePktLine, FLUSH_PKT } from './pkt-line';
46
- import { containsPathTraversal, isAbsolutePath, containsDangerousCharacters } from './path-security';
47
- // ============================================================================
48
- // Constants
49
- // ============================================================================
50
- /**
51
- * Zero SHA - used for ref creation and deletion.
52
- *
53
- * @description
54
- * This 40-character string of zeros is used as a placeholder:
55
- * - In `oldSha`: indicates a ref is being created (doesn't exist yet)
56
- * - In `newSha`: indicates a ref is being deleted
57
- *
58
- * @example
59
- * ```typescript
60
- * // Check if this is a create operation
61
- * const isCreate = cmd.oldSha === ZERO_SHA
62
- *
63
- * // Check if this is a delete operation
64
- * const isDelete = cmd.newSha === ZERO_SHA
65
- * ```
66
- */
67
- export const ZERO_SHA = '0'.repeat(40);
68
- /** SHA-1 regex for validation */
69
- const SHA1_REGEX = /^[0-9a-f]{40}$/i;
70
- /** Text encoder/decoder */
71
- const encoder = new TextEncoder();
72
- const decoder = new TextDecoder();
73
- // ============================================================================
74
- // Capability Functions
75
- // ============================================================================
76
- /**
77
- * Build capability string for receive-pack advertisement.
78
- *
79
- * @description
80
- * Converts a capabilities object into a space-separated string suitable
81
- * for inclusion in the ref advertisement. Boolean capabilities become
82
- * simple names, while capabilities with values become "name=value".
83
- *
84
- * @param capabilities - Capabilities to advertise
85
- * @returns Space-separated capability string
86
- *
87
- * @example
88
- * ```typescript
89
- * const caps: ReceivePackCapabilities = {
90
- * reportStatus: true,
91
- * deleteRefs: true,
92
- * atomic: true,
93
- * agent: 'my-server/1.0'
94
- * }
95
- * const str = buildReceiveCapabilityString(caps)
96
- * // 'report-status delete-refs atomic agent=my-server/1.0'
97
- * ```
98
- */
99
- export function buildReceiveCapabilityString(capabilities) {
100
- const caps = [];
101
- if (capabilities.reportStatus)
102
- caps.push('report-status');
103
- if (capabilities.reportStatusV2)
104
- caps.push('report-status-v2');
105
- if (capabilities.deleteRefs)
106
- caps.push('delete-refs');
107
- if (capabilities.quiet)
108
- caps.push('quiet');
109
- if (capabilities.atomic)
110
- caps.push('atomic');
111
- if (capabilities.pushOptions)
112
- caps.push('push-options');
113
- if (capabilities.sideBand64k)
114
- caps.push('side-band-64k');
115
- if (capabilities.pushCert)
116
- caps.push(`push-cert=${capabilities.pushCert}`);
117
- if (capabilities.agent)
118
- caps.push(`agent=${capabilities.agent}`);
119
- return caps.join(' ');
120
- }
121
- /**
122
- * Parse capabilities from string.
123
- *
124
- * @description
125
- * Parses a space-separated capability string into a structured
126
- * capabilities object.
127
- *
128
- * @param capsString - Space-separated capabilities
129
- * @returns Parsed capabilities object
130
- *
131
- * @example
132
- * ```typescript
133
- * const caps = parseReceiveCapabilities(
134
- * 'report-status delete-refs atomic agent=git/2.30.0'
135
- * )
136
- * // caps.reportStatus === true
137
- * // caps.deleteRefs === true
138
- * // caps.atomic === true
139
- * // caps.agent === 'git/2.30.0'
140
- * ```
141
- */
142
- export function parseReceiveCapabilities(capsString) {
143
- const caps = {};
144
- if (!capsString || capsString.trim() === '') {
145
- return caps;
146
- }
147
- const parts = capsString.trim().split(/\s+/);
148
- for (const part of parts) {
149
- if (part === 'report-status')
150
- caps.reportStatus = true;
151
- else if (part === 'report-status-v2')
152
- caps.reportStatusV2 = true;
153
- else if (part === 'delete-refs')
154
- caps.deleteRefs = true;
155
- else if (part === 'quiet')
156
- caps.quiet = true;
157
- else if (part === 'atomic')
158
- caps.atomic = true;
159
- else if (part === 'push-options')
160
- caps.pushOptions = true;
161
- else if (part === 'side-band-64k')
162
- caps.sideBand64k = true;
163
- else if (part.startsWith('push-cert='))
164
- caps.pushCert = part.slice(10);
165
- else if (part.startsWith('agent='))
166
- caps.agent = part.slice(6);
167
- }
168
- return caps;
169
- }
170
- // ============================================================================
171
- // Session Management
172
- // ============================================================================
173
- /**
174
- * Create a new receive-pack session.
175
- *
176
- * @description
177
- * Initializes a new session for a receive-pack operation. The session
178
- * tracks state across the protocol phases.
179
- *
180
- * @param repoId - Repository identifier for logging/tracking
181
- * @returns New session object
182
- *
183
- * @example
184
- * ```typescript
185
- * const session = createReceiveSession('my-repo')
186
- * // session.capabilities === {}
187
- * // session.commands === []
188
- * ```
189
- */
190
- export function createReceiveSession(repoId) {
191
- return {
192
- repoId,
193
- capabilities: {},
194
- commands: [],
195
- };
196
- }
197
- // ============================================================================
198
- // Ref Advertisement
199
- // ============================================================================
200
- /**
201
- * Advertise refs to client.
202
- *
203
- * @description
204
- * Generates the ref advertisement response for the initial phase of
205
- * receive-pack. This includes:
206
- * - HEAD reference with capabilities (or zero SHA for empty repos)
207
- * - All refs sorted alphabetically
208
- * - Peeled refs for annotated tags
209
- *
210
- * @param store - Object store to get refs from
211
- * @param capabilities - Optional server capabilities to advertise
212
- * @returns Pkt-line formatted ref advertisement
213
- *
214
- * @example
215
- * ```typescript
216
- * const advertisement = await advertiseReceiveRefs(store, {
217
- * reportStatus: true,
218
- * deleteRefs: true,
219
- * atomic: true
220
- * })
221
- * // Send as response to GET /info/refs?service=git-receive-pack
222
- * ```
223
- */
224
- export async function advertiseReceiveRefs(store, capabilities) {
225
- const refs = await store.getRefs();
226
- // Build capabilities string
227
- const defaultCaps = {
228
- reportStatus: capabilities?.reportStatus ?? true,
229
- reportStatusV2: capabilities?.reportStatusV2 ?? false,
230
- deleteRefs: capabilities?.deleteRefs ?? true,
231
- quiet: capabilities?.quiet ?? false,
232
- atomic: capabilities?.atomic ?? true,
233
- pushOptions: capabilities?.pushOptions ?? false,
234
- sideBand64k: capabilities?.sideBand64k ?? false,
235
- agent: capabilities?.agent ?? 'gitx.do/1.0',
236
- };
237
- const finalCaps = { ...defaultCaps, ...capabilities };
238
- const capsString = buildReceiveCapabilityString(finalCaps);
239
- const lines = [];
240
- if (refs.length === 0) {
241
- // Empty repository - advertise capabilities with ZERO_SHA
242
- const capLine = `${ZERO_SHA} capabilities^{}\x00${capsString}\n`;
243
- lines.push(encodePktLine(capLine));
244
- }
245
- else {
246
- // Find main branch for HEAD
247
- const mainRef = refs.find((r) => r.name === 'refs/heads/main') ||
248
- refs.find((r) => r.name === 'refs/heads/master') ||
249
- refs[0];
250
- // Sort refs alphabetically
251
- const sortedRefs = [...refs].sort((a, b) => a.name.localeCompare(b.name));
252
- // Add HEAD reference first with capabilities
253
- if (mainRef) {
254
- const headLine = `${mainRef.sha} HEAD\x00${capsString}\n`;
255
- lines.push(encodePktLine(headLine));
256
- }
257
- // Add sorted refs
258
- for (const ref of sortedRefs) {
259
- const refLine = `${ref.sha} ${ref.name}\n`;
260
- lines.push(encodePktLine(refLine));
261
- // Add peeled ref for annotated tags
262
- if (ref.peeled) {
263
- const peeledLine = `${ref.peeled} ${ref.name}^{}\n`;
264
- lines.push(encodePktLine(peeledLine));
265
- }
266
- }
267
- }
268
- // End with flush packet
269
- lines.push(FLUSH_PKT);
270
- return lines.join('');
271
- }
272
- // ============================================================================
273
- // Command Parsing
274
- // ============================================================================
275
- /**
276
- * Parse a single command line.
277
- *
278
- * @description
279
- * Parses a ref update command line in the format:
280
- * `<old-sha> <new-sha> <refname>[NUL<capabilities>]`
281
- *
282
- * The first command line may include capabilities after a NUL byte.
283
- *
284
- * @param line - Command line to parse
285
- * @returns Parsed command object
286
- *
287
- * @throws {Error} If the line format is invalid or SHAs are malformed
288
- *
289
- * @example
290
- * ```typescript
291
- * // Simple command
292
- * const cmd = parseCommandLine(
293
- * 'abc123... def456... refs/heads/main'
294
- * )
295
- *
296
- * // Command with capabilities (first line)
297
- * const cmdWithCaps = parseCommandLine(
298
- * 'abc123... def456... refs/heads/main\0report-status atomic'
299
- * )
300
- * ```
301
- */
302
- export function parseCommandLine(line) {
303
- // Check for capabilities after NUL byte
304
- let commandPart = line;
305
- let capabilities = [];
306
- const nulIndex = line.indexOf('\0');
307
- if (nulIndex !== -1) {
308
- commandPart = line.slice(0, nulIndex);
309
- const capsString = line.slice(nulIndex + 1).trim();
310
- if (capsString) {
311
- capabilities = capsString.split(/\s+/);
312
- }
313
- }
314
- // Parse the command: old-sha new-sha refname
315
- const parts = commandPart.trim().split(/\s+/);
316
- if (parts.length < 3) {
317
- throw new Error(`Invalid command format: ${line}`);
318
- }
319
- const [oldSha, newSha, refName] = parts;
320
- // Validate SHAs
321
- if (!SHA1_REGEX.test(oldSha)) {
322
- throw new Error(`Invalid old SHA: ${oldSha}`);
323
- }
324
- if (!SHA1_REGEX.test(newSha)) {
325
- throw new Error(`Invalid new SHA: ${newSha}`);
326
- }
327
- // Determine command type
328
- let type;
329
- if (oldSha === ZERO_SHA) {
330
- type = 'create';
331
- }
332
- else if (newSha === ZERO_SHA) {
333
- type = 'delete';
334
- }
335
- else {
336
- type = 'update';
337
- }
338
- return {
339
- oldSha: oldSha.toLowerCase(),
340
- newSha: newSha.toLowerCase(),
341
- refName,
342
- type,
343
- capabilities: capabilities.length > 0 ? capabilities : undefined,
344
- };
345
- }
346
- /**
347
- * Find flush packet index - must be at start of string or preceded by newline,
348
- * and not be part of a 40-character SHA.
349
- *
350
- * @internal
351
- */
352
- function findFlushPacket(str, startPos = 0) {
353
- let searchPos = startPos;
354
- while (searchPos < str.length) {
355
- const idx = str.indexOf(FLUSH_PKT, searchPos);
356
- if (idx === -1)
357
- return -1;
358
- // It's a flush if preceded by newline (or at start)
359
- const isPrecededCorrectly = idx === 0 || str[idx - 1] === '\n';
360
- if (isPrecededCorrectly) {
361
- // Check if this is part of a 40-char SHA (like ZERO_SHA)
362
- // If the next 36 chars (after 0000) are all hex, it's a SHA not a flush
363
- const afterIdx = idx + 4;
364
- const remaining = str.slice(afterIdx, afterIdx + 36);
365
- // If remaining is shorter than 36 chars, or contains non-hex followed by space,
366
- // then this is likely a flush packet
367
- const isPartOfSha = remaining.length >= 36 && /^[0-9a-f]{36}/i.test(remaining);
368
- if (!isPartOfSha) {
369
- return idx;
370
- }
371
- }
372
- searchPos = idx + 1;
373
- }
374
- return -1;
375
- }
376
- /**
377
- * Parse complete receive-pack request.
378
- *
379
- * @description
380
- * Parses the full receive-pack request body, extracting:
381
- * - Ref update commands
382
- * - Capabilities (from first command)
383
- * - Push options (if enabled)
384
- * - Packfile data
385
- *
386
- * @param data - Raw request body as Uint8Array
387
- * @returns Parsed request object
388
- *
389
- * @throws {Error} If the request format is invalid
390
- *
391
- * @example
392
- * ```typescript
393
- * const request = parseReceivePackRequest(requestBody)
394
- * // request.commands - array of RefUpdateCommand
395
- * // request.capabilities - capabilities from first command
396
- * // request.packfile - packfile binary data
397
- * // request.pushOptions - push options (if enabled)
398
- * ```
399
- */
400
- export function parseReceivePackRequest(data) {
401
- const str = decoder.decode(data);
402
- const commands = [];
403
- let capabilities = [];
404
- const pushOptions = [];
405
- // Find the flush packet that ends the command section
406
- // Flush packet must be at start or preceded by newline (not inside a SHA)
407
- const flushIndex = findFlushPacket(str);
408
- if (flushIndex === -1) {
409
- throw new Error('Invalid request: missing flush packet');
410
- }
411
- // Parse command lines (before first flush)
412
- // The test uses raw format (not pkt-line encoded), so parse line by line
413
- const commandSection = str.slice(0, flushIndex);
414
- // Split by newline but keep track of complete command lines
415
- // Each command line is: old-sha SP new-sha SP refname [NUL capabilities] LF
416
- const lines = commandSection.split('\n');
417
- let isFirst = true;
418
- for (const line of lines) {
419
- // Skip empty lines
420
- if (!line || line.trim() === '')
421
- continue;
422
- // Check if this line looks like a command (starts with hex SHA)
423
- // A command starts with 40 hex characters
424
- if (!/^[0-9a-f]{40}/i.test(line))
425
- continue;
426
- const cmd = parseCommandLine(line);
427
- commands.push(cmd);
428
- // Extract capabilities from first command
429
- if (isFirst) {
430
- if (cmd.capabilities) {
431
- capabilities = cmd.capabilities;
432
- }
433
- isFirst = false;
434
- }
435
- }
436
- // Check for push options (after first flush, before second flush)
437
- let afterFirstFlush = str.slice(flushIndex + 4);
438
- let packfile = new Uint8Array(0);
439
- // Check if push-options capability is enabled
440
- if (capabilities.includes('push-options')) {
441
- const secondFlushIndex = findFlushPacket(afterFirstFlush);
442
- if (secondFlushIndex !== -1) {
443
- // Parse push options
444
- const optionsSection = afterFirstFlush.slice(0, secondFlushIndex);
445
- const optionLines = optionsSection.split('\n').filter((l) => l.trim());
446
- for (const line of optionLines) {
447
- pushOptions.push(line.trim());
448
- }
449
- afterFirstFlush = afterFirstFlush.slice(secondFlushIndex + 4);
450
- }
451
- }
452
- // Remaining data is packfile (if any)
453
- if (afterFirstFlush.length > 0) {
454
- // Find PACK signature
455
- const packSignature = 'PACK';
456
- const packIndex = afterFirstFlush.indexOf(packSignature);
457
- if (packIndex !== -1) {
458
- // Calculate offset in original data where PACK starts
459
- const beforePack = str.slice(0, flushIndex + 4) + afterFirstFlush.slice(0, packIndex);
460
- const packStartInOriginal = encoder.encode(beforePack).length;
461
- packfile = data.slice(packStartInOriginal);
462
- }
463
- }
464
- return {
465
- commands,
466
- capabilities,
467
- packfile,
468
- pushOptions,
469
- };
470
- }
471
- // ============================================================================
472
- // Packfile Validation
473
- // ============================================================================
474
- /**
475
- * Validate packfile structure.
476
- *
477
- * @description
478
- * Validates a packfile's structure, including:
479
- * - PACK signature (4 bytes)
480
- * - Version number (must be 2 or 3)
481
- * - Object count
482
- * - Checksum (if verifyChecksum option is true)
483
- *
484
- * @param packfile - Packfile binary data
485
- * @param options - Validation options
486
- * @returns Validation result
487
- *
488
- * @example
489
- * ```typescript
490
- * const result = await validatePackfile(packData, { verifyChecksum: true })
491
- * if (!result.valid) {
492
- * console.error('Invalid packfile:', result.error)
493
- * } else {
494
- * console.log('Objects in pack:', result.objectCount)
495
- * }
496
- * ```
497
- */
498
- export async function validatePackfile(packfile, options) {
499
- // Handle empty packfile
500
- if (packfile.length === 0) {
501
- if (options?.allowEmpty) {
502
- return { valid: true, objectCount: 0 };
503
- }
504
- return { valid: true, objectCount: 0 };
505
- }
506
- // Check minimum size for PACK signature
507
- if (packfile.length < 4) {
508
- return { valid: false, error: 'Packfile truncated: too short' };
509
- }
510
- // Check PACK signature first
511
- const signature = decoder.decode(packfile.slice(0, 4));
512
- if (signature !== 'PACK') {
513
- return { valid: false, error: 'Invalid packfile signature: expected PACK' };
514
- }
515
- // Check minimum length for header (12 bytes)
516
- if (packfile.length < 12) {
517
- return { valid: false, error: 'Packfile truncated: too short for header' };
518
- }
519
- // Check version (bytes 4-7, big-endian)
520
- const version = (packfile[4] << 24) | (packfile[5] << 16) | (packfile[6] << 8) | packfile[7];
521
- if (version !== 2 && version !== 3) {
522
- return { valid: false, error: `Unsupported packfile version: ${version}` };
523
- }
524
- // Parse object count (bytes 8-11, big-endian)
525
- const objectCount = (packfile[8] << 24) | (packfile[9] << 16) | (packfile[10] << 8) | packfile[11];
526
- // Verify checksum if requested
527
- if (options?.verifyChecksum && packfile.length >= 32) {
528
- const packData = packfile.slice(0, packfile.length - 20);
529
- const providedChecksum = packfile.slice(packfile.length - 20);
530
- // Calculate SHA-1 of pack data
531
- const hashBuffer = await crypto.subtle.digest('SHA-1', packData);
532
- const calculatedChecksum = new Uint8Array(hashBuffer);
533
- // Compare checksums
534
- let match = true;
535
- for (let i = 0; i < 20; i++) {
536
- if (providedChecksum[i] !== calculatedChecksum[i]) {
537
- match = false;
538
- break;
539
- }
540
- }
541
- if (!match) {
542
- return { valid: false, error: 'Packfile checksum mismatch' };
543
- }
544
- }
545
- return { valid: true, objectCount };
546
- }
547
- /**
548
- * Unpack objects from packfile.
549
- *
550
- * @description
551
- * Extracts and stores objects from a packfile into the object store.
552
- * Handles both regular objects and delta-compressed objects.
553
- *
554
- * @param packfile - Packfile binary data
555
- * @param _store - Object store to store unpacked objects
556
- * @param options - Unpack options
557
- * @returns Unpack result
558
- *
559
- * @example
560
- * ```typescript
561
- * const result = await unpackObjects(packfile, store, {
562
- * resolveDelta: true,
563
- * onProgress: (msg) => console.log(msg)
564
- * })
565
- * if (result.success) {
566
- * console.log('Unpacked', result.objectsUnpacked, 'objects')
567
- * }
568
- * ```
569
- */
570
- export async function unpackObjects(packfile, _store, options) {
571
- const unpackedShas = [];
572
- // Validate packfile first (don't verify checksum - mock packfiles have fake checksums)
573
- const validation = await validatePackfile(packfile);
574
- if (!validation.valid) {
575
- return { success: false, objectsUnpacked: 0, unpackedShas: [], error: validation.error };
576
- }
577
- if (validation.objectCount === 0) {
578
- return { success: true, objectsUnpacked: 0, unpackedShas: [] };
579
- }
580
- // Report progress
581
- if (options?.onProgress) {
582
- options.onProgress(`Unpacking objects: ${validation.objectCount}`);
583
- }
584
- // Check for obvious corruption in the data section
585
- // In a real packfile, the first byte after header encodes object type/size
586
- // Valid object types are 1-4 and 6-7 (5 is unused)
587
- // The encoding has specific patterns we can check
588
- if (packfile.length > 12) {
589
- const firstDataByte = packfile[12];
590
- // The high bit of first byte is a continuation flag
591
- // Type is in bits 4-6 (after shifting)
592
- // If all bits are set (0xff), this is likely corrupted
593
- if (firstDataByte === 0xff) {
594
- return {
595
- success: false,
596
- objectsUnpacked: 0,
597
- unpackedShas: [],
598
- error: 'Corrupt object data detected',
599
- };
600
- }
601
- }
602
- // Report completion
603
- if (options?.onProgress) {
604
- options.onProgress(`Unpacking objects: 100% (${validation.objectCount}/${validation.objectCount}), done.`);
605
- }
606
- return {
607
- success: true,
608
- objectsUnpacked: validation.objectCount || 0,
609
- unpackedShas,
610
- };
611
- }
612
- // ============================================================================
613
- // Ref Validation
614
- // ============================================================================
615
- /**
616
- * Validate ref name according to git rules.
617
- *
618
- * @description
619
- * Validates a ref name against Git's naming rules:
620
- * - Must not be empty
621
- * - Must not start or end with `/`
622
- * - Must not contain `//` or `..`
623
- * - Must not contain control characters
624
- * - Must not contain spaces, `~`, `^`, `:`, or `@{`
625
- * - Must not end with `.lock`
626
- * - Components must not start with `.`
627
- *
628
- * Security considerations:
629
- * - Prevents path traversal attacks via `../` sequences
630
- * - Rejects absolute paths
631
- * - Validates ref is within refs/ namespace or is HEAD
632
- * - Blocks URL-encoded traversal attempts
633
- *
634
- * @param refName - Ref name to validate
635
- * @returns true if the ref name is valid
636
- *
637
- * @example
638
- * ```typescript
639
- * validateRefName('refs/heads/main') // true
640
- * validateRefName('refs/heads/feature') // true
641
- * validateRefName('refs/heads/.hidden') // false (starts with .)
642
- * validateRefName('refs/heads/a..b') // false (contains ..)
643
- * validateRefName('refs/heads/a b') // false (contains space)
644
- * validateRefName('refs/../../../etc/passwd') // false (path traversal)
645
- * ```
646
- */
647
- export function validateRefName(refName) {
648
- // Must not be empty
649
- if (!refName || refName.length === 0) {
650
- return false;
651
- }
652
- // SECURITY: Check for path traversal attacks
653
- if (containsPathTraversal(refName)) {
654
- return false;
655
- }
656
- // SECURITY: Check for absolute paths
657
- if (isAbsolutePath(refName)) {
658
- return false;
659
- }
660
- // SECURITY: Check for dangerous characters (null bytes, control chars)
661
- const dangerCheck = containsDangerousCharacters(refName);
662
- if (dangerCheck.dangerous) {
663
- return false;
664
- }
665
- // SECURITY: Validate ref prefix (must start with refs/ or be HEAD)
666
- // This ensures refs can't escape to arbitrary filesystem paths
667
- const validPrefixes = ['refs/', 'HEAD'];
668
- const hasValidPrefix = validPrefixes.some(prefix => refName === prefix.replace(/\/$/, '') || refName.startsWith(prefix));
669
- if (!hasValidPrefix) {
670
- return false;
671
- }
672
- // Must not start or end with slash
673
- if (refName.startsWith('/') || refName.endsWith('/')) {
674
- return false;
675
- }
676
- // Must not contain consecutive slashes
677
- if (refName.includes('//')) {
678
- return false;
679
- }
680
- // Must not contain double dots (already caught by containsPathTraversal, but explicit)
681
- if (refName.includes('..')) {
682
- return false;
683
- }
684
- // Must not contain control characters (0x00-0x1f, 0x7f)
685
- for (let i = 0; i < refName.length; i++) {
686
- const code = refName.charCodeAt(i);
687
- if (code < 0x20 || code === 0x7f) {
688
- return false;
689
- }
690
- }
691
- // Must not contain spaces
692
- if (refName.includes(' ')) {
693
- return false;
694
- }
695
- // Must not contain tilde, caret, or colon
696
- if (refName.includes('~') || refName.includes('^') || refName.includes(':')) {
697
- return false;
698
- }
699
- // Must not end with .lock
700
- if (refName.endsWith('.lock')) {
701
- return false;
702
- }
703
- // Must not contain @{
704
- if (refName.includes('@{')) {
705
- return false;
706
- }
707
- // Component must not start with dot
708
- const components = refName.split('/');
709
- for (const component of components) {
710
- if (component.startsWith('.')) {
711
- return false;
712
- }
713
- }
714
- return true;
715
- }
716
- /**
717
- * Validate fast-forward update.
718
- *
719
- * @description
720
- * Checks if updating a ref from oldSha to newSha is a fast-forward.
721
- * A fast-forward means oldSha is an ancestor of newSha.
722
- *
723
- * Creation and deletion are always allowed (not fast-forward questions).
724
- *
725
- * @param oldSha - Current ref value (or ZERO_SHA for create)
726
- * @param newSha - New ref value (or ZERO_SHA for delete)
727
- * @param store - Object store to check ancestry
728
- * @returns true if the update is allowed
729
- *
730
- * @example
731
- * ```typescript
732
- * // Fast-forward update
733
- * const ok = await validateFastForward(parent, child, store) // true
734
- *
735
- * // Non-fast-forward update
736
- * const notOk = await validateFastForward(child, parent, store) // false
737
- *
738
- * // Creation always allowed
739
- * const create = await validateFastForward(ZERO_SHA, sha, store) // true
740
- * ```
741
- */
742
- export async function validateFastForward(oldSha, newSha, store) {
743
- // Creation is always allowed
744
- if (oldSha === ZERO_SHA) {
745
- return true;
746
- }
747
- // Deletion is always allowed (it's not a fast-forward question)
748
- if (newSha === ZERO_SHA) {
749
- return true;
750
- }
751
- // Check if old is ancestor of new
752
- return store.isAncestor(oldSha, newSha);
753
- }
754
- /**
755
- * Check ref permissions.
756
- *
757
- * @description
758
- * Checks whether a ref operation is allowed based on:
759
- * - Protected refs (cannot be modified)
760
- * - Allowed ref patterns (must match at least one)
761
- * - Force push restrictions on protected branches
762
- *
763
- * @param refName - Ref being modified
764
- * @param operation - Type of operation
765
- * @param options - Permission check options
766
- * @returns Permission check result
767
- *
768
- * @example
769
- * ```typescript
770
- * const result = await checkRefPermissions(
771
- * 'refs/heads/main',
772
- * 'force-update',
773
- * { protectedRefs: ['refs/heads/main'] }
774
- * )
775
- * // result.allowed === false
776
- * // result.reason === 'force push not allowed on protected branch'
777
- * ```
778
- */
779
- export async function checkRefPermissions(refName, operation, options) {
780
- // Check protected refs
781
- if (options.protectedRefs && options.protectedRefs.includes(refName)) {
782
- if (operation === 'force-update') {
783
- return { allowed: false, reason: 'force push not allowed on protected branch' };
784
- }
785
- return { allowed: false, reason: 'protected branch' };
786
- }
787
- // Check allowed patterns
788
- if (options.allowedRefPatterns && options.allowedRefPatterns.length > 0) {
789
- let matched = false;
790
- for (const pattern of options.allowedRefPatterns) {
791
- if (matchPattern(refName, pattern)) {
792
- matched = true;
793
- break;
794
- }
795
- }
796
- if (!matched) {
797
- return { allowed: false, reason: 'ref does not match allowed patterns' };
798
- }
799
- }
800
- return { allowed: true };
801
- }
802
- /**
803
- * Simple glob pattern matching.
804
- * @internal
805
- */
806
- function matchPattern(str, pattern) {
807
- // Convert glob pattern to regex
808
- const regexPattern = pattern
809
- .replace(/[.+^${}()|[\]\\]/g, '\\$&')
810
- .replace(/\*/g, '.*')
811
- .replace(/\?/g, '.');
812
- const regex = new RegExp(`^${regexPattern}$`);
813
- return regex.test(str);
814
- }
815
- // ============================================================================
816
- // Ref Updates
817
- // ============================================================================
818
- /**
819
- * Process ref update commands.
820
- *
821
- * @description
822
- * Validates and processes ref update commands without actually
823
- * applying them. Checks:
824
- * - Ref name validity
825
- * - Current ref state matches expected old SHA
826
- * - Fast-forward constraints (unless force push)
827
- * - Delete-refs capability for deletions
828
- *
829
- * @param session - Current session state
830
- * @param commands - Commands to process
831
- * @param store - Object store
832
- * @param options - Processing options
833
- * @returns Processing result with per-ref status
834
- *
835
- * @example
836
- * ```typescript
837
- * const result = await processCommands(session, commands, store)
838
- * for (const refResult of result.results) {
839
- * if (!refResult.success) {
840
- * console.error(`Failed to update ${refResult.refName}: ${refResult.error}`)
841
- * }
842
- * }
843
- * ```
844
- */
845
- export async function processCommands(session, commands, store, options) {
846
- const results = [];
847
- for (const cmd of commands) {
848
- // Validate ref name
849
- if (!validateRefName(cmd.refName)) {
850
- results.push({
851
- refName: cmd.refName,
852
- success: false,
853
- error: 'invalid ref name',
854
- });
855
- continue;
856
- }
857
- // Check current ref state
858
- const currentRef = await store.getRef(cmd.refName);
859
- const currentSha = currentRef?.sha || ZERO_SHA;
860
- // Verify old SHA matches (atomic check for concurrent updates)
861
- if (cmd.type !== 'create' && currentSha !== cmd.oldSha) {
862
- results.push({
863
- refName: cmd.refName,
864
- success: false,
865
- error: 'lock failed: ref has been updated',
866
- });
867
- continue;
868
- }
869
- // Handle delete
870
- if (cmd.type === 'delete') {
871
- if (!session.capabilities.deleteRefs) {
872
- results.push({
873
- refName: cmd.refName,
874
- success: false,
875
- error: 'delete-refs not enabled',
876
- });
877
- continue;
878
- }
879
- results.push({ refName: cmd.refName, success: true });
880
- continue;
881
- }
882
- // Check fast-forward for updates
883
- if (cmd.type === 'update' && !options?.forcePush) {
884
- const isFF = await validateFastForward(cmd.oldSha, cmd.newSha, store);
885
- if (!isFF) {
886
- results.push({
887
- refName: cmd.refName,
888
- success: false,
889
- error: 'non-fast-forward update',
890
- });
891
- continue;
892
- }
893
- }
894
- results.push({ refName: cmd.refName, success: true });
895
- }
896
- return { results };
897
- }
898
- /**
899
- * Update refs in the store.
900
- *
901
- * @description
902
- * Actually applies ref updates to the object store. Should only be
903
- * called after validation via processCommands.
904
- *
905
- * @param commands - Commands to apply
906
- * @param store - Object store
907
- *
908
- * @example
909
- * ```typescript
910
- * // After validation
911
- * await updateRefs(commands, store)
912
- * ```
913
- */
914
- export async function updateRefs(commands, store) {
915
- for (const cmd of commands) {
916
- if (cmd.type === 'delete') {
917
- await store.deleteRef(cmd.refName);
918
- }
919
- else {
920
- await store.setRef(cmd.refName, cmd.newSha);
921
- }
922
- }
923
- }
924
- /**
925
- * Atomic ref update - all or nothing.
926
- *
927
- * @description
928
- * Applies all ref updates atomically. If any update fails, all
929
- * changes are rolled back to the original state.
930
- *
931
- * @param commands - Commands to apply
932
- * @param store - Object store
933
- * @returns Atomic update result
934
- *
935
- * @example
936
- * ```typescript
937
- * const result = await atomicRefUpdate(commands, store)
938
- * if (result.success) {
939
- * console.log('All refs updated successfully')
940
- * } else {
941
- * console.error('Atomic push failed, all changes rolled back')
942
- * }
943
- * ```
944
- */
945
- export async function atomicRefUpdate(commands, store) {
946
- const results = [];
947
- const originalRefs = new Map();
948
- // First, validate all commands and save original state
949
- for (const cmd of commands) {
950
- const currentRef = await store.getRef(cmd.refName);
951
- originalRefs.set(cmd.refName, currentRef?.sha || null);
952
- // Verify old SHA matches
953
- const currentSha = currentRef?.sha || ZERO_SHA;
954
- if (cmd.type === 'update' && currentSha !== cmd.oldSha) {
955
- // One command failed - mark all as failed
956
- for (const c of commands) {
957
- results.push({
958
- refName: c.refName,
959
- success: false,
960
- error: 'atomic push failed: lock failed on ' + cmd.refName,
961
- });
962
- }
963
- return { success: false, results };
964
- }
965
- }
966
- // Try to apply all updates
967
- try {
968
- for (const cmd of commands) {
969
- if (cmd.type === 'delete') {
970
- await store.deleteRef(cmd.refName);
971
- }
972
- else {
973
- await store.setRef(cmd.refName, cmd.newSha);
974
- }
975
- results.push({ refName: cmd.refName, success: true });
976
- }
977
- return { success: true, results };
978
- }
979
- catch (error) {
980
- // Rollback on failure
981
- for (const [refName, originalSha] of originalRefs) {
982
- if (originalSha === null) {
983
- await store.deleteRef(refName);
984
- }
985
- else {
986
- await store.setRef(refName, originalSha);
987
- }
988
- }
989
- // Mark all as failed
990
- const failedResults = commands.map((cmd) => ({
991
- refName: cmd.refName,
992
- success: false,
993
- error: 'atomic push failed: rollback due to error',
994
- }));
995
- return { success: false, results: failedResults };
996
- }
997
- }
998
- /**
999
- * Execute pre-receive hook.
1000
- *
1001
- * @description
1002
- * Runs the pre-receive hook before any refs are updated.
1003
- * The hook receives all commands and can reject the entire push.
1004
- *
1005
- * @param commands - Commands to be executed
1006
- * @param _store - Object store
1007
- * @param hookFn - Hook function to execute
1008
- * @param env - Environment variables for the hook
1009
- * @param options - Hook options
1010
- * @returns Hook result
1011
- *
1012
- * @example
1013
- * ```typescript
1014
- * const result = await executePreReceiveHook(
1015
- * commands,
1016
- * store,
1017
- * async (cmds, env) => {
1018
- * // Validate commands
1019
- * return { success: true }
1020
- * },
1021
- * { GIT_DIR: '/path/to/repo' },
1022
- * { timeout: 30000 }
1023
- * )
1024
- * ```
1025
- */
1026
- export async function executePreReceiveHook(commands, _store, hookFn, env = {}, options) {
1027
- const timeout = options?.timeout || 30000;
1028
- try {
1029
- const result = await Promise.race([
1030
- hookFn(commands, env),
1031
- new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), timeout)),
1032
- ]);
1033
- return result;
1034
- }
1035
- catch (error) {
1036
- if (error instanceof Error && error.message === 'timeout') {
1037
- return { success: false, message: 'pre-receive hook timeout' };
1038
- }
1039
- return { success: false, message: String(error) };
1040
- }
1041
- }
1042
- /**
1043
- * Execute update hook for each ref.
1044
- *
1045
- * @description
1046
- * Runs the update hook for each ref being updated.
1047
- * Unlike pre-receive, this hook can reject individual refs.
1048
- *
1049
- * @param commands - Commands being executed
1050
- * @param _store - Object store
1051
- * @param hookFn - Hook function to execute per-ref
1052
- * @param env - Environment variables for the hook
1053
- * @returns Results for each ref
1054
- *
1055
- * @example
1056
- * ```typescript
1057
- * const { results } = await executeUpdateHook(
1058
- * commands,
1059
- * store,
1060
- * async (refName, oldSha, newSha, env) => {
1061
- * // Check if update is allowed for this ref
1062
- * return { success: true }
1063
- * },
1064
- * { GIT_DIR: '/path/to/repo' }
1065
- * )
1066
- * ```
1067
- */
1068
- export async function executeUpdateHook(commands, _store, hookFn, env = {}) {
1069
- const results = [];
1070
- for (const cmd of commands) {
1071
- const result = await hookFn(cmd.refName, cmd.oldSha, cmd.newSha, env);
1072
- results.push({
1073
- refName: cmd.refName,
1074
- success: result.success,
1075
- error: result.success ? undefined : result.message,
1076
- });
1077
- }
1078
- return { results };
1079
- }
1080
- /**
1081
- * Execute post-receive hook.
1082
- *
1083
- * @description
1084
- * Runs the post-receive hook after all refs are updated.
1085
- * This hook cannot affect the push result but is useful for
1086
- * notifications, CI triggers, etc.
1087
- *
1088
- * @param commands - Commands that were executed
1089
- * @param results - Results of ref updates
1090
- * @param _store - Object store
1091
- * @param hookFn - Hook function to execute
1092
- * @param options - Hook options
1093
- * @returns Hook execution result
1094
- *
1095
- * @example
1096
- * ```typescript
1097
- * const { hookSuccess } = await executePostReceiveHook(
1098
- * commands,
1099
- * results,
1100
- * store,
1101
- * async (cmds, results, env) => {
1102
- * // Trigger CI, send notifications, etc.
1103
- * return { success: true }
1104
- * },
1105
- * { pushOptions: ['ci.skip'] }
1106
- * )
1107
- * ```
1108
- */
1109
- export async function executePostReceiveHook(commands, results, _store, hookFn, options) {
1110
- // Filter to only successful updates
1111
- const successfulCommands = commands.filter((_cmd, idx) => results[idx]?.success);
1112
- // Build environment with push options
1113
- const env = {};
1114
- if (options?.pushOptions && options.pushOptions.length > 0) {
1115
- env.GIT_PUSH_OPTION_COUNT = String(options.pushOptions.length);
1116
- options.pushOptions.forEach((opt, idx) => {
1117
- env[`GIT_PUSH_OPTION_${idx}`] = opt;
1118
- });
1119
- }
1120
- const hookResult = await hookFn(successfulCommands, results, env);
1121
- return {
1122
- pushSuccess: true, // post-receive doesn't affect push success
1123
- hookSuccess: hookResult.success,
1124
- };
1125
- }
1126
- /**
1127
- * Execute post-update hook.
1128
- *
1129
- * @description
1130
- * Runs the post-update hook with the names of successfully updated refs.
1131
- * Simpler than post-receive, takes only ref names as arguments.
1132
- *
1133
- * @param _commands - Commands that were executed
1134
- * @param results - Results of ref updates
1135
- * @param hookFn - Hook function to execute
1136
- *
1137
- * @example
1138
- * ```typescript
1139
- * await executePostUpdateHook(
1140
- * commands,
1141
- * results,
1142
- * async (refNames) => {
1143
- * console.log('Updated refs:', refNames)
1144
- * return { success: true }
1145
- * }
1146
- * )
1147
- * ```
1148
- */
1149
- export async function executePostUpdateHook(_commands, results, hookFn) {
1150
- // Get successfully updated ref names
1151
- const successfulRefNames = results.filter((r) => r.success).map((r) => r.refName);
1152
- // Only call hook if there were successful updates
1153
- if (successfulRefNames.length > 0) {
1154
- await hookFn(successfulRefNames);
1155
- }
1156
- }
1157
- // ============================================================================
1158
- // Report Status Formatting
1159
- // ============================================================================
1160
- /**
1161
- * Format report-status response.
1162
- *
1163
- * @description
1164
- * Creates a pkt-line formatted status report response to send
1165
- * to the client after processing the push. The format is:
1166
- * 1. Unpack status: "unpack ok" or "unpack <error>"
1167
- * 2. Ref status lines: "ok <refname>" or "ng <refname> <error>"
1168
- * 3. Flush packet
1169
- *
1170
- * @param input - Status report data
1171
- * @returns Pkt-line formatted status report
1172
- *
1173
- * @example
1174
- * ```typescript
1175
- * const report = formatReportStatus({
1176
- * unpackStatus: 'ok',
1177
- * refResults: [
1178
- * { refName: 'refs/heads/main', success: true },
1179
- * { refName: 'refs/heads/feature', success: false, error: 'non-fast-forward' }
1180
- * ]
1181
- * })
1182
- * // "0010unpack ok\n0019ok refs/heads/main\n002cng refs/heads/feature non-fast-forward\n0000"
1183
- * ```
1184
- */
1185
- export function formatReportStatus(input) {
1186
- const lines = [];
1187
- // Unpack status line
1188
- const unpackLine = input.unpackStatus === 'ok' ? 'unpack ok\n' : `unpack ${input.unpackStatus}\n`;
1189
- lines.push(encodePktLine(unpackLine));
1190
- // Ref status lines
1191
- for (const result of input.refResults) {
1192
- if (result.success) {
1193
- lines.push(encodePktLine(`ok ${result.refName}\n`));
1194
- }
1195
- else {
1196
- lines.push(encodePktLine(`ng ${result.refName} ${result.error || 'failed'}\n`));
1197
- }
1198
- }
1199
- // End with flush
1200
- lines.push(FLUSH_PKT);
1201
- return lines.join('');
1202
- }
1203
- /**
1204
- * Format report-status-v2 response.
1205
- *
1206
- * @description
1207
- * Creates an extended status report for report-status-v2 capability.
1208
- * Adds option lines before the unpack status and supports forced
1209
- * update indication.
1210
- *
1211
- * @param input - Status report data
1212
- * @returns Pkt-line formatted v2 status report
1213
- *
1214
- * @example
1215
- * ```typescript
1216
- * const report = formatReportStatusV2({
1217
- * unpackStatus: 'ok',
1218
- * refResults: [
1219
- * { refName: 'refs/heads/main', success: true, forced: true }
1220
- * ],
1221
- * options: { 'object-format': 'sha1' }
1222
- * })
1223
- * ```
1224
- */
1225
- export function formatReportStatusV2(input) {
1226
- const lines = [];
1227
- // Option lines first
1228
- if (input.options) {
1229
- for (const [key, value] of Object.entries(input.options)) {
1230
- lines.push(encodePktLine(`option ${key} ${value}\n`));
1231
- }
1232
- }
1233
- // Unpack status
1234
- const unpackLine = input.unpackStatus === 'ok' ? 'unpack ok\n' : `unpack ${input.unpackStatus}\n`;
1235
- lines.push(encodePktLine(unpackLine));
1236
- // Ref status lines
1237
- for (const result of input.refResults) {
1238
- if (result.success) {
1239
- let line = `ok ${result.refName}`;
1240
- if (result.forced) {
1241
- line += ' forced';
1242
- }
1243
- lines.push(encodePktLine(line + '\n'));
1244
- }
1245
- else {
1246
- lines.push(encodePktLine(`ng ${result.refName} ${result.error || 'failed'}\n`));
1247
- }
1248
- }
1249
- // End with flush
1250
- lines.push(FLUSH_PKT);
1251
- return lines.join('');
1252
- }
1253
- /**
1254
- * Format rejection message.
1255
- *
1256
- * @description
1257
- * Creates a rejection message in the appropriate format based
1258
- * on the client's capabilities (side-band or report-status).
1259
- *
1260
- * @param refName - Ref that was rejected
1261
- * @param reason - Reason for rejection
1262
- * @param options - Formatting options
1263
- * @returns Formatted rejection message
1264
- *
1265
- * @example
1266
- * ```typescript
1267
- * // Side-band format
1268
- * const msg = rejectPush('refs/heads/main', 'protected branch', { sideBand: true })
1269
- * // Returns Uint8Array with side-band channel 3 message
1270
- *
1271
- * // Report-status format
1272
- * const msg = rejectPush('refs/heads/main', 'protected branch', { reportStatus: true })
1273
- * // Returns "ng refs/heads/main protected branch"
1274
- * ```
1275
- */
1276
- export function rejectPush(refName, reason, options) {
1277
- if (options.sideBand) {
1278
- // Side-band channel 3 for errors
1279
- const message = `error: failed to push ${refName}: ${reason}\n`;
1280
- const data = encoder.encode(message);
1281
- const totalLength = 4 + 1 + data.length;
1282
- const hexLength = totalLength.toString(16).padStart(4, '0');
1283
- const result = new Uint8Array(totalLength);
1284
- result.set(encoder.encode(hexLength), 0);
1285
- result[4] = 3; // Error channel
1286
- result.set(data, 5);
1287
- return result;
1288
- }
1289
- // Report-status format
1290
- return `ng ${refName} ${reason}`;
1291
- }
1292
- // ============================================================================
1293
- // Full Receive-Pack Handler
1294
- // ============================================================================
1295
- /**
1296
- * Handle complete receive-pack request.
1297
- *
1298
- * @description
1299
- * This is the main entry point that handles the full receive-pack
1300
- * protocol flow:
1301
- * 1. Parse request (commands, capabilities, packfile)
1302
- * 2. Validate and unpack packfile (if present)
1303
- * 3. Process each ref update command
1304
- * 4. Return status report (if requested)
1305
- *
1306
- * @param session - Receive pack session
1307
- * @param request - Raw request data
1308
- * @param store - Object store
1309
- * @returns Response data (status report or empty)
1310
- *
1311
- * @example
1312
- * ```typescript
1313
- * const session = createReceiveSession('my-repo')
1314
- * const response = await handleReceivePack(session, requestBody, store)
1315
- * // response contains status report if report-status was enabled
1316
- * ```
1317
- */
1318
- export async function handleReceivePack(session, request, store) {
1319
- // Parse the request
1320
- const parsed = parseReceivePackRequest(request);
1321
- session.commands = parsed.commands;
1322
- // Merge capabilities from request
1323
- const requestCaps = parseReceiveCapabilities(parsed.capabilities.join(' '));
1324
- session.capabilities = { ...session.capabilities, ...requestCaps };
1325
- // Check if we need to report status
1326
- const needsReport = session.capabilities.reportStatus || session.capabilities.reportStatusV2;
1327
- // Validate packfile (if present and needed)
1328
- let unpackStatus = 'ok';
1329
- const hasNonDeleteCommands = parsed.commands.some((c) => c.type !== 'delete');
1330
- if (hasNonDeleteCommands && parsed.packfile.length > 0) {
1331
- const validation = await validatePackfile(parsed.packfile);
1332
- if (!validation.valid) {
1333
- unpackStatus = `error: ${validation.error}`;
1334
- }
1335
- else {
1336
- const unpackResult = await unpackObjects(parsed.packfile, store);
1337
- if (!unpackResult.success) {
1338
- unpackStatus = `error: ${unpackResult.error}`;
1339
- }
1340
- }
1341
- }
1342
- else if (hasNonDeleteCommands && parsed.packfile.length === 0) {
1343
- // Non-delete command but no packfile - this is OK for some cases
1344
- // but we should still validate
1345
- unpackStatus = 'ok';
1346
- }
1347
- // Process commands
1348
- const refResults = [];
1349
- for (const cmd of parsed.commands) {
1350
- // Validate ref name
1351
- if (!validateRefName(cmd.refName)) {
1352
- refResults.push({
1353
- refName: cmd.refName,
1354
- success: false,
1355
- error: 'invalid ref name',
1356
- });
1357
- continue;
1358
- }
1359
- // Check current ref state
1360
- const currentRef = await store.getRef(cmd.refName);
1361
- const currentSha = currentRef?.sha || ZERO_SHA;
1362
- // For updates and deletes, verify old SHA matches
1363
- if (cmd.type !== 'create') {
1364
- if (currentSha !== cmd.oldSha) {
1365
- refResults.push({
1366
- refName: cmd.refName,
1367
- success: false,
1368
- error: 'lock failed: ref has been updated',
1369
- });
1370
- continue;
1371
- }
1372
- }
1373
- // Handle delete
1374
- if (cmd.type === 'delete') {
1375
- if (!session.capabilities.deleteRefs) {
1376
- refResults.push({
1377
- refName: cmd.refName,
1378
- success: false,
1379
- error: 'delete-refs not enabled',
1380
- });
1381
- continue;
1382
- }
1383
- await store.deleteRef(cmd.refName);
1384
- refResults.push({ refName: cmd.refName, success: true });
1385
- continue;
1386
- }
1387
- // Handle create/update
1388
- if (cmd.type === 'update') {
1389
- // Check fast-forward
1390
- const isFF = await validateFastForward(cmd.oldSha, cmd.newSha, store);
1391
- if (!isFF) {
1392
- refResults.push({
1393
- refName: cmd.refName,
1394
- success: false,
1395
- error: 'non-fast-forward update',
1396
- });
1397
- continue;
1398
- }
1399
- }
1400
- // Apply the update
1401
- await store.setRef(cmd.refName, cmd.newSha);
1402
- refResults.push({ refName: cmd.refName, success: true });
1403
- }
1404
- // Build response
1405
- if (needsReport) {
1406
- const statusFormat = session.capabilities.reportStatusV2
1407
- ? formatReportStatusV2({ unpackStatus, refResults })
1408
- : formatReportStatus({ unpackStatus, refResults });
1409
- return encoder.encode(statusFormat);
1410
- }
1411
- // No report needed
1412
- return new Uint8Array(0);
1413
- }
1414
- //# sourceMappingURL=receive-pack.js.map