git ssb

0+

cel / pull-git-remote-helper



Tree: 666eaeb2e3d956c1c7e70c3e279e5b993a71a829

Files: 666eaeb2e3d956c1c7e70c3e279e5b993a71a829 / index.js

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

Built with git-ssb-web