git ssb

0+

cel / pull-git-remote-helper



Tree: 89903ba2fa42a5bb486607c2b44b9d740697af3b

Files: 89903ba2fa42a5bb486607c2b44b9d740697af3b / index.js

10450 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('./lib/pack')
6var pktLine = require('./lib/pkt-line')
7var util = require('./lib/util')
8var multicb = require('multicb')
9
10function handleOption(options, name, value) {
11 switch (name) {
12 case 'verbosity':
13 options.verbosity = +value || 0
14 return true
15 case 'progress':
16 options.progress = !!value && value !== 'false'
17 return true
18 default:
19 console.error('unknown option', name + ': ' + value)
20 return false
21 }
22}
23
24function capabilitiesSource() {
25 return pull.once([
26 'option',
27 'connect',
28 ].join('\n') + '\n\n')
29}
30
31function optionSource(cmd, options) {
32 var args = util.split2(cmd)
33 var msg = handleOption(options, args[0], args[1])
34 msg = (msg === true) ? 'ok'
35 : (msg === false) ? 'unsupported'
36 : 'error ' + msg
37 return pull.once(msg + '\n')
38}
39
40// transform ref objects into lines
41function listRefs(read) {
42 var ended
43 return function (abort, cb) {
44 if (ended) return cb(ended)
45 read(abort, function (end, ref) {
46 ended = end
47 if (end === true) cb(null, '\n')
48 if (end) cb(end)
49 else cb(null,
50 [ref.value, ref.name].concat(ref.attrs || []).join(' ') + '\n')
51 })
52 }
53}
54
55// upload-pack: fetch to client
56function uploadPack(read, repo, options) {
57 /* multi_ack thin-pack side-band side-band-64k ofs-delta shallow no-progress
58 * include-tag multi_ack_detailed symref=HEAD:refs/heads/master
59 * agent=git/2.7.0 */
60 var refSource = repo.refs.bind(repo)
61 var sendRefs = receivePackHeader([
62 ], refSource, false)
63
64 var lines = pktLine.decode(read, options)
65 var readHave = lines.haves()
66 var acked
67 var commonHash
68 var sendPack
69 var wants = {}
70 var shallows = {}
71
72 // Packfile negotiation
73 return cat([
74 pktLine.encode(cat([
75 sendRefs,
76 pull.once(''),
77 function (abort, cb) {
78 if (abort) return
79 if (acked) return cb(true)
80
81 // read upload request (wants list) from client
82 var readWant = lines.wants()
83 readWant(null, function (end, want) {
84 if (end === true) return // early client disconnect
85 else if (end) cb(end)
86 else nextWant(null, want)
87 })
88 function nextWant(end, want) {
89 if (end) return wantsDone(end === true ? null : end)
90 if (want.type == 'want') {
91 wants[want.hash] = true
92 } else if (want.type == 'shallow') {
93 shallows[want.hash] = true
94 } else {
95 var err = new Error("Unknown thing", want.type, want.hash)
96 return readWant(err, function (e) { cb(e || err) })
97 }
98 readWant(null, nextWant)
99 }
100
101 function wantsDone(err) {
102 if (err) return cb(err)
103 // Read upload haves (haves list).
104 // On first obj-id that we have, ACK
105 // If we have none, NAK.
106 // TODO: implement multi_ack_detailed
107 readHave(null, function next(end, have) {
108 if (end === true) {
109 // found no common object
110 acked = true
111 cb(null, 'NAK')
112 } else if (end)
113 cb(end)
114 else if (have.type != 'have')
115 cb(new Error('Unknown have' + JSON.stringify(have)))
116 else
117 repo.hasObject(have.hash, function (err, haveIt) {
118 if (err) return cb(err)
119 if (!haveIt)
120 return readHave(null, next)
121 commonHash = haveIt
122 acked = true
123 cb(null, 'ACK ' + have.hash)
124 })
125 })
126 }
127 },
128 ])),
129
130 function havesDone(abort, cb) {
131 if (abort) return cb(abort)
132 // send pack file to client
133 if (!sendPack)
134 getObjects(repo, commonHash, wants, shallows,
135 function (err, numObjects, readObjects) {
136 if (err) return cb(err)
137 sendPack = pack.encode(numObjects, readObjects)
138 havesDone(abort, cb)
139 }
140 )
141 else
142 sendPack(abort, cb)
143 }
144 ])
145}
146
147function getObjects(repo, commonHash, heads, shallows, cb) {
148 // get objects from commonHash to each head, inclusive.
149 // if commonHash is falsy, use root
150 var objects = []
151 var objectsAdded = {}
152 var done = multicb({pluck: 1})
153 var ended
154
155 // walk back from heads until get to commonHash
156 for (var hash in heads)
157 addObject(hash, done())
158
159 // TODO: only add new objects
160
161 function addObject(hash, cb) {
162 if (ended) return cb(ended)
163 if (hash in objectsAdded || hash == commonHash) return cb()
164 objectsAdded[hash] = true
165 repo.getObject(hash, function (err, object) {
166 if (err) return cb(err)
167 if (object.type == 'blob') {
168 objects.push(object)
169 cb()
170 } else {
171 // object must be read twice, so buffer it
172 bufferObject(object, function (err, object) {
173 if (err) return cb(err)
174 objects.push(object)
175 var hashes = getObjectLinks(object)
176 for (var sha1 in hashes)
177 addObject(sha1, done())
178 cb()
179 })
180 }
181 })
182 }
183
184 done(function (err) {
185 if (err) return cb(err)
186 cb(null, objects.length, pull.values(objects))
187 })
188}
189
190function bufferObject(object, cb) {
191 pull(
192 object.read,
193 pull.collect(function (err, bufs) {
194 if (err) return cb(err)
195 var buf = Buffer.concat(bufs, object.length)
196 cb(null, {
197 type: object.type,
198 length: object.length,
199 data: buf,
200 read: pull.once(buf)
201 })
202 })
203 )
204}
205
206// get hashes of git objects linked to from other git objects
207function getObjectLinks(object, cb) {
208 switch (object.type) {
209 case 'blob':
210 return {}
211 case 'tree':
212 return getTreeLinks(object.data)
213 case 'tag':
214 case 'commit':
215 return getCommitOrTagLinks(object.data)
216 }
217}
218
219function getTreeLinks(buf) {
220 var links = {}
221 for (var i = 0, j; j = buf.indexOf(0, i, 'ascii') + 1; i = j + 20) {
222 var hash = buf.slice(j, j + 20).toString('hex')
223 if (!(hash in links))
224 links[hash] = true
225 }
226 return links
227}
228
229function getCommitOrTagLinks(buf) {
230 var lines = buf.toString('utf8').split('\n')
231 var links = {}
232 // iterate until reach blank line (indicating start of commit/tag body)
233 for (var i = 0; lines[i]; i++) {
234 var args = lines[i].split(' ')
235 switch (args[0]) {
236 case 'tree':
237 case 'parent':
238 case 'object':
239 var hash = args[1]
240 if (!(hash in links))
241 links[hash] = true
242 }
243 }
244 return links
245}
246
247/*
248TODO: investigate capabilities
249report-status delete-refs side-band-64k quiet atomic ofs-delta
250*/
251
252// Get a line for each ref that we have. The first line also has capabilities.
253// Wrap with pktLine.encode.
254function receivePackHeader(capabilities, refSource, usePlaceholder) {
255 var first = true
256 var ended
257 return function (abort, cb) {
258 if (ended) return cb(true)
259 refSource(abort, function (end, ref) {
260 ended = end
261 var name = ref && ref.name
262 var value = ref && ref.value
263 if (first && usePlaceholder) {
264 first = false
265 if (end) {
266 // use placeholder data if there are no refs
267 value = '0000000000000000000000000000000000000000'
268 name = 'capabilities^{}'
269 }
270 name += '\0' + capabilities.join(' ')
271 } else if (end) {
272 return cb(true)
273 }
274 cb(null, value + ' ' + name)
275 })
276 }
277}
278
279// receive-pack: push from client
280function receivePack(read, repo, options) {
281 var ended
282 var refSource = repo.refs.bind(repo)
283 var sendRefs = receivePackHeader([
284 'delete-refs',
285 ], refSource, true)
286
287 return pktLine.encode(
288 cat([
289 // send our refs
290 sendRefs,
291 pull.once(''),
292 function (abort, cb) {
293 if (abort) return
294 // receive their refs
295 var lines = pktLine.decode(read, options)
296 pull(
297 lines.updates,
298 pull.collect(function (err, updates) {
299 if (err) return cb(err)
300 repo.update(pull.values(updates), pull(
301 lines.passthrough,
302 pack.decode(onEnd)
303 ), onEnd)
304 })
305 )
306 function onEnd(err) {
307 if (!ended)
308 cb(ended = err)
309 }
310 },
311 pull.once('unpack ok')
312 ])
313 )
314}
315
316function prepend(data, read) {
317 var done
318 return function (end, cb) {
319 if (done) {
320 read(end, cb)
321 } else {
322 done = true
323 cb(null, data)
324 }
325 }
326}
327
328module.exports = function (repo) {
329 var ended
330 var options = {
331 verbosity: 1,
332 progress: false
333 }
334
335 function handleConnect(cmd, read) {
336 var args = util.split2(cmd)
337 switch (args[0]) {
338 case 'git-upload-pack':
339 return prepend('\n', uploadPack(read, repo, options))
340 case 'git-receive-pack':
341 return prepend('\n', receivePack(read, repo, options))
342 default:
343 return pull.error(new Error('Unknown service ' + args[0]))
344 }
345 }
346
347 function handleCommand(line, read) {
348 var args = util.split2(line)
349 switch (args[0]) {
350 case 'capabilities':
351 return capabilitiesSource()
352 case 'list':
353 return listRefs(refSource)
354 case 'connect':
355 return handleConnect(args[1], read)
356 case 'option':
357 return optionSource(args[1], options)
358 default:
359 return pull.error(new Error('Unknown command ' + line))
360 }
361 }
362
363 return function (read) {
364 var b = buffered()
365 b(read)
366 var command
367
368 function getCommand(cb) {
369 b.lines(null, function next(end, line) {
370 if (ended = end)
371 return cb(end)
372
373 if (line == '')
374 return b.lines(null, next)
375
376 if (options.verbosity > 1)
377 console.error('command:', line)
378
379 var cmdSource = handleCommand(line, b.passthrough)
380 cb(null, cmdSource)
381 })
382 }
383
384 return function next(abort, cb) {
385 if (ended) return cb(ended)
386
387 if (!command) {
388 if (abort) return
389 getCommand(function (end, cmd) {
390 command = cmd
391 next(end, cb)
392 })
393 return
394 }
395
396 command(abort, function (err, data) {
397 if (err) {
398 command = null
399 if (err !== true)
400 cb(err, data)
401 else
402 next(abort, cb)
403 } else {
404 cb(null, data)
405 }
406 })
407 }
408 }
409}
410

Built with git-ssb-web