git ssb

0+

cel / pull-git-remote-helper



Tree: bed0b983a87d456d6c072f88bb5f7081614d4ed1

Files: bed0b983a87d456d6c072f88bb5f7081614d4ed1 / index.js

11970 bytesRaw
1var packCodec = require('js-git/lib/pack-codec')
2var pull = require('pull-stream')
3var cat = require('pull-cat')
4var buffered = require('pull-buffered')
5var pack = require('./pack')
6
7function handleOption(options, name, value) {
8 switch (name) {
9 case 'verbosity':
10 options.verbosity = +value || 0
11 return true
12 case 'progress':
13 options.progress = !!value && value !== 'false'
14 return true
15 default:
16 console.error('unknown option', name + ': ' + value)
17 return false
18 }
19}
20
21function capabilitiesSource() {
22 return pull.once([
23 'option',
24 'connect',
25 ].join('\n') + '\n\n')
26}
27
28function split2(str, delim) {
29 var i = str.indexOf(delim || ' ')
30 return (i === -1) ? [str, ''] : [
31 str.substr(0, i),
32 str.substr(i + 1)
33 ]
34}
35
36function split3(str) {
37 var args = split2(str)
38 return [args[0]].concat(split2(args[1]))
39}
40
41function optionSource(cmd, options) {
42 var args = split2(cmd)
43 var msg = handleOption(options, args[0], args[1])
44 msg = (msg === true) ? 'ok'
45 : (msg === false) ? 'unsupported'
46 : 'error ' + msg
47 return pull.once(msg + '\n')
48}
49
50// transform ref objects into lines
51function listRefs(read) {
52 var ended
53 return function (abort, cb) {
54 if (ended) return cb(ended)
55 read(abort, function (end, ref) {
56 ended = end
57 if (end === true) cb(null, '\n')
58 if (end) cb(end)
59 else cb(null,
60 [ref.value, ref.name].concat(ref.attrs || []).join(' ') + '\n')
61 })
62 }
63}
64
65// upload-pack: fetch to client
66function uploadPack(read, getObjects, refSource, wantSink, options) {
67 /* multi_ack thin-pack side-band side-band-64k ofs-delta shallow no-progress
68 * include-tag multi_ack_detailed symref=HEAD:refs/heads/master
69 * agent=git/2.7.0 */
70 var sendRefs = receivePackHeader([
71 ], refSource, false)
72
73 var lines = packLineDecode(read, options)
74 var readHave = lines.haves()
75 var acked
76 var commonHash
77 var sendPack
78 var earlyDisconnect
79
80 function haveObject(hash, cb) {
81 cb(/* TODO */)
82 // cb(true)
83 }
84
85 // Packfile negotiation
86 return cat([
87 packLineEncode(cat([
88 sendRefs,
89 pull.once(''),
90 function (abort, cb) {
91 if (abort) return
92 if (acked) return cb(true)
93 // read upload request (wants list) from client
94 var readWant = lines.wants(wantsDone)
95 readWant(null, function (end, want) {
96 if (end === true) {
97 // client disconnected before sending wants
98 earlyDisconnect = true
99 cb(true)
100 } else if (end) {
101 cb(end)
102 } else {
103 wantSink(readWant)
104 }
105 })
106
107 function wantsDone(err) {
108 // console.error('wants done', err, earlyDisconnect)
109 if (err) return cb(err)
110 if (earlyDisconnect) return cb(true)
111 // Read upload haves (haves list).
112 // On first obj-id that we have, ACK
113 // If we have none, NAK.
114 // TODO: implement multi_ack_detailed
115 readHave(null, function next(end, have) {
116 if (end === true) {
117 // found no common object
118 acked = true
119 cb(null, 'NAK')
120 } else if (end)
121 cb(end)
122 else if (have.type != 'have')
123 cb(new Error('Unknown have' + JSON.stringify(have)))
124 else
125 haveObject(have.hash, function (haveIt) {
126 if (!haveIt)
127 return readHave(null, next)
128 commonHash = haveIt
129 acked = true
130 cb(null, 'ACK ' + have.hash)
131 })
132 })
133 }
134 },
135 ])),
136 function havesDone(abort, cb) {
137 // console.error("haves done", abort && typeof abort, sendPack && typeof sendPack, abort, earlyDisconnect)
138 if (abort || earlyDisconnect) return cb(abort || true)
139 // send pack file to client
140 if (!sendPack)
141 getObjects(commonHash, function (err, numObjects, readObject) {
142 sendPack = pack.encode(numObjects, readObject)
143 havesDone(abort, cb)
144 })
145 else
146 sendPack(abort, cb)
147 }
148 ])
149}
150
151function packLineEncode(read) {
152 var ended
153 return function (end, cb) {
154 if (ended) return cb(ended)
155 read(end, function (end, data) {
156 if (ended = end) {
157 cb(end)
158 } else {
159 if (data)
160 data += '\n'
161 else
162 data = ''
163 var len = data ? data.length + 4 : 0
164 var hexLen = ('000' + len.toString(16)).substr(-4)
165 var pkt = hexLen + data
166 // console.error('>', JSON.stringify(pkt))
167 cb(end, pkt)
168 }
169 })
170 }
171}
172
173function rev(str) {
174 return str === '0000000000000000000000000000000000000000' ? null : str
175}
176
177/* pull-stream/source.js */
178function abortCb(cb, abort, onAbort) {
179 cb(abort)
180 onAbort && onAbort(abort === true ? null: abort)
181 return
182}
183
184function packLineDecode(read, options) {
185 var b = buffered(read)
186 var readPrefix = b.chunks(4)
187 var ended
188
189 function readPackLine(abort, cb) {
190 if (ended) return cb(ended)
191 readPrefix(abort, function (end, buf) {
192 if (ended = end) return cb(end)
193 var len = parseInt(buf, 16)
194 if (!len)
195 return cb(null, new Buffer(''))
196 // TODO: figure out this -4 thing
197 b.chunks(len - 4)(null, function (end, buf) {
198 if (ended = end) return cb(end)
199 cb(end, buf)
200 })
201 })
202 }
203
204 function readPackLineStr(abort, cb) {
205 if (ended) return cb(ended)
206 readPackLine(abort, function (end, buf) {
207 if (ended = end) return cb(end)
208 // trim newline
209 var len = buf.length
210 if (buf[len - 1] == 0xa)
211 len--
212 var line = buf.toString('ascii', 0, len)
213 cb(null, line)
214 })
215 }
216
217 function readUpdate(abort, cb) {
218 readPackLine(abort, function (end, line) {
219 if (end) return cb(end)
220 if (options.verbosity >= 2)
221 console.error('line', line.toString('ascii'))
222 if (!line.length) return cb(true)
223 var args = split3(line.toString('ascii'))
224 var args2 = split2(args[2], '\0')
225 var caps = args2[1]
226 if (caps && options.verbosity >= 2)
227 console.error('update capabilities:', caps)
228 cb(null, {
229 old: rev(args[0]),
230 new: rev(args[1]),
231 name: args2[0]
232 })
233 })
234 }
235
236 function havesWants(onEnd) {
237 return function readWant(abort, cb) {
238 readPackLineStr(abort, function (end, line) {
239 if (end) return abortCb(cb, end, onEnd)
240 if (options.verbosity >= 2)
241 console.error('line', line)
242 // if (!line.length) return cb(true)
243 if (!line.length || line == 'done')
244 return abortCb(cb, true, onEnd)
245 var args = split3(line)
246 var caps = args[2]
247 if (caps && options.verbosity >= 2)
248 console.error('want capabilities:', caps)
249 cb(null, {
250 type: args[0],
251 hash: args[1],
252 })
253 })
254 }
255 }
256
257 /*
258 function readWant(abort, cb) {
259 readPackLine(abort, function (end, line) {
260 if (end) return cb(end)
261 if (options.verbosity >= 2)
262 console.error('line', line.toString('ascii'))
263 if (!line.length || line == 'done') {
264 console.error('WANTS done', line, line.length)
265 return cb(true)
266 }
267 var args = split3(line.toString('ascii'))
268 var caps = args[2]
269 if (caps && options.verbosity >= 2)
270 console.error('want capabilities:', caps)
271 cb(null, {
272 type: args[0],
273 hash: args[1].replace(/\n$/, ''),
274 })
275 })
276 }
277 */
278
279 b.packLines = readPackLine
280 b.updates = readUpdate
281 b.wants = b.haves = havesWants
282
283 return b
284}
285
286// run a callback when a pipeline ends
287// TODO: find a better way to do this
288function onThroughEnd(onEnd) {
289 return function (read) {
290 return function (end, cb) {
291 read(end, function (end, data) {
292 cb(end, data)
293 if (end)
294 onEnd(end === true ? null : end)
295 })
296 }
297 }
298}
299
300/*
301TODO: investigate capabilities
302report-status delete-refs side-band-64k quiet atomic ofs-delta
303*/
304
305// Get a line for each ref that we have. The first line also has capabilities.
306// Wrap with packLineEncode.
307function receivePackHeader(capabilities, refSource, usePlaceholder) {
308 var first = true
309 var ended
310 return function (abort, cb) {
311 if (ended) return cb(true)
312 refSource(abort, function (end, ref) {
313 ended = end
314 var name = ref && ref.name
315 var value = ref && ref.value
316 if (first && usePlaceholder) {
317 first = false
318 if (end) {
319 // use placeholder data if there are no refs
320 value = '0000000000000000000000000000000000000000'
321 name = 'capabilities^{}'
322 }
323 name += '\0' + capabilities.join(' ')
324 } else if (end) {
325 return cb(true)
326 }
327 cb(null, value + ' ' + name)
328 })
329 }
330}
331
332// receive-pack: push from client
333function receivePack(read, objectSink, refSource, refSink, options) {
334 var ended
335 var sendRefs = receivePackHeader([
336 'delete-refs',
337 ], refSource, true)
338
339 return packLineEncode(
340 cat([
341 // send our refs
342 sendRefs,
343 pull.once(''),
344 function (abort, cb) {
345 if (abort) return
346 // receive their refs
347 var lines = packLineDecode(read, options)
348 pull(
349 lines.updates,
350 onThroughEnd(refsDone),
351 refSink
352 )
353 function refsDone(err) {
354 if (err) return cb(err)
355 pull(
356 lines.passthrough,
357 pack.decode(cb),
358 objectSink
359 )
360 }
361 },
362 pull.once('unpack ok')
363 ])
364 )
365}
366
367function prepend(data, read) {
368 var done
369 return function (end, cb) {
370 if (done) {
371 read(end, cb)
372 } else {
373 done = true
374 cb(null, data)
375 }
376 }
377}
378
379module.exports = function (opts) {
380 var ended
381 var objectSink = opts.objectSink
382 var getObjects = opts.getObjects || function (id, cb) {
383 cb(null, 0, pull.empty())
384 }
385 var refSource = opts.refSource || pull.empty()
386 var refSink = opts.refSink || pull.drain()
387 var wantSink = opts.wantSink || pull.drain()
388
389 var options = {
390 verbosity: 1,
391 progress: false
392 }
393
394 function handleConnect(cmd, read) {
395 var args = split2(cmd)
396 switch (args[0]) {
397 case 'git-upload-pack':
398 return prepend('\n', uploadPack(read, getObjects, refSource,
399 wantSink, options))
400 case 'git-receive-pack':
401 return prepend('\n', receivePack(read, objectSink, refSource,
402 refSink, options))
403 default:
404 return pull.error(new Error('Unknown service ' + args[0]))
405 }
406 }
407
408 function handleCommand(line, read) {
409 var args = split2(line)
410 switch (args[0]) {
411 case 'capabilities':
412 return capabilitiesSource()
413 case 'list':
414 return listRefs(refSource)
415 case 'connect':
416 return handleConnect(args[1], read)
417 case 'option':
418 return optionSource(args[1], options)
419 default:
420 return pull.error(new Error('Unknown command ' + line))
421 }
422 }
423
424 return function (read) {
425 var b = buffered()
426 b(read)
427 var command
428
429 function getCommand(cb) {
430 b.lines(null, function next(end, line) {
431 if (ended = end)
432 return cb(end)
433
434 if (line == '')
435 return b.lines(null, next)
436
437 if (options.verbosity > 1)
438 console.error('command:', line)
439
440 var cmdSource = handleCommand(line, b.passthrough)
441 cb(null, cmdSource)
442 })
443 }
444
445 return function next(abort, cb) {
446 if (ended) return cb(ended)
447
448 if (!command) {
449 if (abort) return
450 getCommand(function (end, cmd) {
451 command = cmd
452 next(end, cb)
453 })
454 return
455 }
456
457 command(abort, function (err, data) {
458 if (err) {
459 command = null
460 if (err !== true)
461 cb(err, data)
462 else
463 next(abort, cb)
464 } else {
465 cb(null, data)
466 }
467 })
468 }
469 }
470}
471

Built with git-ssb-web