git ssb

0+

cel / pull-git-remote-helper



Tree: 6198d3e1ad4b68fee408f6eade0a3ec353c06a25

Files: 6198d3e1ad4b68fee408f6eade0a3ec353c06a25 / index.js

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

Built with git-ssb-web