git ssb

0+

cel / pull-git-remote-helper



Tree: 20c102b7036f7fee19f151e269d33cd7da837a3b

Files: 20c102b7036f7fee19f151e269d33cd7da837a3b / index.js

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

Built with git-ssb-web