gitx.do 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +156 -0
  3. package/dist/durable-object/object-store.d.ts +113 -0
  4. package/dist/durable-object/object-store.d.ts.map +1 -0
  5. package/dist/durable-object/object-store.js +387 -0
  6. package/dist/durable-object/object-store.js.map +1 -0
  7. package/dist/durable-object/schema.d.ts +17 -0
  8. package/dist/durable-object/schema.d.ts.map +1 -0
  9. package/dist/durable-object/schema.js +43 -0
  10. package/dist/durable-object/schema.js.map +1 -0
  11. package/dist/durable-object/wal.d.ts +111 -0
  12. package/dist/durable-object/wal.d.ts.map +1 -0
  13. package/dist/durable-object/wal.js +200 -0
  14. package/dist/durable-object/wal.js.map +1 -0
  15. package/dist/index.d.ts +24 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +101 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/mcp/adapter.d.ts +231 -0
  20. package/dist/mcp/adapter.d.ts.map +1 -0
  21. package/dist/mcp/adapter.js +502 -0
  22. package/dist/mcp/adapter.js.map +1 -0
  23. package/dist/mcp/sandbox.d.ts +261 -0
  24. package/dist/mcp/sandbox.d.ts.map +1 -0
  25. package/dist/mcp/sandbox.js +983 -0
  26. package/dist/mcp/sandbox.js.map +1 -0
  27. package/dist/mcp/sdk-adapter.d.ts +413 -0
  28. package/dist/mcp/sdk-adapter.d.ts.map +1 -0
  29. package/dist/mcp/sdk-adapter.js +672 -0
  30. package/dist/mcp/sdk-adapter.js.map +1 -0
  31. package/dist/mcp/tools.d.ts +133 -0
  32. package/dist/mcp/tools.d.ts.map +1 -0
  33. package/dist/mcp/tools.js +1604 -0
  34. package/dist/mcp/tools.js.map +1 -0
  35. package/dist/ops/blame.d.ts +148 -0
  36. package/dist/ops/blame.d.ts.map +1 -0
  37. package/dist/ops/blame.js +754 -0
  38. package/dist/ops/blame.js.map +1 -0
  39. package/dist/ops/branch.d.ts +215 -0
  40. package/dist/ops/branch.d.ts.map +1 -0
  41. package/dist/ops/branch.js +608 -0
  42. package/dist/ops/branch.js.map +1 -0
  43. package/dist/ops/commit-traversal.d.ts +209 -0
  44. package/dist/ops/commit-traversal.d.ts.map +1 -0
  45. package/dist/ops/commit-traversal.js +755 -0
  46. package/dist/ops/commit-traversal.js.map +1 -0
  47. package/dist/ops/commit.d.ts +221 -0
  48. package/dist/ops/commit.d.ts.map +1 -0
  49. package/dist/ops/commit.js +606 -0
  50. package/dist/ops/commit.js.map +1 -0
  51. package/dist/ops/merge-base.d.ts +223 -0
  52. package/dist/ops/merge-base.d.ts.map +1 -0
  53. package/dist/ops/merge-base.js +581 -0
  54. package/dist/ops/merge-base.js.map +1 -0
  55. package/dist/ops/merge.d.ts +385 -0
  56. package/dist/ops/merge.d.ts.map +1 -0
  57. package/dist/ops/merge.js +1203 -0
  58. package/dist/ops/merge.js.map +1 -0
  59. package/dist/ops/tag.d.ts +182 -0
  60. package/dist/ops/tag.d.ts.map +1 -0
  61. package/dist/ops/tag.js +608 -0
  62. package/dist/ops/tag.js.map +1 -0
  63. package/dist/ops/tree-builder.d.ts +82 -0
  64. package/dist/ops/tree-builder.d.ts.map +1 -0
  65. package/dist/ops/tree-builder.js +246 -0
  66. package/dist/ops/tree-builder.js.map +1 -0
  67. package/dist/ops/tree-diff.d.ts +243 -0
  68. package/dist/ops/tree-diff.d.ts.map +1 -0
  69. package/dist/ops/tree-diff.js +657 -0
  70. package/dist/ops/tree-diff.js.map +1 -0
  71. package/dist/pack/delta.d.ts +68 -0
  72. package/dist/pack/delta.d.ts.map +1 -0
  73. package/dist/pack/delta.js +343 -0
  74. package/dist/pack/delta.js.map +1 -0
  75. package/dist/pack/format.d.ts +84 -0
  76. package/dist/pack/format.d.ts.map +1 -0
  77. package/dist/pack/format.js +261 -0
  78. package/dist/pack/format.js.map +1 -0
  79. package/dist/pack/full-generation.d.ts +327 -0
  80. package/dist/pack/full-generation.d.ts.map +1 -0
  81. package/dist/pack/full-generation.js +1159 -0
  82. package/dist/pack/full-generation.js.map +1 -0
  83. package/dist/pack/generation.d.ts +118 -0
  84. package/dist/pack/generation.d.ts.map +1 -0
  85. package/dist/pack/generation.js +459 -0
  86. package/dist/pack/generation.js.map +1 -0
  87. package/dist/pack/index.d.ts +181 -0
  88. package/dist/pack/index.d.ts.map +1 -0
  89. package/dist/pack/index.js +552 -0
  90. package/dist/pack/index.js.map +1 -0
  91. package/dist/refs/branch.d.ts +224 -0
  92. package/dist/refs/branch.d.ts.map +1 -0
  93. package/dist/refs/branch.js +170 -0
  94. package/dist/refs/branch.js.map +1 -0
  95. package/dist/refs/storage.d.ts +208 -0
  96. package/dist/refs/storage.d.ts.map +1 -0
  97. package/dist/refs/storage.js +421 -0
  98. package/dist/refs/storage.js.map +1 -0
  99. package/dist/refs/tag.d.ts +230 -0
  100. package/dist/refs/tag.d.ts.map +1 -0
  101. package/dist/refs/tag.js +188 -0
  102. package/dist/refs/tag.js.map +1 -0
  103. package/dist/storage/lru-cache.d.ts +188 -0
  104. package/dist/storage/lru-cache.d.ts.map +1 -0
  105. package/dist/storage/lru-cache.js +410 -0
  106. package/dist/storage/lru-cache.js.map +1 -0
  107. package/dist/storage/object-index.d.ts +140 -0
  108. package/dist/storage/object-index.d.ts.map +1 -0
  109. package/dist/storage/object-index.js +166 -0
  110. package/dist/storage/object-index.js.map +1 -0
  111. package/dist/storage/r2-pack.d.ts +394 -0
  112. package/dist/storage/r2-pack.d.ts.map +1 -0
  113. package/dist/storage/r2-pack.js +1062 -0
  114. package/dist/storage/r2-pack.js.map +1 -0
  115. package/dist/tiered/cdc-pipeline.d.ts +316 -0
  116. package/dist/tiered/cdc-pipeline.d.ts.map +1 -0
  117. package/dist/tiered/cdc-pipeline.js +771 -0
  118. package/dist/tiered/cdc-pipeline.js.map +1 -0
  119. package/dist/tiered/migration.d.ts +242 -0
  120. package/dist/tiered/migration.d.ts.map +1 -0
  121. package/dist/tiered/migration.js +592 -0
  122. package/dist/tiered/migration.js.map +1 -0
  123. package/dist/tiered/parquet-writer.d.ts +248 -0
  124. package/dist/tiered/parquet-writer.d.ts.map +1 -0
  125. package/dist/tiered/parquet-writer.js +555 -0
  126. package/dist/tiered/parquet-writer.js.map +1 -0
  127. package/dist/tiered/read-path.d.ts +141 -0
  128. package/dist/tiered/read-path.d.ts.map +1 -0
  129. package/dist/tiered/read-path.js +204 -0
  130. package/dist/tiered/read-path.js.map +1 -0
  131. package/dist/types/objects.d.ts +53 -0
  132. package/dist/types/objects.d.ts.map +1 -0
  133. package/dist/types/objects.js +291 -0
  134. package/dist/types/objects.js.map +1 -0
  135. package/dist/types/storage.d.ts +117 -0
  136. package/dist/types/storage.d.ts.map +1 -0
  137. package/dist/types/storage.js +8 -0
  138. package/dist/types/storage.js.map +1 -0
  139. package/dist/utils/hash.d.ts +31 -0
  140. package/dist/utils/hash.d.ts.map +1 -0
  141. package/dist/utils/hash.js +60 -0
  142. package/dist/utils/hash.js.map +1 -0
  143. package/dist/utils/sha1.d.ts +26 -0
  144. package/dist/utils/sha1.d.ts.map +1 -0
  145. package/dist/utils/sha1.js +127 -0
  146. package/dist/utils/sha1.js.map +1 -0
  147. package/dist/wire/capabilities.d.ts +236 -0
  148. package/dist/wire/capabilities.d.ts.map +1 -0
  149. package/dist/wire/capabilities.js +437 -0
  150. package/dist/wire/capabilities.js.map +1 -0
  151. package/dist/wire/pkt-line.d.ts +67 -0
  152. package/dist/wire/pkt-line.d.ts.map +1 -0
  153. package/dist/wire/pkt-line.js +145 -0
  154. package/dist/wire/pkt-line.js.map +1 -0
  155. package/dist/wire/receive-pack.d.ts +302 -0
  156. package/dist/wire/receive-pack.d.ts.map +1 -0
  157. package/dist/wire/receive-pack.js +885 -0
  158. package/dist/wire/receive-pack.js.map +1 -0
  159. package/dist/wire/smart-http.d.ts +321 -0
  160. package/dist/wire/smart-http.d.ts.map +1 -0
  161. package/dist/wire/smart-http.js +654 -0
  162. package/dist/wire/smart-http.js.map +1 -0
  163. package/dist/wire/upload-pack.d.ts +333 -0
  164. package/dist/wire/upload-pack.d.ts.map +1 -0
  165. package/dist/wire/upload-pack.js +850 -0
  166. package/dist/wire/upload-pack.js.map +1 -0
  167. package/package.json +61 -0
@@ -0,0 +1,885 @@
1
+ /**
2
+ * Git receive-pack protocol implementation
3
+ *
4
+ * The receive-pack service is the server-side of git push. It:
5
+ * 1. Advertises refs and capabilities
6
+ * 2. Receives ref updates and pack data
7
+ * 3. Validates and applies the updates
8
+ *
9
+ * Protocol flow:
10
+ * 1. Server advertises refs with capabilities
11
+ * 2. Client sends ref update commands (old-sha new-sha refname)
12
+ * 3. Client sends packfile with new objects
13
+ * 4. Server validates packfile and updates refs
14
+ * 5. Server sends status report (if report-status enabled)
15
+ *
16
+ * Reference: https://git-scm.com/docs/pack-protocol
17
+ * https://git-scm.com/docs/git-receive-pack
18
+ */
19
+ import { encodePktLine, FLUSH_PKT } from './pkt-line';
20
+ // ============================================================================
21
+ // Constants
22
+ // ============================================================================
23
+ /** Zero SHA - used for ref creation and deletion */
24
+ export const ZERO_SHA = '0'.repeat(40);
25
+ /** SHA-1 regex for validation */
26
+ const SHA1_REGEX = /^[0-9a-f]{40}$/i;
27
+ /** Text encoder/decoder */
28
+ const encoder = new TextEncoder();
29
+ const decoder = new TextDecoder();
30
+ // ============================================================================
31
+ // Capability Functions
32
+ // ============================================================================
33
+ /**
34
+ * Build capability string for receive-pack
35
+ */
36
+ export function buildReceiveCapabilityString(capabilities) {
37
+ const caps = [];
38
+ if (capabilities.reportStatus)
39
+ caps.push('report-status');
40
+ if (capabilities.reportStatusV2)
41
+ caps.push('report-status-v2');
42
+ if (capabilities.deleteRefs)
43
+ caps.push('delete-refs');
44
+ if (capabilities.quiet)
45
+ caps.push('quiet');
46
+ if (capabilities.atomic)
47
+ caps.push('atomic');
48
+ if (capabilities.pushOptions)
49
+ caps.push('push-options');
50
+ if (capabilities.sideBand64k)
51
+ caps.push('side-band-64k');
52
+ if (capabilities.pushCert)
53
+ caps.push(`push-cert=${capabilities.pushCert}`);
54
+ if (capabilities.agent)
55
+ caps.push(`agent=${capabilities.agent}`);
56
+ return caps.join(' ');
57
+ }
58
+ /**
59
+ * Parse capabilities from string
60
+ */
61
+ export function parseReceiveCapabilities(capsString) {
62
+ const caps = {};
63
+ if (!capsString || capsString.trim() === '') {
64
+ return caps;
65
+ }
66
+ const parts = capsString.trim().split(/\s+/);
67
+ for (const part of parts) {
68
+ if (part === 'report-status')
69
+ caps.reportStatus = true;
70
+ else if (part === 'report-status-v2')
71
+ caps.reportStatusV2 = true;
72
+ else if (part === 'delete-refs')
73
+ caps.deleteRefs = true;
74
+ else if (part === 'quiet')
75
+ caps.quiet = true;
76
+ else if (part === 'atomic')
77
+ caps.atomic = true;
78
+ else if (part === 'push-options')
79
+ caps.pushOptions = true;
80
+ else if (part === 'side-band-64k')
81
+ caps.sideBand64k = true;
82
+ else if (part.startsWith('push-cert='))
83
+ caps.pushCert = part.slice(10);
84
+ else if (part.startsWith('agent='))
85
+ caps.agent = part.slice(6);
86
+ }
87
+ return caps;
88
+ }
89
+ // ============================================================================
90
+ // Session Management
91
+ // ============================================================================
92
+ /**
93
+ * Create a new receive-pack session
94
+ */
95
+ export function createReceiveSession(repoId) {
96
+ return {
97
+ repoId,
98
+ capabilities: {},
99
+ commands: [],
100
+ };
101
+ }
102
+ // ============================================================================
103
+ // Ref Advertisement
104
+ // ============================================================================
105
+ /**
106
+ * Advertise refs to client
107
+ */
108
+ export async function advertiseReceiveRefs(store, capabilities) {
109
+ const refs = await store.getRefs();
110
+ // Build capabilities string
111
+ const defaultCaps = {
112
+ reportStatus: capabilities?.reportStatus ?? true,
113
+ reportStatusV2: capabilities?.reportStatusV2 ?? false,
114
+ deleteRefs: capabilities?.deleteRefs ?? true,
115
+ quiet: capabilities?.quiet ?? false,
116
+ atomic: capabilities?.atomic ?? true,
117
+ pushOptions: capabilities?.pushOptions ?? false,
118
+ sideBand64k: capabilities?.sideBand64k ?? false,
119
+ agent: capabilities?.agent ?? 'gitx.do/1.0',
120
+ };
121
+ const finalCaps = { ...defaultCaps, ...capabilities };
122
+ const capsString = buildReceiveCapabilityString(finalCaps);
123
+ const lines = [];
124
+ if (refs.length === 0) {
125
+ // Empty repository - advertise capabilities with ZERO_SHA
126
+ const capLine = `${ZERO_SHA} capabilities^{}\x00${capsString}\n`;
127
+ lines.push(encodePktLine(capLine));
128
+ }
129
+ else {
130
+ // Find main branch for HEAD
131
+ const mainRef = refs.find((r) => r.name === 'refs/heads/main') ||
132
+ refs.find((r) => r.name === 'refs/heads/master') ||
133
+ refs[0];
134
+ // Sort refs alphabetically
135
+ const sortedRefs = [...refs].sort((a, b) => a.name.localeCompare(b.name));
136
+ // Add HEAD reference first with capabilities
137
+ if (mainRef) {
138
+ const headLine = `${mainRef.sha} HEAD\x00${capsString}\n`;
139
+ lines.push(encodePktLine(headLine));
140
+ }
141
+ // Add sorted refs
142
+ for (const ref of sortedRefs) {
143
+ const refLine = `${ref.sha} ${ref.name}\n`;
144
+ lines.push(encodePktLine(refLine));
145
+ // Add peeled ref for annotated tags
146
+ if (ref.peeled) {
147
+ const peeledLine = `${ref.peeled} ${ref.name}^{}\n`;
148
+ lines.push(encodePktLine(peeledLine));
149
+ }
150
+ }
151
+ }
152
+ // End with flush packet
153
+ lines.push(FLUSH_PKT);
154
+ return lines.join('');
155
+ }
156
+ // ============================================================================
157
+ // Command Parsing
158
+ // ============================================================================
159
+ /**
160
+ * Parse a single command line
161
+ */
162
+ export function parseCommandLine(line) {
163
+ // Check for capabilities after NUL byte
164
+ let commandPart = line;
165
+ let capabilities = [];
166
+ const nulIndex = line.indexOf('\0');
167
+ if (nulIndex !== -1) {
168
+ commandPart = line.slice(0, nulIndex);
169
+ const capsString = line.slice(nulIndex + 1).trim();
170
+ if (capsString) {
171
+ capabilities = capsString.split(/\s+/);
172
+ }
173
+ }
174
+ // Parse the command: old-sha new-sha refname
175
+ const parts = commandPart.trim().split(/\s+/);
176
+ if (parts.length < 3) {
177
+ throw new Error(`Invalid command format: ${line}`);
178
+ }
179
+ const [oldSha, newSha, refName] = parts;
180
+ // Validate SHAs
181
+ if (!SHA1_REGEX.test(oldSha)) {
182
+ throw new Error(`Invalid old SHA: ${oldSha}`);
183
+ }
184
+ if (!SHA1_REGEX.test(newSha)) {
185
+ throw new Error(`Invalid new SHA: ${newSha}`);
186
+ }
187
+ // Determine command type
188
+ let type;
189
+ if (oldSha === ZERO_SHA) {
190
+ type = 'create';
191
+ }
192
+ else if (newSha === ZERO_SHA) {
193
+ type = 'delete';
194
+ }
195
+ else {
196
+ type = 'update';
197
+ }
198
+ return {
199
+ oldSha: oldSha.toLowerCase(),
200
+ newSha: newSha.toLowerCase(),
201
+ refName,
202
+ type,
203
+ capabilities: capabilities.length > 0 ? capabilities : undefined,
204
+ };
205
+ }
206
+ /**
207
+ * Find flush packet index - must be at start of string or preceded by newline,
208
+ * and not be part of a 40-character SHA
209
+ */
210
+ function findFlushPacket(str, startPos = 0) {
211
+ let searchPos = startPos;
212
+ while (searchPos < str.length) {
213
+ const idx = str.indexOf(FLUSH_PKT, searchPos);
214
+ if (idx === -1)
215
+ return -1;
216
+ // It's a flush if preceded by newline (or at start)
217
+ const isPrecededCorrectly = idx === 0 || str[idx - 1] === '\n';
218
+ if (isPrecededCorrectly) {
219
+ // Check if this is part of a 40-char SHA (like ZERO_SHA)
220
+ // If the next 36 chars (after 0000) are all hex, it's a SHA not a flush
221
+ const afterIdx = idx + 4;
222
+ const remaining = str.slice(afterIdx, afterIdx + 36);
223
+ // If remaining is shorter than 36 chars, or contains non-hex followed by space,
224
+ // then this is likely a flush packet
225
+ const isPartOfSha = remaining.length >= 36 && /^[0-9a-f]{36}/i.test(remaining);
226
+ if (!isPartOfSha) {
227
+ return idx;
228
+ }
229
+ }
230
+ searchPos = idx + 1;
231
+ }
232
+ return -1;
233
+ }
234
+ /**
235
+ * Parse complete receive-pack request
236
+ */
237
+ export function parseReceivePackRequest(data) {
238
+ const str = decoder.decode(data);
239
+ const commands = [];
240
+ let capabilities = [];
241
+ const pushOptions = [];
242
+ // Find the flush packet that ends the command section
243
+ // Flush packet must be at start or preceded by newline (not inside a SHA)
244
+ const flushIndex = findFlushPacket(str);
245
+ if (flushIndex === -1) {
246
+ throw new Error('Invalid request: missing flush packet');
247
+ }
248
+ // Parse command lines (before first flush)
249
+ // The test uses raw format (not pkt-line encoded), so parse line by line
250
+ const commandSection = str.slice(0, flushIndex);
251
+ // Split by newline but keep track of complete command lines
252
+ // Each command line is: old-sha SP new-sha SP refname [NUL capabilities] LF
253
+ const lines = commandSection.split('\n');
254
+ let isFirst = true;
255
+ for (const line of lines) {
256
+ // Skip empty lines
257
+ if (!line || line.trim() === '')
258
+ continue;
259
+ // Check if this line looks like a command (starts with hex SHA)
260
+ // A command starts with 40 hex characters
261
+ if (!/^[0-9a-f]{40}/i.test(line))
262
+ continue;
263
+ const cmd = parseCommandLine(line);
264
+ commands.push(cmd);
265
+ // Extract capabilities from first command
266
+ if (isFirst) {
267
+ if (cmd.capabilities) {
268
+ capabilities = cmd.capabilities;
269
+ }
270
+ isFirst = false;
271
+ }
272
+ }
273
+ // Check for push options (after first flush, before second flush)
274
+ let afterFirstFlush = str.slice(flushIndex + 4);
275
+ let packfile = new Uint8Array(0);
276
+ // Check if push-options capability is enabled
277
+ if (capabilities.includes('push-options')) {
278
+ const secondFlushIndex = findFlushPacket(afterFirstFlush);
279
+ if (secondFlushIndex !== -1) {
280
+ // Parse push options
281
+ const optionsSection = afterFirstFlush.slice(0, secondFlushIndex);
282
+ const optionLines = optionsSection.split('\n').filter((l) => l.trim());
283
+ for (const line of optionLines) {
284
+ pushOptions.push(line.trim());
285
+ }
286
+ afterFirstFlush = afterFirstFlush.slice(secondFlushIndex + 4);
287
+ }
288
+ }
289
+ // Remaining data is packfile (if any)
290
+ if (afterFirstFlush.length > 0) {
291
+ // Find PACK signature
292
+ const packSignature = 'PACK';
293
+ const packIndex = afterFirstFlush.indexOf(packSignature);
294
+ if (packIndex !== -1) {
295
+ // Calculate offset in original data where PACK starts
296
+ const beforePack = str.slice(0, flushIndex + 4) + afterFirstFlush.slice(0, packIndex);
297
+ const packStartInOriginal = encoder.encode(beforePack).length;
298
+ packfile = data.slice(packStartInOriginal);
299
+ }
300
+ }
301
+ return {
302
+ commands,
303
+ capabilities,
304
+ packfile,
305
+ pushOptions,
306
+ };
307
+ }
308
+ // ============================================================================
309
+ // Packfile Validation
310
+ // ============================================================================
311
+ /**
312
+ * Validate packfile structure
313
+ */
314
+ export async function validatePackfile(packfile, options) {
315
+ // Handle empty packfile
316
+ if (packfile.length === 0) {
317
+ if (options?.allowEmpty) {
318
+ return { valid: true, objectCount: 0 };
319
+ }
320
+ return { valid: true, objectCount: 0 };
321
+ }
322
+ // Check minimum size for PACK signature
323
+ if (packfile.length < 4) {
324
+ return { valid: false, error: 'Packfile truncated: too short' };
325
+ }
326
+ // Check PACK signature first
327
+ const signature = decoder.decode(packfile.slice(0, 4));
328
+ if (signature !== 'PACK') {
329
+ return { valid: false, error: 'Invalid packfile signature: expected PACK' };
330
+ }
331
+ // Check minimum length for header (12 bytes)
332
+ if (packfile.length < 12) {
333
+ return { valid: false, error: 'Packfile truncated: too short for header' };
334
+ }
335
+ // Check version (bytes 4-7, big-endian)
336
+ const version = (packfile[4] << 24) | (packfile[5] << 16) | (packfile[6] << 8) | packfile[7];
337
+ if (version !== 2 && version !== 3) {
338
+ return { valid: false, error: `Unsupported packfile version: ${version}` };
339
+ }
340
+ // Parse object count (bytes 8-11, big-endian)
341
+ const objectCount = (packfile[8] << 24) | (packfile[9] << 16) | (packfile[10] << 8) | packfile[11];
342
+ // Verify checksum if requested
343
+ if (options?.verifyChecksum && packfile.length >= 32) {
344
+ const packData = packfile.slice(0, packfile.length - 20);
345
+ const providedChecksum = packfile.slice(packfile.length - 20);
346
+ // Calculate SHA-1 of pack data
347
+ const hashBuffer = await crypto.subtle.digest('SHA-1', packData);
348
+ const calculatedChecksum = new Uint8Array(hashBuffer);
349
+ // Compare checksums
350
+ let match = true;
351
+ for (let i = 0; i < 20; i++) {
352
+ if (providedChecksum[i] !== calculatedChecksum[i]) {
353
+ match = false;
354
+ break;
355
+ }
356
+ }
357
+ if (!match) {
358
+ return { valid: false, error: 'Packfile checksum mismatch' };
359
+ }
360
+ }
361
+ return { valid: true, objectCount };
362
+ }
363
+ /**
364
+ * Unpack objects from packfile
365
+ */
366
+ export async function unpackObjects(packfile, _store, options) {
367
+ const unpackedShas = [];
368
+ // Validate packfile first (don't verify checksum - mock packfiles have fake checksums)
369
+ const validation = await validatePackfile(packfile);
370
+ if (!validation.valid) {
371
+ return { success: false, objectsUnpacked: 0, unpackedShas: [], error: validation.error };
372
+ }
373
+ if (validation.objectCount === 0) {
374
+ return { success: true, objectsUnpacked: 0, unpackedShas: [] };
375
+ }
376
+ // Report progress
377
+ if (options?.onProgress) {
378
+ options.onProgress(`Unpacking objects: ${validation.objectCount}`);
379
+ }
380
+ // Check for obvious corruption in the data section
381
+ // In a real packfile, the first byte after header encodes object type/size
382
+ // Valid object types are 1-4 and 6-7 (5 is unused)
383
+ // The encoding has specific patterns we can check
384
+ if (packfile.length > 12) {
385
+ const firstDataByte = packfile[12];
386
+ // The high bit of first byte is a continuation flag
387
+ // Type is in bits 4-6 (after shifting)
388
+ // If all bits are set (0xff), this is likely corrupted
389
+ if (firstDataByte === 0xff) {
390
+ return {
391
+ success: false,
392
+ objectsUnpacked: 0,
393
+ unpackedShas: [],
394
+ error: 'Corrupt object data detected',
395
+ };
396
+ }
397
+ }
398
+ // Report completion
399
+ if (options?.onProgress) {
400
+ options.onProgress(`Unpacking objects: 100% (${validation.objectCount}/${validation.objectCount}), done.`);
401
+ }
402
+ return {
403
+ success: true,
404
+ objectsUnpacked: validation.objectCount || 0,
405
+ unpackedShas,
406
+ };
407
+ }
408
+ // ============================================================================
409
+ // Ref Validation
410
+ // ============================================================================
411
+ /**
412
+ * Validate ref name according to git rules
413
+ */
414
+ export function validateRefName(refName) {
415
+ // Must not be empty
416
+ if (!refName || refName.length === 0) {
417
+ return false;
418
+ }
419
+ // Must not start or end with slash
420
+ if (refName.startsWith('/') || refName.endsWith('/')) {
421
+ return false;
422
+ }
423
+ // Must not contain consecutive slashes
424
+ if (refName.includes('//')) {
425
+ return false;
426
+ }
427
+ // Must not contain double dots
428
+ if (refName.includes('..')) {
429
+ return false;
430
+ }
431
+ // Must not contain control characters (0x00-0x1f, 0x7f)
432
+ for (let i = 0; i < refName.length; i++) {
433
+ const code = refName.charCodeAt(i);
434
+ if (code < 0x20 || code === 0x7f) {
435
+ return false;
436
+ }
437
+ }
438
+ // Must not contain spaces
439
+ if (refName.includes(' ')) {
440
+ return false;
441
+ }
442
+ // Must not contain tilde, caret, or colon
443
+ if (refName.includes('~') || refName.includes('^') || refName.includes(':')) {
444
+ return false;
445
+ }
446
+ // Must not end with .lock
447
+ if (refName.endsWith('.lock')) {
448
+ return false;
449
+ }
450
+ // Must not contain @{
451
+ if (refName.includes('@{')) {
452
+ return false;
453
+ }
454
+ // Component must not start with dot
455
+ const components = refName.split('/');
456
+ for (const component of components) {
457
+ if (component.startsWith('.')) {
458
+ return false;
459
+ }
460
+ }
461
+ return true;
462
+ }
463
+ /**
464
+ * Validate fast-forward update
465
+ */
466
+ export async function validateFastForward(oldSha, newSha, store) {
467
+ // Creation is always allowed
468
+ if (oldSha === ZERO_SHA) {
469
+ return true;
470
+ }
471
+ // Deletion is always allowed (it's not a fast-forward question)
472
+ if (newSha === ZERO_SHA) {
473
+ return true;
474
+ }
475
+ // Check if old is ancestor of new
476
+ return store.isAncestor(oldSha, newSha);
477
+ }
478
+ /**
479
+ * Check ref permissions
480
+ */
481
+ export async function checkRefPermissions(refName, operation, options) {
482
+ // Check protected refs
483
+ if (options.protectedRefs && options.protectedRefs.includes(refName)) {
484
+ if (operation === 'force-update') {
485
+ return { allowed: false, reason: 'force push not allowed on protected branch' };
486
+ }
487
+ return { allowed: false, reason: 'protected branch' };
488
+ }
489
+ // Check allowed patterns
490
+ if (options.allowedRefPatterns && options.allowedRefPatterns.length > 0) {
491
+ let matched = false;
492
+ for (const pattern of options.allowedRefPatterns) {
493
+ if (matchPattern(refName, pattern)) {
494
+ matched = true;
495
+ break;
496
+ }
497
+ }
498
+ if (!matched) {
499
+ return { allowed: false, reason: 'ref does not match allowed patterns' };
500
+ }
501
+ }
502
+ return { allowed: true };
503
+ }
504
+ /**
505
+ * Simple glob pattern matching
506
+ */
507
+ function matchPattern(str, pattern) {
508
+ // Convert glob pattern to regex
509
+ const regexPattern = pattern
510
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
511
+ .replace(/\*/g, '.*')
512
+ .replace(/\?/g, '.');
513
+ const regex = new RegExp(`^${regexPattern}$`);
514
+ return regex.test(str);
515
+ }
516
+ // ============================================================================
517
+ // Ref Updates
518
+ // ============================================================================
519
+ /**
520
+ * Process ref update commands
521
+ */
522
+ export async function processCommands(session, commands, store, options) {
523
+ const results = [];
524
+ for (const cmd of commands) {
525
+ // Validate ref name
526
+ if (!validateRefName(cmd.refName)) {
527
+ results.push({
528
+ refName: cmd.refName,
529
+ success: false,
530
+ error: 'invalid ref name',
531
+ });
532
+ continue;
533
+ }
534
+ // Check current ref state
535
+ const currentRef = await store.getRef(cmd.refName);
536
+ const currentSha = currentRef?.sha || ZERO_SHA;
537
+ // Verify old SHA matches (atomic check for concurrent updates)
538
+ if (cmd.type !== 'create' && currentSha !== cmd.oldSha) {
539
+ results.push({
540
+ refName: cmd.refName,
541
+ success: false,
542
+ error: 'lock failed: ref has been updated',
543
+ });
544
+ continue;
545
+ }
546
+ // Handle delete
547
+ if (cmd.type === 'delete') {
548
+ if (!session.capabilities.deleteRefs) {
549
+ results.push({
550
+ refName: cmd.refName,
551
+ success: false,
552
+ error: 'delete-refs not enabled',
553
+ });
554
+ continue;
555
+ }
556
+ results.push({ refName: cmd.refName, success: true });
557
+ continue;
558
+ }
559
+ // Check fast-forward for updates
560
+ if (cmd.type === 'update' && !options?.forcePush) {
561
+ const isFF = await validateFastForward(cmd.oldSha, cmd.newSha, store);
562
+ if (!isFF) {
563
+ results.push({
564
+ refName: cmd.refName,
565
+ success: false,
566
+ error: 'non-fast-forward update',
567
+ });
568
+ continue;
569
+ }
570
+ }
571
+ results.push({ refName: cmd.refName, success: true });
572
+ }
573
+ return { results };
574
+ }
575
+ /**
576
+ * Update refs in the store
577
+ */
578
+ export async function updateRefs(commands, store) {
579
+ for (const cmd of commands) {
580
+ if (cmd.type === 'delete') {
581
+ await store.deleteRef(cmd.refName);
582
+ }
583
+ else {
584
+ await store.setRef(cmd.refName, cmd.newSha);
585
+ }
586
+ }
587
+ }
588
+ /**
589
+ * Atomic ref update - all or nothing
590
+ */
591
+ export async function atomicRefUpdate(commands, store) {
592
+ const results = [];
593
+ const originalRefs = new Map();
594
+ // First, validate all commands and save original state
595
+ for (const cmd of commands) {
596
+ const currentRef = await store.getRef(cmd.refName);
597
+ originalRefs.set(cmd.refName, currentRef?.sha || null);
598
+ // Verify old SHA matches
599
+ const currentSha = currentRef?.sha || ZERO_SHA;
600
+ if (cmd.type === 'update' && currentSha !== cmd.oldSha) {
601
+ // One command failed - mark all as failed
602
+ for (const c of commands) {
603
+ results.push({
604
+ refName: c.refName,
605
+ success: false,
606
+ error: 'atomic push failed: lock failed on ' + cmd.refName,
607
+ });
608
+ }
609
+ return { success: false, results };
610
+ }
611
+ }
612
+ // Try to apply all updates
613
+ try {
614
+ for (const cmd of commands) {
615
+ if (cmd.type === 'delete') {
616
+ await store.deleteRef(cmd.refName);
617
+ }
618
+ else {
619
+ await store.setRef(cmd.refName, cmd.newSha);
620
+ }
621
+ results.push({ refName: cmd.refName, success: true });
622
+ }
623
+ return { success: true, results };
624
+ }
625
+ catch (error) {
626
+ // Rollback on failure
627
+ for (const [refName, originalSha] of originalRefs) {
628
+ if (originalSha === null) {
629
+ await store.deleteRef(refName);
630
+ }
631
+ else {
632
+ await store.setRef(refName, originalSha);
633
+ }
634
+ }
635
+ // Mark all as failed
636
+ const failedResults = commands.map((cmd) => ({
637
+ refName: cmd.refName,
638
+ success: false,
639
+ error: 'atomic push failed: rollback due to error',
640
+ }));
641
+ return { success: false, results: failedResults };
642
+ }
643
+ }
644
+ /**
645
+ * Execute pre-receive hook
646
+ */
647
+ export async function executePreReceiveHook(commands, _store, hookFn, env = {}, options) {
648
+ const timeout = options?.timeout || 30000;
649
+ try {
650
+ const result = await Promise.race([
651
+ hookFn(commands, env),
652
+ new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), timeout)),
653
+ ]);
654
+ return result;
655
+ }
656
+ catch (error) {
657
+ if (error instanceof Error && error.message === 'timeout') {
658
+ return { success: false, message: 'pre-receive hook timeout' };
659
+ }
660
+ return { success: false, message: String(error) };
661
+ }
662
+ }
663
+ /**
664
+ * Execute update hook for each ref
665
+ */
666
+ export async function executeUpdateHook(commands, _store, hookFn, env = {}) {
667
+ const results = [];
668
+ for (const cmd of commands) {
669
+ const result = await hookFn(cmd.refName, cmd.oldSha, cmd.newSha, env);
670
+ results.push({
671
+ refName: cmd.refName,
672
+ success: result.success,
673
+ error: result.success ? undefined : result.message,
674
+ });
675
+ }
676
+ return { results };
677
+ }
678
+ /**
679
+ * Execute post-receive hook
680
+ */
681
+ export async function executePostReceiveHook(commands, results, _store, hookFn, options) {
682
+ // Filter to only successful updates
683
+ const successfulCommands = commands.filter((_cmd, idx) => results[idx]?.success);
684
+ // Build environment with push options
685
+ const env = {};
686
+ if (options?.pushOptions && options.pushOptions.length > 0) {
687
+ env.GIT_PUSH_OPTION_COUNT = String(options.pushOptions.length);
688
+ options.pushOptions.forEach((opt, idx) => {
689
+ env[`GIT_PUSH_OPTION_${idx}`] = opt;
690
+ });
691
+ }
692
+ const hookResult = await hookFn(successfulCommands, results, env);
693
+ return {
694
+ pushSuccess: true, // post-receive doesn't affect push success
695
+ hookSuccess: hookResult.success,
696
+ };
697
+ }
698
+ /**
699
+ * Execute post-update hook
700
+ */
701
+ export async function executePostUpdateHook(_commands, results, hookFn) {
702
+ // Get successfully updated ref names
703
+ const successfulRefNames = results.filter((r) => r.success).map((r) => r.refName);
704
+ // Only call hook if there were successful updates
705
+ if (successfulRefNames.length > 0) {
706
+ await hookFn(successfulRefNames);
707
+ }
708
+ }
709
+ // ============================================================================
710
+ // Report Status Formatting
711
+ // ============================================================================
712
+ /**
713
+ * Format report-status response
714
+ */
715
+ export function formatReportStatus(input) {
716
+ const lines = [];
717
+ // Unpack status line
718
+ const unpackLine = input.unpackStatus === 'ok' ? 'unpack ok\n' : `unpack ${input.unpackStatus}\n`;
719
+ lines.push(encodePktLine(unpackLine));
720
+ // Ref status lines
721
+ for (const result of input.refResults) {
722
+ if (result.success) {
723
+ lines.push(encodePktLine(`ok ${result.refName}\n`));
724
+ }
725
+ else {
726
+ lines.push(encodePktLine(`ng ${result.refName} ${result.error || 'failed'}\n`));
727
+ }
728
+ }
729
+ // End with flush
730
+ lines.push(FLUSH_PKT);
731
+ return lines.join('');
732
+ }
733
+ /**
734
+ * Format report-status-v2 response
735
+ */
736
+ export function formatReportStatusV2(input) {
737
+ const lines = [];
738
+ // Option lines first
739
+ if (input.options) {
740
+ for (const [key, value] of Object.entries(input.options)) {
741
+ lines.push(encodePktLine(`option ${key} ${value}\n`));
742
+ }
743
+ }
744
+ // Unpack status
745
+ const unpackLine = input.unpackStatus === 'ok' ? 'unpack ok\n' : `unpack ${input.unpackStatus}\n`;
746
+ lines.push(encodePktLine(unpackLine));
747
+ // Ref status lines
748
+ for (const result of input.refResults) {
749
+ if (result.success) {
750
+ let line = `ok ${result.refName}`;
751
+ if (result.forced) {
752
+ line += ' forced';
753
+ }
754
+ lines.push(encodePktLine(line + '\n'));
755
+ }
756
+ else {
757
+ lines.push(encodePktLine(`ng ${result.refName} ${result.error || 'failed'}\n`));
758
+ }
759
+ }
760
+ // End with flush
761
+ lines.push(FLUSH_PKT);
762
+ return lines.join('');
763
+ }
764
+ /**
765
+ * Format rejection message
766
+ */
767
+ export function rejectPush(refName, reason, options) {
768
+ if (options.sideBand) {
769
+ // Side-band channel 3 for errors
770
+ const message = `error: failed to push ${refName}: ${reason}\n`;
771
+ const data = encoder.encode(message);
772
+ const totalLength = 4 + 1 + data.length;
773
+ const hexLength = totalLength.toString(16).padStart(4, '0');
774
+ const result = new Uint8Array(totalLength);
775
+ result.set(encoder.encode(hexLength), 0);
776
+ result[4] = 3; // Error channel
777
+ result.set(data, 5);
778
+ return result;
779
+ }
780
+ // Report-status format
781
+ return `ng ${refName} ${reason}`;
782
+ }
783
+ // ============================================================================
784
+ // Full Receive-Pack Handler
785
+ // ============================================================================
786
+ /**
787
+ * Handle complete receive-pack request
788
+ */
789
+ export async function handleReceivePack(session, request, store) {
790
+ // Parse the request
791
+ const parsed = parseReceivePackRequest(request);
792
+ session.commands = parsed.commands;
793
+ // Merge capabilities from request
794
+ const requestCaps = parseReceiveCapabilities(parsed.capabilities.join(' '));
795
+ session.capabilities = { ...session.capabilities, ...requestCaps };
796
+ // Check if we need to report status
797
+ const needsReport = session.capabilities.reportStatus || session.capabilities.reportStatusV2;
798
+ // Validate packfile (if present and needed)
799
+ let unpackStatus = 'ok';
800
+ const hasNonDeleteCommands = parsed.commands.some((c) => c.type !== 'delete');
801
+ if (hasNonDeleteCommands && parsed.packfile.length > 0) {
802
+ const validation = await validatePackfile(parsed.packfile);
803
+ if (!validation.valid) {
804
+ unpackStatus = `error: ${validation.error}`;
805
+ }
806
+ else {
807
+ const unpackResult = await unpackObjects(parsed.packfile, store);
808
+ if (!unpackResult.success) {
809
+ unpackStatus = `error: ${unpackResult.error}`;
810
+ }
811
+ }
812
+ }
813
+ else if (hasNonDeleteCommands && parsed.packfile.length === 0) {
814
+ // Non-delete command but no packfile - this is OK for some cases
815
+ // but we should still validate
816
+ unpackStatus = 'ok';
817
+ }
818
+ // Process commands
819
+ const refResults = [];
820
+ for (const cmd of parsed.commands) {
821
+ // Validate ref name
822
+ if (!validateRefName(cmd.refName)) {
823
+ refResults.push({
824
+ refName: cmd.refName,
825
+ success: false,
826
+ error: 'invalid ref name',
827
+ });
828
+ continue;
829
+ }
830
+ // Check current ref state
831
+ const currentRef = await store.getRef(cmd.refName);
832
+ const currentSha = currentRef?.sha || ZERO_SHA;
833
+ // For updates and deletes, verify old SHA matches
834
+ if (cmd.type !== 'create') {
835
+ if (currentSha !== cmd.oldSha) {
836
+ refResults.push({
837
+ refName: cmd.refName,
838
+ success: false,
839
+ error: 'lock failed: ref has been updated',
840
+ });
841
+ continue;
842
+ }
843
+ }
844
+ // Handle delete
845
+ if (cmd.type === 'delete') {
846
+ if (!session.capabilities.deleteRefs) {
847
+ refResults.push({
848
+ refName: cmd.refName,
849
+ success: false,
850
+ error: 'delete-refs not enabled',
851
+ });
852
+ continue;
853
+ }
854
+ await store.deleteRef(cmd.refName);
855
+ refResults.push({ refName: cmd.refName, success: true });
856
+ continue;
857
+ }
858
+ // Handle create/update
859
+ if (cmd.type === 'update') {
860
+ // Check fast-forward
861
+ const isFF = await validateFastForward(cmd.oldSha, cmd.newSha, store);
862
+ if (!isFF) {
863
+ refResults.push({
864
+ refName: cmd.refName,
865
+ success: false,
866
+ error: 'non-fast-forward update',
867
+ });
868
+ continue;
869
+ }
870
+ }
871
+ // Apply the update
872
+ await store.setRef(cmd.refName, cmd.newSha);
873
+ refResults.push({ refName: cmd.refName, success: true });
874
+ }
875
+ // Build response
876
+ if (needsReport) {
877
+ const statusFormat = session.capabilities.reportStatusV2
878
+ ? formatReportStatusV2({ unpackStatus, refResults })
879
+ : formatReportStatus({ unpackStatus, refResults });
880
+ return encoder.encode(statusFormat);
881
+ }
882
+ // No report needed
883
+ return new Uint8Array(0);
884
+ }
885
+ //# sourceMappingURL=receive-pack.js.map