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.
- package/LICENSE +21 -0
- package/README.md +156 -0
- package/dist/durable-object/object-store.d.ts +113 -0
- package/dist/durable-object/object-store.d.ts.map +1 -0
- package/dist/durable-object/object-store.js +387 -0
- package/dist/durable-object/object-store.js.map +1 -0
- package/dist/durable-object/schema.d.ts +17 -0
- package/dist/durable-object/schema.d.ts.map +1 -0
- package/dist/durable-object/schema.js +43 -0
- package/dist/durable-object/schema.js.map +1 -0
- package/dist/durable-object/wal.d.ts +111 -0
- package/dist/durable-object/wal.d.ts.map +1 -0
- package/dist/durable-object/wal.js +200 -0
- package/dist/durable-object/wal.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +101 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/adapter.d.ts +231 -0
- package/dist/mcp/adapter.d.ts.map +1 -0
- package/dist/mcp/adapter.js +502 -0
- package/dist/mcp/adapter.js.map +1 -0
- package/dist/mcp/sandbox.d.ts +261 -0
- package/dist/mcp/sandbox.d.ts.map +1 -0
- package/dist/mcp/sandbox.js +983 -0
- package/dist/mcp/sandbox.js.map +1 -0
- package/dist/mcp/sdk-adapter.d.ts +413 -0
- package/dist/mcp/sdk-adapter.d.ts.map +1 -0
- package/dist/mcp/sdk-adapter.js +672 -0
- package/dist/mcp/sdk-adapter.js.map +1 -0
- package/dist/mcp/tools.d.ts +133 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +1604 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/ops/blame.d.ts +148 -0
- package/dist/ops/blame.d.ts.map +1 -0
- package/dist/ops/blame.js +754 -0
- package/dist/ops/blame.js.map +1 -0
- package/dist/ops/branch.d.ts +215 -0
- package/dist/ops/branch.d.ts.map +1 -0
- package/dist/ops/branch.js +608 -0
- package/dist/ops/branch.js.map +1 -0
- package/dist/ops/commit-traversal.d.ts +209 -0
- package/dist/ops/commit-traversal.d.ts.map +1 -0
- package/dist/ops/commit-traversal.js +755 -0
- package/dist/ops/commit-traversal.js.map +1 -0
- package/dist/ops/commit.d.ts +221 -0
- package/dist/ops/commit.d.ts.map +1 -0
- package/dist/ops/commit.js +606 -0
- package/dist/ops/commit.js.map +1 -0
- package/dist/ops/merge-base.d.ts +223 -0
- package/dist/ops/merge-base.d.ts.map +1 -0
- package/dist/ops/merge-base.js +581 -0
- package/dist/ops/merge-base.js.map +1 -0
- package/dist/ops/merge.d.ts +385 -0
- package/dist/ops/merge.d.ts.map +1 -0
- package/dist/ops/merge.js +1203 -0
- package/dist/ops/merge.js.map +1 -0
- package/dist/ops/tag.d.ts +182 -0
- package/dist/ops/tag.d.ts.map +1 -0
- package/dist/ops/tag.js +608 -0
- package/dist/ops/tag.js.map +1 -0
- package/dist/ops/tree-builder.d.ts +82 -0
- package/dist/ops/tree-builder.d.ts.map +1 -0
- package/dist/ops/tree-builder.js +246 -0
- package/dist/ops/tree-builder.js.map +1 -0
- package/dist/ops/tree-diff.d.ts +243 -0
- package/dist/ops/tree-diff.d.ts.map +1 -0
- package/dist/ops/tree-diff.js +657 -0
- package/dist/ops/tree-diff.js.map +1 -0
- package/dist/pack/delta.d.ts +68 -0
- package/dist/pack/delta.d.ts.map +1 -0
- package/dist/pack/delta.js +343 -0
- package/dist/pack/delta.js.map +1 -0
- package/dist/pack/format.d.ts +84 -0
- package/dist/pack/format.d.ts.map +1 -0
- package/dist/pack/format.js +261 -0
- package/dist/pack/format.js.map +1 -0
- package/dist/pack/full-generation.d.ts +327 -0
- package/dist/pack/full-generation.d.ts.map +1 -0
- package/dist/pack/full-generation.js +1159 -0
- package/dist/pack/full-generation.js.map +1 -0
- package/dist/pack/generation.d.ts +118 -0
- package/dist/pack/generation.d.ts.map +1 -0
- package/dist/pack/generation.js +459 -0
- package/dist/pack/generation.js.map +1 -0
- package/dist/pack/index.d.ts +181 -0
- package/dist/pack/index.d.ts.map +1 -0
- package/dist/pack/index.js +552 -0
- package/dist/pack/index.js.map +1 -0
- package/dist/refs/branch.d.ts +224 -0
- package/dist/refs/branch.d.ts.map +1 -0
- package/dist/refs/branch.js +170 -0
- package/dist/refs/branch.js.map +1 -0
- package/dist/refs/storage.d.ts +208 -0
- package/dist/refs/storage.d.ts.map +1 -0
- package/dist/refs/storage.js +421 -0
- package/dist/refs/storage.js.map +1 -0
- package/dist/refs/tag.d.ts +230 -0
- package/dist/refs/tag.d.ts.map +1 -0
- package/dist/refs/tag.js +188 -0
- package/dist/refs/tag.js.map +1 -0
- package/dist/storage/lru-cache.d.ts +188 -0
- package/dist/storage/lru-cache.d.ts.map +1 -0
- package/dist/storage/lru-cache.js +410 -0
- package/dist/storage/lru-cache.js.map +1 -0
- package/dist/storage/object-index.d.ts +140 -0
- package/dist/storage/object-index.d.ts.map +1 -0
- package/dist/storage/object-index.js +166 -0
- package/dist/storage/object-index.js.map +1 -0
- package/dist/storage/r2-pack.d.ts +394 -0
- package/dist/storage/r2-pack.d.ts.map +1 -0
- package/dist/storage/r2-pack.js +1062 -0
- package/dist/storage/r2-pack.js.map +1 -0
- package/dist/tiered/cdc-pipeline.d.ts +316 -0
- package/dist/tiered/cdc-pipeline.d.ts.map +1 -0
- package/dist/tiered/cdc-pipeline.js +771 -0
- package/dist/tiered/cdc-pipeline.js.map +1 -0
- package/dist/tiered/migration.d.ts +242 -0
- package/dist/tiered/migration.d.ts.map +1 -0
- package/dist/tiered/migration.js +592 -0
- package/dist/tiered/migration.js.map +1 -0
- package/dist/tiered/parquet-writer.d.ts +248 -0
- package/dist/tiered/parquet-writer.d.ts.map +1 -0
- package/dist/tiered/parquet-writer.js +555 -0
- package/dist/tiered/parquet-writer.js.map +1 -0
- package/dist/tiered/read-path.d.ts +141 -0
- package/dist/tiered/read-path.d.ts.map +1 -0
- package/dist/tiered/read-path.js +204 -0
- package/dist/tiered/read-path.js.map +1 -0
- package/dist/types/objects.d.ts +53 -0
- package/dist/types/objects.d.ts.map +1 -0
- package/dist/types/objects.js +291 -0
- package/dist/types/objects.js.map +1 -0
- package/dist/types/storage.d.ts +117 -0
- package/dist/types/storage.d.ts.map +1 -0
- package/dist/types/storage.js +8 -0
- package/dist/types/storage.js.map +1 -0
- package/dist/utils/hash.d.ts +31 -0
- package/dist/utils/hash.d.ts.map +1 -0
- package/dist/utils/hash.js +60 -0
- package/dist/utils/hash.js.map +1 -0
- package/dist/utils/sha1.d.ts +26 -0
- package/dist/utils/sha1.d.ts.map +1 -0
- package/dist/utils/sha1.js +127 -0
- package/dist/utils/sha1.js.map +1 -0
- package/dist/wire/capabilities.d.ts +236 -0
- package/dist/wire/capabilities.d.ts.map +1 -0
- package/dist/wire/capabilities.js +437 -0
- package/dist/wire/capabilities.js.map +1 -0
- package/dist/wire/pkt-line.d.ts +67 -0
- package/dist/wire/pkt-line.d.ts.map +1 -0
- package/dist/wire/pkt-line.js +145 -0
- package/dist/wire/pkt-line.js.map +1 -0
- package/dist/wire/receive-pack.d.ts +302 -0
- package/dist/wire/receive-pack.d.ts.map +1 -0
- package/dist/wire/receive-pack.js +885 -0
- package/dist/wire/receive-pack.js.map +1 -0
- package/dist/wire/smart-http.d.ts +321 -0
- package/dist/wire/smart-http.d.ts.map +1 -0
- package/dist/wire/smart-http.js +654 -0
- package/dist/wire/smart-http.js.map +1 -0
- package/dist/wire/upload-pack.d.ts +333 -0
- package/dist/wire/upload-pack.d.ts.map +1 -0
- package/dist/wire/upload-pack.js +850 -0
- package/dist/wire/upload-pack.js.map +1 -0
- 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
|