git ssb

0+

cel / pull-git-remote-helper



Tree: a59538039caf66a1a2dfa869c577df77ecb11288

Files: a59538039caf66a1a2dfa869c577df77ecb11288 / index.js

13084 bytesRaw
1var pull = require('pull-stream')
2var cat = require('pull-cat')
3var buffered = require('pull-buffered')
4var Repo = require('pull-git-repo')
5var pack = require('pull-git-pack')
6var pktLine = require('./lib/pkt-line')
7var indexPack = require('pull-git-pack/lib/index-pack')
8var util = require('./lib/util')
9var multicb = require('multicb')
10var ProgressBar = require('progress')
11var pkg = require('./package.json')
12
13var agentCap = 'agent=' + pkg.name + '/' + pkg.version
14
15var dummyCapsRef = {
16 name: 'capabilities^{}',
17 hash: '0000000000000000000000000000000000000000'
18}
19
20function handleOption(options, name, value) {
21 switch (name) {
22 case 'verbosity':
23 options.verbosity = value|0
24 return true
25 case 'progress':
26 options.progress = !!value && value !== 'false'
27 return true
28 /*
29 case 'followtags':
30 options.followtags = !!value
31 return true
32 case 'depth':
33 options.depth = depth|0
34 return true
35 case 'deepen-relative':
36 options.deepenRelative = !!value && value !== 'false'
37 return true
38 */
39 default:
40 console.error('unknown option', name + ': ' + value)
41 return false
42 }
43}
44
45function capabilitiesSource() {
46 return pull.once([
47 'option',
48 'connect',
49 ].join('\n') + '\n\n')
50}
51
52function optionSource(cmd, options) {
53 var args = util.split2(cmd)
54 var msg = handleOption(options, args[0], args[1])
55 msg = (msg === true) ? 'ok'
56 : (msg === false) ? 'unsupported'
57 : 'error ' + msg
58 return pull.once(msg + '\n')
59}
60
61// transform ref objects into lines
62function listRefs(read) {
63 var ended
64 return function (abort, cb) {
65 if (ended) return cb(ended)
66 read(abort, function (end, ref) {
67 ended = end
68 if (end === true) cb(null, '\n')
69 if (end) cb(end)
70 else cb(null,
71 [ref.value, ref.name].concat(ref.attrs || []).join(' ') + '\n')
72 })
73 }
74}
75
76// upload-pack: fetch to client
77// references:
78// git/documentation/technical/pack-protocol.txt
79// git/documentation/technical/protocol-capabilities.txt
80function uploadPack(read, repo, options) {
81 var sendRefs = receivePackHeader([
82 agentCap,
83 ], repo.refs(), repo.symrefs())
84
85 var lines = pktLine.decode(read, {
86 onCaps: onCaps,
87 verbosity: options.verbosity
88 })
89 var readWantHave = lines.haves()
90 var acked
91 var haves = {}
92 var sendPack
93 var wants = {}
94 var shallows = {}
95 var aborted
96 var hasWants
97 var gotHaves
98
99 function onCaps(caps) {
100 if (options.verbosity >= 2) {
101 console.error('client capabilities:', caps)
102 }
103 }
104
105 function readWant(abort, cb) {
106 if (abort) return
107 // read upload request (wants list) from client
108 readWantHave(null, function next(end, want) {
109 if (end || want.type == 'flush-pkt') {
110 cb(end || true, cb)
111 return
112 }
113 if (want.type == 'want') {
114 wants[want.hash] = true
115 hasWants = true
116 } else if (want.type == 'shallow') {
117 shallows[want.hash] = true
118 } else {
119 var err = new Error("Unknown thing", want.type, want.hash)
120 return readWantHave(err, function (e) { cb(e || err) })
121 }
122 readWantHave(null, next)
123 })
124 }
125
126 function readHave(abort, cb) {
127 // Read upload haves (haves list).
128 // On first obj-id that we have, ACK
129 // If we have none, NAK.
130 if (abort) return
131 if (gotHaves) return cb(true)
132 readWantHave(null, function next(end, have) {
133 if (end === true) { // done
134 gotHaves = true
135 if (!acked) {
136 cb(null, 'NAK')
137 } else {
138 cb(true)
139 }
140 } else if (have.type === 'flush-pkt') {
141 // found no common object
142 if (!acked) {
143 cb(null, 'NAK')
144 } else {
145 readWantHave(null, next)
146 }
147 } else if (end)
148 cb(end)
149 else if (have.type != 'have')
150 cb(new Error('Unknown have' + JSON.stringify(have)))
151 else {
152 haves[have.hash] = true
153 if (acked) {
154 readWantHave(null, next)
155 } else if (repo.hasObjectQuick) {
156 gotHasObject(null, repo.hasObjectQuick(have.hash))
157 } else {
158 repo.hasObjectFromAny(have.hash, gotHasObject)
159 }
160 }
161 function gotHasObject(err, haveIt) {
162 if (err) return cb(err)
163 if (!haveIt) return readWantHave(null, next)
164 acked = true
165 cb(null, 'ACK ' + have.hash)
166 }
167 })
168 }
169
170 function readPack(abort, cb) {
171 if (sendPack) return sendPack(abort, cb)
172 if (abort || aborted) return console.error('abrt', abort || aborted), cb(abort || aborted)
173 // send pack file to client
174 if (!hasWants) return cb(true)
175 if (options.verbosity >= 2) {
176 console.error('wants', wants)
177 }
178 repo.getPack(wants, haves, options, function (err, _sendPack) {
179 if (err) return cb(err)
180 sendPack = _sendPack
181 sendPack(null, cb)
182 })
183 }
184
185 // Packfile negotiation
186 return cat([
187 pktLine.encode(cat([
188 sendRefs,
189 pull.once(''),
190 readWant,
191 readHave
192 ])),
193 readPack
194 ])
195}
196
197// through stream to show a progress bar for objects being read
198function progressObjects(options) {
199 // Only show progress bar if it is requested and if it won't interfere with
200 // the debug output
201 if (!options.progress || options.verbosity > 1) {
202 var dummyProgress = function (readObject) { return readObject }
203 dummyProgress.setNumObjects = function () {}
204 return dummyProgress
205 }
206
207 var numObjects
208 var size = process.stderr.columns
209 var bar = new ProgressBar(':percent :bar', {
210 total: size,
211 clear: true
212 })
213
214 var progress = function (readObject) {
215 return function (abort, cb) {
216 readObject(abort, function next(end, object) {
217 if (end === true) {
218 bar.terminate()
219 } else if (!end) {
220 var name = object.type + ' ' + object.length
221 bar.tick(size / numObjects)
222 }
223
224 cb(end, object)
225 })
226 }
227 }
228 // TODO: put the num objects in the objects stream as a header object
229 progress.setNumObjects = function (n) {
230 numObjects = n
231 }
232 return progress
233}
234
235// Get a line for each ref that we have. The first line also has capabilities.
236// Wrap with pktLine.encode.
237function receivePackHeader(capabilities, refSource, symrefs) {
238 var hasRefs = false
239 var first = true
240 var symrefed = {}
241 var symrefsObj = {}
242
243 return cat([
244 function (end, cb) {
245 if (end) cb(true)
246 else if (!symrefs) cb(true)
247 else pull(
248 symrefs,
249 pull.map(function (sym) {
250 symrefed[sym.ref] = true
251 symrefsObj[sym.name] = sym.ref
252 return 'symref=' + sym.name + ':' + sym.ref
253 }),
254 pull.collect(function (err, symrefCaps) {
255 if (err) return cb(err)
256 capabilities = capabilities.concat(symrefCaps)
257 cb(true)
258 })
259 )
260 },
261 pull(
262 cat([refSource, pull.once(dummyCapsRef)]),
263 pull.map(function (ref) {
264 if (ref !== dummyCapsRef) hasRefs = true
265 else if (hasRefs) return []
266 else return [ref]
267
268 // insert symrefs next to the refs that they point to
269 var out = [ref]
270 if (ref.name in symrefed)
271 for (var symrefName in symrefsObj)
272 if (symrefsObj[symrefName] === ref.name)
273 out.push({name: symrefName, hash: ref.hash})
274 return out
275 }),
276 pull.flatten(),
277 pull.map(function (ref) {
278 var name = ref.name
279 var value = ref.hash
280 if (first) {
281 first = false
282 name += '\0' + capabilities.join(' ')
283 }
284 return value + ' ' + name
285 })
286 )
287 ])
288}
289
290// receive-pack: push from client
291function receivePack(read, repo, options) {
292 var serverCaps = [
293 agentCap,
294 'delete-refs',
295 'quiet',
296 'no-thin'
297 ]
298 if (repo.setPushOptions) serverCaps.push('push-options')
299 var sendRefs = receivePackHeader(serverCaps, repo.refs(), null)
300 var done = multicb({pluck: 1})
301
302 var clientCaps = {}
303
304 function onCaps(caps) {
305 clientCaps = caps
306 if (options.verbosity >= 2) {
307 console.error('client capabilities:', caps)
308 }
309 }
310
311 return pktLine.encode(
312 cat([
313 // send our refs
314 sendRefs,
315 pull.once(''),
316 function (abort, cb) {
317 if (abort) return
318 // receive their refs
319 var lines = pktLine.decode(read, {
320 onCaps: onCaps,
321 verbosity: options.verbosity
322 })
323 pull(
324 lines.updates,
325 pull.collect(function (err, updates) {
326 if (err) return cb(err)
327 if (updates.length === 0) return cb(true)
328 var usePushOpts = clientCaps['push-options']
329 pull(
330 usePushOpts ? lines.pktLineStrs : pull.empty(),
331 pull.reduce(function (opts, opt) {
332 opts[opt] = true
333 return opts
334 }, {}, pushOptsCb)
335 )
336 function pushOptsCb(err, pushOpts) {
337 if (err) return cb(err)
338 repo.setPushOptions(pushOpts)
339 var progress = progressObjects(options)
340
341 var hasPack = !updates.every(function (update) {
342 return update.new === null
343 })
344 if (repo.uploadPack) {
345 if (!hasPack) {
346 repo.uploadPack(pull.values(updates), pull.empty(), done())
347 } else {
348 var idxCb = done()
349 pull(lines.passthrough, indexPack(function (err, idx, packfileFixed) {
350 if (err) return idxCb(err)
351 repo.uploadPack(pull.values(updates), pull.once({
352 pack: pull(
353 packfileFixed,
354 // for some reason i was getting zero length buffers which
355 // were causing muxrpc to fail, so remove them here.
356 pull.filter(function (buf) {
357 return buf.length
358 })
359 ),
360 idx: idx
361 }), idxCb)
362 }))
363 }
364 } else {
365 repo.update(pull.values(updates), !hasPack ? pull.empty() : pull(
366 lines.passthrough,
367 pack.decode({
368 verbosity: options.verbosity,
369 onHeader: function (numObjects) {
370 progress.setNumObjects(numObjects)
371 }
372 }, repo, done()),
373 progress
374 ), done())
375 }
376
377 done(function (err) {
378 cb(err || true)
379 })
380 }
381 })
382 )
383 },
384 pull.once('unpack ok')
385 ])
386 )
387}
388
389function prepend(data, read) {
390 var done
391 return function (end, cb) {
392 if (done) {
393 read(end, cb)
394 } else {
395 done = true
396 cb(null, data)
397 }
398 }
399}
400
401module.exports = function (repo) {
402 var ended
403 var options = {
404 verbosity: +process.env.GIT_VERBOSITY || 1,
405 progress: false
406 }
407
408 repo = Repo(repo)
409
410 function handleConnect(cmd, read) {
411 var args = util.split2(cmd)
412 switch (args[0]) {
413 case 'git-upload-pack':
414 return prepend('\n', uploadPack(read, repo, options))
415 case 'git-receive-pack':
416 return prepend('\n', receivePack(read, repo, options))
417 default:
418 return pull.error(new Error('Unknown service ' + args[0]))
419 }
420 }
421
422 function handleCommand(line, read) {
423 var args = util.split2(line)
424 switch (args[0]) {
425 case 'capabilities':
426 return capabilitiesSource()
427 case 'list':
428 return listRefs(refSource)
429 case 'connect':
430 return handleConnect(args[1], read)
431 case 'option':
432 return optionSource(args[1], options)
433 default:
434 return pull.error(new Error('Unknown command ' + line))
435 }
436 }
437
438 return function (read) {
439 var b = buffered()
440 if (options.verbosity >= 3) {
441 read = pull.through(function (data) {
442 console.error('>', JSON.stringify(data.toString('ascii')))
443 })(read)
444 }
445 b(read)
446
447 var command
448
449 function getCommand(cb) {
450 b.lines(null, function next(end, line) {
451 if (ended = end)
452 return cb(end)
453
454 if (line == '')
455 return b.lines(null, next)
456
457 if (options.verbosity > 1)
458 console.error('command:', line)
459
460 var cmdSource = handleCommand(line, b.passthrough)
461 cb(null, cmdSource)
462 })
463 }
464
465 return function next(abort, cb) {
466 if (ended) return cb(ended)
467
468 if (!command) {
469 if (abort) return
470 getCommand(function (end, cmd) {
471 command = cmd
472 next(end, cb)
473 })
474 return
475 }
476
477 command(abort, function (err, data) {
478 if (err) {
479 command = null
480 if (err !== true)
481 cb(err, data)
482 else
483 next(abort, cb)
484 } else {
485 if (options.verbosity >= 3) {
486 console.error('<', JSON.stringify(data))
487 }
488 cb(null, data)
489 }
490 })
491 }
492 }
493}
494

Built with git-ssb-web