Files: 502850b502fa9b6314bd583f93e5b47bee787c17 / index.js
27297 bytesRaw
1 | var pull = require('pull-stream') |
2 | var paramap = require('pull-paramap') |
3 | var lru = require('hashlru') |
4 | var memo = require('asyncmemo') |
5 | var u = require('./lib/util') |
6 | var packidx = require('pull-git-packidx-parser') |
7 | var Reader = require('pull-reader') |
8 | var toPull = require('stream-to-pull-stream') |
9 | var zlib = require('zlib') |
10 | var looper = require('looper') |
11 | var multicb = require('multicb') |
12 | var kvdiff = require('pull-kvdiff') |
13 | |
14 | var ObjectNotFoundError = u.customError('ObjectNotFoundError') |
15 | |
16 | var emptyBlobHash = 'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391' |
17 | var emptyBlobHashBad = '77e98a59e190ce51ab7ec86deb24492d1434247e' |
18 | |
19 | var PGP_SIGNATURE = '-----BEGIN PGP SIGNATURE-----' |
20 | var gpgSigBuffer = new Buffer('gpgsig ') |
21 | |
22 | function SSBGit(sbot, config) { |
23 | this.sbot = sbot |
24 | this.config = config |
25 | |
26 | this.unboxContent = memo({ |
27 | cache: lru(100) |
28 | }, function (content, cb) { |
29 | sbot.private.unbox(content, cb) |
30 | }) |
31 | |
32 | this.getMsgDecrypted = memo({ |
33 | cache: lru(100) |
34 | }, this.getMsgDecrypted.bind(this)) |
35 | |
36 | this.findObject = memo({ |
37 | cache: lru(5), |
38 | asString: function (opts) { |
39 | return opts.obj + opts.headMsgId |
40 | } |
41 | }, this.findObject.bind(this)) |
42 | |
43 | this.findObjectInMsg = memo({ |
44 | cache: lru(5), |
45 | asString: function (opts) { |
46 | return opts.obj + opts.msg |
47 | } |
48 | }, this.findObjectInMsg.bind(this)) |
49 | |
50 | this.getPackIndex = memo({ |
51 | cache: lru(4), |
52 | asString: JSON.stringify |
53 | }, this.getPackIndex.bind(this)) |
54 | |
55 | this.getObjectAtPath = memo({ |
56 | cache: lru(5), |
57 | asString: function (opts) { |
58 | return opts.msg + opts.obj + opts.path |
59 | } |
60 | }, this.getObjectAtPath.bind(this)) |
61 | } |
62 | |
63 | SSBGit.prototype.unboxMsg = function (msg, cb) { |
64 | var self = this |
65 | var c = msg && msg.value && msg.value.content |
66 | if (typeof c !== 'string') cb(null, msg) |
67 | else self.unboxContent(c, function (err, content) { |
68 | if (err || !content) return cb(null, msg) |
69 | var m = {} |
70 | for (var k in msg) m[k] = msg[k] |
71 | m.value = {} |
72 | for (var k in msg.value) m.value[k] = msg.value[k] |
73 | m.value.content = content |
74 | m.value.private = true |
75 | cb(null, m) |
76 | }) |
77 | } |
78 | |
79 | SSBGit.prototype.ensureHasBlobs = function (links, cb) { |
80 | var self = this |
81 | var done = multicb({pluck: 1}) |
82 | links.filter(Boolean).forEach(function (link) { |
83 | var cb = done() |
84 | self.sbot.blobs.size(link.link, function (err, size) { |
85 | if (err) cb(err) |
86 | else if (size == null) cb(null, link) |
87 | else cb() |
88 | }) |
89 | }) |
90 | done(function (err, missingLinks) { |
91 | if (err) console.trace(err) |
92 | missingLinks = missingLinks.filter(Boolean) |
93 | if (missingLinks.length == 0) return cb() |
94 | return cb({name: 'BlobNotFoundError', links: missingLinks}) |
95 | }) |
96 | } |
97 | |
98 | SSBGit.prototype.getMsg = function (id, cb) { |
99 | if (!id) return cb() |
100 | this.sbot.get(id, function (err, value) { |
101 | if (err) return cb(err) |
102 | cb(null, {key: id, value: value}) |
103 | }) |
104 | } |
105 | |
106 | SSBGit.prototype.getMsgDecrypted = function (key, cb) { |
107 | var self = this |
108 | self.getMsg(key, function (err, msg) { |
109 | if (err) return cb(err) |
110 | self.unboxMsg(msg, cb) |
111 | }) |
112 | } |
113 | |
114 | SSBGit.prototype.readBlob = function (link) { |
115 | link = u.toLink(link) |
116 | return this.sbot.blobs.get({ |
117 | hash: link.link, |
118 | size: link.size, |
119 | }) |
120 | } |
121 | |
122 | SSBGit.prototype.readBlobSlice = function (link, opts) { |
123 | return this.sbot.blobs.getSlice |
124 | ? this.sbot.blobs.getSlice({ |
125 | hash: link.link, |
126 | size: link.size, |
127 | start: opts.start, |
128 | end: opts.end, |
129 | }) |
130 | : pull( |
131 | this.readBlob(link), |
132 | u.pullSlice(opts.start, opts.end) |
133 | ) |
134 | } |
135 | |
136 | // open, read, buffer and callback an object |
137 | SSBGit.prototype.getObject = function (opts, cb) { |
138 | var self = this |
139 | self.openObject(opts, function (err, obj) { |
140 | if (err) return cb(err) |
141 | pull( |
142 | self.readObject(obj), |
143 | u.pullConcat(cb) |
144 | ) |
145 | }) |
146 | } |
147 | |
148 | // get a message that pushed an object |
149 | SSBGit.prototype.getObjectMsg = function (opts, cb) { |
150 | this.findObject(opts, function (err, loc) { |
151 | if (err) return cb(err) |
152 | cb(null, loc.msg) |
153 | }) |
154 | } |
155 | |
156 | SSBGit.prototype.openObject = function (opts, cb) { |
157 | var self = this |
158 | self.findObjectInMsg(opts, function (err, loc) { |
159 | if (err) return cb(err) |
160 | self.ensureHasBlobs([loc.packLink], function (err) { |
161 | if (err) return cb(err) |
162 | cb(null, { |
163 | type: opts.type, |
164 | length: opts.length, |
165 | offset: loc.offset, |
166 | next: loc.next, |
167 | packLink: loc.packLink, |
168 | idx: loc.idx, |
169 | msg: loc.msg, |
170 | hash: opts.obj, |
171 | }) |
172 | }) |
173 | }) |
174 | } |
175 | |
176 | SSBGit.prototype.readObject = function (obj) { |
177 | if (obj.offset === obj.next) return pull.empty() |
178 | return pull( |
179 | this.readBlobSlice(obj.packLink, {start: obj.offset, end: obj.next}), |
180 | this.decodeObject({ |
181 | msg: obj.msg, |
182 | type: obj.type, |
183 | length: obj.length, |
184 | packLink: obj.packLink, |
185 | idx: obj.idx, |
186 | }) |
187 | ) |
188 | } |
189 | |
190 | SSBGit.prototype.statObject = function (obj, cb) { |
191 | if (obj.offset === obj.next) return cb(new Error('empty object')) |
192 | pull( |
193 | this.readBlobSlice(obj.packLink, {start: obj.offset, end: obj.next}), |
194 | this.decodeObject({ |
195 | msg: obj.msg, |
196 | type: obj.type, |
197 | length: obj.length, |
198 | packLink: obj.packLink, |
199 | idx: obj.idx, |
200 | statOnly: true |
201 | }), |
202 | pull.collect(function (err, objs) { |
203 | cb(err, objs && objs[0]) |
204 | }) |
205 | ) |
206 | } |
207 | |
208 | function extractSignature(buf, type) { |
209 | if (type === 'commit') { |
210 | var payloadBufs = [] |
211 | var signatureBufs = [] |
212 | var inSignature = false |
213 | for (var i = 0, next; i < buf.length; i = next) { |
214 | var eol = buf.indexOf('\n', i) |
215 | next = eol === -1 ? buf.length : eol + 1 |
216 | if (buf[i] === 10) { // empty line: end of headers |
217 | payloadBufs.push(buf.slice(i)) |
218 | break |
219 | } |
220 | if (inSignature) { |
221 | if (buf[i] === 0x20) signatureBufs.push(buf.slice(i + 1, next)) |
222 | else { |
223 | payloadBufs.push(buf.slice(i, next)) |
224 | inSignature = false |
225 | } |
226 | } else if (!buf.compare(gpgSigBuffer, 0, 7, i, i + 7)) { |
227 | signatureBufs.push(buf.slice(i + 7, next)) |
228 | inSignature = true |
229 | } else { |
230 | payloadBufs.push(buf.slice(i, next)) |
231 | } |
232 | } |
233 | if (signatureBufs.length > 0) return { |
234 | payload: Buffer.concat(payloadBufs), |
235 | signature: Buffer.concat(signatureBufs) |
236 | } |
237 | } else { |
238 | var i = buf.indexOf(PGP_SIGNATURE) |
239 | if (i !== -1) return { |
240 | payload: buf.slice(0, i), |
241 | signature: buf.slice(i) |
242 | } |
243 | } |
244 | } |
245 | |
246 | SSBGit.prototype.extractSignature = function (obj, cb) { |
247 | var self = this |
248 | if (obj.type) gotType(obj.type) |
249 | else self.statObject(obj, function (err, stat) { |
250 | if (err) return cb(err) |
251 | gotType(stat.type) |
252 | }) |
253 | function gotType(type) { |
254 | pull( |
255 | self.readObject(obj), |
256 | pull.collect(function (err, bufs) { |
257 | if (err) return cb(err) |
258 | var parts = extractSignature(Buffer.concat(bufs), type) |
259 | if (!parts) return cb(new Error('signature not found')) |
260 | cb(null, parts) |
261 | }) |
262 | ) |
263 | } |
264 | } |
265 | |
266 | // find which packfile contains a git object, and where in the packfile it is |
267 | // located |
268 | SSBGit.prototype.findObject = function (opts, cb) { |
269 | if (!opts.headMsgId) return cb(new TypeError('missing head message id')) |
270 | if (!opts.obj) return cb(new TypeError('missing object id')) |
271 | var self = this |
272 | var objId = opts.obj |
273 | self.findObjectMsgs(opts, function (err, msgs) { |
274 | if (err) return cb(err) |
275 | if (msgs.length === 0) |
276 | return cb(new ObjectNotFoundError('unable to find git object ' + objId)) |
277 | self.findObjectInMsgs(objId, msgs, cb) |
278 | }) |
279 | } |
280 | |
281 | SSBGit.prototype.findObjectInMsg = function (opts, cb) { |
282 | if (!opts.msg) return cb(new TypeError('missing message id')) |
283 | if (!opts.obj) return cb(new TypeError('missing object id')) |
284 | var self = this |
285 | self.getMsgDecrypted(opts.msg, function (err, msg) { |
286 | if (err) return cb(err) |
287 | self.findObjectInMsgs(opts.obj, [msg], cb) |
288 | }) |
289 | } |
290 | |
291 | SSBGit.prototype.findObjectInMsgs = function (objId, msgs, cb) { |
292 | var self = this |
293 | var objIdBuf = new Buffer(objId, 'hex') |
294 | // if blobs may need to be fetched, try to ask the user about as many of them |
295 | // at one time as possible |
296 | var packidxs = [].concat.apply([], msgs.map(function (msg) { |
297 | var c = msg.value.content |
298 | var idxs = u.toArray(c.indexes).map(u.toLink) |
299 | return u.toArray(c.packs).map(u.toLink).map(function (pack, i) { |
300 | var idx = idxs[i] |
301 | if (pack && idx) return { |
302 | msg: msg, |
303 | packLink: pack, |
304 | idxLink: idx, |
305 | } |
306 | }) |
307 | })).filter(Boolean) |
308 | var blobLinks = packidxs.length === 1 |
309 | ? [packidxs[0].idxLink, packidxs[0].packLink] |
310 | : packidxs.map(function (packidx) { |
311 | return packidx.idxLink |
312 | }) |
313 | self.ensureHasBlobs(blobLinks, function (err) { |
314 | if (err) return cb(err) |
315 | pull( |
316 | pull.values(packidxs), |
317 | paramap(function (pack, cb) { |
318 | self.getPackIndex(pack.idxLink, function (err, idx) { |
319 | if (err) return cb(err) |
320 | var offset = idx.find(objIdBuf) |
321 | if (!offset) return cb() |
322 | cb(null, { |
323 | offset: offset.offset, |
324 | next: offset.next, |
325 | packLink: pack.packLink, |
326 | idx: idx, |
327 | msg: pack.msg, |
328 | }) |
329 | }) |
330 | }, 4), |
331 | pull.filter(), |
332 | pull.take(1), |
333 | pull.collect(function (err, offsets) { |
334 | if (err) return cb(err) |
335 | if (offsets.length === 0) |
336 | return cb(new ObjectNotFoundError('unable to find git object ' |
337 | + objId + ' in ' + msgs.length + ' messages')) |
338 | cb(null, offsets[0]) |
339 | }) |
340 | ) |
341 | }) |
342 | } |
343 | |
344 | // given an object id and ssb msg id, get a set of messages of which at least one pushed the object. |
345 | SSBGit.prototype.findObjectMsgs = function (opts, cb) { |
346 | var self = this |
347 | var id = opts.obj |
348 | var headMsgId = opts.headMsgId |
349 | var ended = false |
350 | var waiting = 0 |
351 | var maybeMsgs = [] |
352 | |
353 | function cbOnce(err, msgs) { |
354 | if (ended) return |
355 | ended = true |
356 | cb(err, msgs) |
357 | } |
358 | |
359 | function objectMatches(commit) { |
360 | if (commit === emptyBlobHashBad) commit = emptyBlobHash |
361 | return commit && (commit === id || commit.sha1 === id) |
362 | } |
363 | |
364 | if (!headMsgId) return cb(new TypeError('missing head message id')) |
365 | if (!u.isRef(headMsgId)) |
366 | return cb(new TypeError('bad head message id \'' + headMsgId + '\'')) |
367 | |
368 | var gitId = id |
369 | ;(function getMsg(id) { |
370 | waiting++ |
371 | self.getMsgDecrypted(id, function (err, msg) { |
372 | waiting-- |
373 | if (ended) return |
374 | if (err && err.name == 'NotFoundError') |
375 | return cbOnce(new Error('missing message ' + headMsgId)) |
376 | if (err) return cbOnce(err) |
377 | var c = msg.value.content |
378 | if (typeof c === 'string') |
379 | return cbOnce(new Error('unable to decrypt message ' + msg.key)) |
380 | if ((u.toArray(c.object_ids).some(objectMatches)) |
381 | || (u.toArray(c.tags).some(objectMatches)) |
382 | || (u.toArray(c.commits).some(objectMatches))) { |
383 | // found the object |
384 | return cbOnce(null, [msg]) |
385 | } else if (!c.object_ids) { |
386 | // the object might be here |
387 | maybeMsgs.push(msg) |
388 | } |
389 | // traverse the DAG to keep looking for the object |
390 | u.toArray(c.repoBranch).concat(u.toArray(c.refsBranch)) |
391 | .filter(u.isRef).forEach(getMsg) |
392 | if (waiting === 0) { |
393 | cbOnce(null, maybeMsgs) |
394 | } |
395 | }) |
396 | })(headMsgId) |
397 | } |
398 | |
399 | SSBGit.prototype.getPackIndex = function (idxBlobLink, cb) { |
400 | pull(this.readBlob(idxBlobLink), packidx(cb)) |
401 | } |
402 | |
403 | var objectTypes = [ |
404 | 'none', 'commit', 'tree', 'blob', |
405 | 'tag', 'unused', 'ofs-delta', 'ref-delta' |
406 | ] |
407 | |
408 | function readTypedVarInt(reader, cb) { |
409 | var type, value, shift |
410 | reader.read(1, function (end, buf) { |
411 | if (ended = end) return cb(end) |
412 | var firstByte = buf[0] |
413 | type = objectTypes[(firstByte >> 4) & 7] |
414 | value = firstByte & 15 |
415 | shift = 4 |
416 | checkByte(firstByte) |
417 | }) |
418 | |
419 | function checkByte(byte) { |
420 | if (byte & 0x80) |
421 | reader.read(1, gotByte) |
422 | else |
423 | cb(null, type, value) |
424 | } |
425 | |
426 | function gotByte(end, buf) { |
427 | if (ended = end) return cb(end) |
428 | var byte = buf[0] |
429 | value += (byte & 0x7f) << shift |
430 | shift += 7 |
431 | checkByte(byte) |
432 | } |
433 | } |
434 | |
435 | function readVarInt(reader, cb) { |
436 | var value = 0, shift = 0 |
437 | reader.read(1, function gotByte(end, buf) { |
438 | if (ended = end) return cb(end) |
439 | var byte = buf[0] |
440 | value += (byte & 0x7f) << shift |
441 | shift += 7 |
442 | if (byte & 0x80) |
443 | reader.read(1, gotByte) |
444 | else |
445 | cb(null, value) |
446 | }) |
447 | } |
448 | |
449 | function inflate(read) { |
450 | return toPull(zlib.createInflate())(read) |
451 | } |
452 | |
453 | SSBGit.prototype.decodeObject = function (opts) { |
454 | var self = this |
455 | var packLink = opts.packLink |
456 | return function (read) { |
457 | var reader = Reader() |
458 | reader(read) |
459 | return u.readNext(function (cb) { |
460 | readTypedVarInt(reader, function (end, type, length) { |
461 | if (end === true) cb(new Error('Missing object type')) |
462 | else if (end) cb(end) |
463 | else if (type === 'ref-delta') getObjectFromRefDelta(length, cb) |
464 | else if (opts.type && type !== opts.type) |
465 | cb(new Error('expected type \'' + opts.type + '\' ' + |
466 | 'but found \'' + type + '\'')) |
467 | else if (opts.length && length !== opts.length) |
468 | cb(new Error('expected length ' + opts.length + ' ' + |
469 | 'but found ' + length)) |
470 | else if (opts.statOnly) |
471 | reader.abort(null, function () { |
472 | cb(null, pull.once({type: type, length: length})) |
473 | }) |
474 | else cb(null, inflate(reader.read())) |
475 | }) |
476 | }) |
477 | |
478 | function getObjectFromRefDelta(length, cb) { |
479 | reader.read(20, function (end, sourceHash) { |
480 | if (end) return cb(end) |
481 | var inflatedReader = Reader() |
482 | pull(reader.read(), inflate, inflatedReader) |
483 | readVarInt(inflatedReader, function (err, expectedSourceLength) { |
484 | if (err) return cb(err) |
485 | readVarInt(inflatedReader, function (err, expectedTargetLength) { |
486 | if (err) return cb(err) |
487 | var offset = opts.idx.find(sourceHash) |
488 | if (!offset) return cb(null, 'missing source object ' + |
489 | sourcehash.toString('hex')) |
490 | var readSource = pull( |
491 | self.readBlobSlice(opts.packLink, { |
492 | start: offset.offset, |
493 | end: offset.next |
494 | }), |
495 | self.decodeObject({ |
496 | msg: opts.msg, |
497 | type: opts.type, |
498 | length: expectedSourceLength, |
499 | packLink: opts.packLink, |
500 | idx: opts.idx |
501 | }) |
502 | ) |
503 | cb(null, patchObject(inflatedReader, length, readSource, expectedTargetLength)) |
504 | }) |
505 | }) |
506 | }) |
507 | } |
508 | } |
509 | } |
510 | |
511 | function readOffsetSize(cmd, reader, readCb) { |
512 | var offset = 0, size = 0 |
513 | |
514 | function addByte(bit, outPos, cb) { |
515 | if (cmd & (1 << bit)) |
516 | reader.read(1, function (err, buf) { |
517 | if (err) readCb(err) |
518 | else cb(buf[0] << (outPos << 3)) |
519 | }) |
520 | else |
521 | cb(0) |
522 | } |
523 | |
524 | addByte(0, 0, function (val) { |
525 | offset = val |
526 | addByte(1, 1, function (val) { |
527 | offset |= val |
528 | addByte(2, 2, function (val) { |
529 | offset |= val |
530 | addByte(3, 3, function (val) { |
531 | offset |= val |
532 | addSize() |
533 | }) |
534 | }) |
535 | }) |
536 | }) |
537 | function addSize() { |
538 | addByte(4, 0, function (val) { |
539 | size = val |
540 | addByte(5, 1, function (val) { |
541 | size |= val |
542 | addByte(6, 2, function (val) { |
543 | size |= val |
544 | readCb(null, offset, size || 0x10000) |
545 | }) |
546 | }) |
547 | }) |
548 | } |
549 | } |
550 | |
551 | function patchObject(deltaReader, deltaLength, readSource, targetLength) { |
552 | var srcBuf |
553 | var ended |
554 | |
555 | return u.readNext(function (cb) { |
556 | pull(readSource, u.pullConcat(function (err, buf) { |
557 | if (err) return cb(err) |
558 | srcBuf = buf |
559 | cb(null, read) |
560 | })) |
561 | }) |
562 | |
563 | function read(abort, cb) { |
564 | if (ended) return cb(ended) |
565 | deltaReader.read(1, function (end, dBuf) { |
566 | if (ended = end) return cb(end) |
567 | var cmd = dBuf[0] |
568 | if (cmd & 0x80) |
569 | // skip a variable amount and then pass through a variable amount |
570 | readOffsetSize(cmd, deltaReader, function (err, offset, size) { |
571 | if (err) return earlyEnd(err) |
572 | var buf = srcBuf.slice(offset, offset + size) |
573 | cb(end, buf) |
574 | }) |
575 | else if (cmd) |
576 | // insert `cmd` bytes from delta |
577 | deltaReader.read(cmd, cb) |
578 | else |
579 | cb(new Error("unexpected delta opcode 0")) |
580 | }) |
581 | |
582 | function earlyEnd(err) { |
583 | cb(err === true ? new Error('stream ended early') : err) |
584 | } |
585 | } |
586 | } |
587 | |
588 | var gitNameRegex = /^(.*) <(([^>@]*)(@[^>]*)?)> (.*) (.*)$/ |
589 | function parseName(line) { |
590 | var m = gitNameRegex.exec(line) |
591 | if (!m) return null |
592 | return { |
593 | name: m[1], |
594 | email: m[2], |
595 | localpart: m[3], |
596 | feed: u.isRef(m[4]) && m[4] || undefined, |
597 | date: new Date(m[5] * 1000), |
598 | tz: m[6], |
599 | } |
600 | } |
601 | |
602 | SSBGit.prototype.getCommit = function (obj, cb) { |
603 | pull(this.readObject(obj), u.pullConcat(function (err, buf) { |
604 | if (err) return cb(err) |
605 | var commit = { |
606 | msg: obj.msg, |
607 | parents: [], |
608 | } |
609 | var authorLine, committerLine |
610 | var lines = buf.toString('utf8').split('\n') |
611 | for (var line; (line = lines.shift()); ) { |
612 | var parts = line.split(' ') |
613 | var prop = parts.shift() |
614 | var value = parts.join(' ') |
615 | switch (prop) { |
616 | case 'tree': |
617 | commit.tree = value |
618 | break |
619 | case 'parent': |
620 | commit.parents.push(value) |
621 | break |
622 | case 'author': |
623 | authorLine = value |
624 | break |
625 | case 'committer': |
626 | committerLine = value |
627 | break |
628 | case 'gpgsig': |
629 | var sigLines = [value] |
630 | while (lines[0] && lines[0][0] == ' ') |
631 | sigLines.push(lines.shift().slice(1)) |
632 | commit.gpgsig = sigLines.join('\n') |
633 | for (var i = 0; i < sigLines.length; i++) { |
634 | if (/Version: /.test(sigLines[i])) commit.signatureVersion = sigLines[i].substr(9) |
635 | } |
636 | break |
637 | default: |
638 | return cb(new TypeError('unknown git object property ' + prop)) |
639 | } |
640 | } |
641 | commit.committer = parseName(committerLine) |
642 | if (authorLine !== committerLine) commit.author = parseName(authorLine) |
643 | commit.body = lines.join('\n') |
644 | cb(null, commit) |
645 | })) |
646 | } |
647 | |
648 | SSBGit.prototype.getTag = function (obj, cb) { |
649 | pull(this.readObject(obj), u.pullConcat(function (err, buf) { |
650 | if (err) return cb(err) |
651 | var tag = { |
652 | msg: obj.msg, |
653 | } |
654 | var authorLine, tagterLine |
655 | var lines = buf.toString('utf8').split('\n') |
656 | var i = lines.lastIndexOf(PGP_SIGNATURE) |
657 | if (i !== -1) { |
658 | for (var j = i + 1; lines[j]; j++) { |
659 | if (/Version: /.test(lines[j])) tag.signatureVersion = lines[j].substr(9) |
660 | } |
661 | tag.gpgsig = lines.splice(i).join('\n') |
662 | } |
663 | for (var line; (line = lines.shift()); ) { |
664 | var parts = line.split(' ') |
665 | var prop = parts.shift() |
666 | var value = parts.join(' ') |
667 | switch (prop) { |
668 | case 'object': |
669 | tag.object = value |
670 | break |
671 | case 'type': |
672 | if (value !== 'blob' && value !== 'commit' && value !== 'tree') |
673 | return cb(new TypeError('unknown git object type ' + type)) |
674 | tag.type = value |
675 | break |
676 | case 'tag': |
677 | tag.tag = value |
678 | break |
679 | case 'tagger': |
680 | tag.tagger = parseName(value) |
681 | break |
682 | default: |
683 | return cb(new TypeError('unknown git object property ' + prop)) |
684 | } |
685 | } |
686 | tag.body = lines.join('\n') |
687 | cb(null, tag) |
688 | })) |
689 | } |
690 | |
691 | function readCString(reader, cb) { |
692 | var chars = [] |
693 | var loop = looper(function () { |
694 | reader.read(1, next) |
695 | }) |
696 | function next(err, ch) { |
697 | if (err) return cb(err) |
698 | if (ch[0] === 0) return cb(null, Buffer.concat(chars).toString('utf8')) |
699 | chars.push(ch) |
700 | loop() |
701 | } |
702 | loop() |
703 | } |
704 | |
705 | SSBGit.prototype.readTree = function (obj) { |
706 | var self = this |
707 | var reader = Reader() |
708 | reader(this.readObject(obj)) |
709 | return function (abort, cb) { |
710 | if (abort) return reader.abort(abort, cb) |
711 | readCString(reader, function (err, str) { |
712 | if (err) return cb(err) |
713 | var parts = str.split(' ') |
714 | var mode = parseInt(parts[0], 8) |
715 | var name = parts.slice(1).join(' ') |
716 | reader.read(20, function (err, hash) { |
717 | if (err) return cb(err) |
718 | cb(null, { |
719 | name: name, |
720 | mode: mode, |
721 | hash: hash.toString('hex'), |
722 | type: mode === 0040000 ? 'tree' : |
723 | mode === 0160000 ? 'commit' : 'blob', |
724 | }) |
725 | }) |
726 | }) |
727 | } |
728 | } |
729 | |
730 | SSBGit.prototype.readTreeFull = function (obj) { |
731 | var self = this |
732 | return pull( |
733 | self.readTree(obj), |
734 | paramap(function (file, cb) { |
735 | self.getObjectMsg({ |
736 | obj: file.hash, |
737 | headMsgId: obj.msg.key, |
738 | }, function (err, msg) { |
739 | if (err) return cb(err) |
740 | file.msg = msg |
741 | cb(null, file) |
742 | }) |
743 | }, 8) |
744 | ) |
745 | } |
746 | |
747 | SSBGit.prototype.readCommitChanges = function (commit) { |
748 | var self = this |
749 | return u.readNext(function (cb) { |
750 | var done = multicb({pluck: 1}) |
751 | commit.parents.forEach(function (rev) { |
752 | var cb = done() |
753 | self.getObjectMsg({ |
754 | obj: rev, |
755 | headMsgId: commit.msg.key, |
756 | type: 'commit', |
757 | }, function (err, msg) { |
758 | if (err) return cb(err) |
759 | self.openObject({ |
760 | obj: rev, |
761 | msg: msg.key, |
762 | }, function (err, obj) { |
763 | if (err) return cb(err) |
764 | self.getCommit(obj, cb) |
765 | }) |
766 | }) |
767 | }) |
768 | done()(null, commit) |
769 | done(function (err, commits) { |
770 | if (err) return cb(err) |
771 | var done = multicb({pluck: 1}) |
772 | commits.forEach(function (commit) { |
773 | var cb = done() |
774 | if (!commit.tree) return cb(null, pull.empty()) |
775 | self.getObjectMsg({ |
776 | obj: commit.tree, |
777 | headMsgId: commit.msg.key, |
778 | type: 'tree', |
779 | }, function (err, msg) { |
780 | if (err) return cb(err) |
781 | self.openObject({ |
782 | obj: commit.tree, |
783 | msg: commit.msg.key, |
784 | }, cb) |
785 | }) |
786 | }) |
787 | done(function (err, trees) { |
788 | if (err) return cb(err) |
789 | cb(null, self.diffTreesRecursive(trees)) |
790 | }) |
791 | }) |
792 | }) |
793 | } |
794 | |
795 | SSBGit.prototype.diffTrees = function (objs) { |
796 | var self = this |
797 | return pull( |
798 | kvdiff(objs.map(function (obj) { |
799 | return self.readTree(obj) |
800 | }), 'name'), |
801 | pull.map(function (item) { |
802 | var diff = item.diff || {} |
803 | var head = item.values[item.values.length-1] |
804 | var created = true |
805 | for (var k = 0; k < item.values.length-1; k++) |
806 | if (item.values[k]) created = false |
807 | return { |
808 | name: item.key, |
809 | hash: item.values.map(function (val) { return val.hash }), |
810 | mode: diff.mode, |
811 | type: item.values.map(function (val) { return val.type }), |
812 | deleted: !head, |
813 | created: created |
814 | } |
815 | }) |
816 | ) |
817 | } |
818 | |
819 | SSBGit.prototype.diffTreesRecursive = function (objs) { |
820 | var self = this |
821 | return pull( |
822 | self.diffTrees(objs), |
823 | paramap(function (item, cb) { |
824 | if (!item.type.some(function (t) { return t === 'tree' })) |
825 | return cb(null, [item]) |
826 | var done = multicb({pluck: 1}) |
827 | item.type.forEach(function (type, i) { |
828 | var cb = done() |
829 | if (type !== 'tree') return cb(null, pull.once(item)) |
830 | var hash = item.hash[i] |
831 | self.getObjectMsg({ |
832 | obj: hash, |
833 | headMsgId: objs[i].msg.key, |
834 | }, function (err, msg) { |
835 | if (err) return cb(err) |
836 | self.openObject({ |
837 | obj: hash, |
838 | msg: msg.key, |
839 | }, cb) |
840 | }) |
841 | }) |
842 | done(function (err, objs) { |
843 | if (err) return cb(err) |
844 | var newTree = objs[objs.length-1] |
845 | cb(null, pull( |
846 | self.diffTreesRecursive(objs), |
847 | paramap(function (file, cb) { |
848 | file.name = item.name + '/' + file.name |
849 | var newHash = file.hash[1] || file.hash[0] |
850 | if (!newHash) return cb(null, file) |
851 | self.getObjectMsg({ |
852 | obj: newHash, |
853 | headMsgId: newTree.msg.key, |
854 | }, function (err, msg) { |
855 | if (err) return cb(err) |
856 | file.msg = msg |
857 | cb(null, file) |
858 | }) |
859 | }, 4) |
860 | )) |
861 | }) |
862 | }, 4), |
863 | pull.flatten() |
864 | ) |
865 | } |
866 | |
867 | SSBGit.prototype.diffFile = function (opts, cb) { |
868 | var done = multicb({pluck: 1, spread: true}) |
869 | var self = this |
870 | self.getObjectAtPath({ |
871 | msg: opts.msg, |
872 | obj: opts.commit, |
873 | path: opts.path, |
874 | }, done()) |
875 | self.openObject({ |
876 | obj: opts.commit, |
877 | msg: opts.msg, |
878 | }, done()) |
879 | done(function (err, blobObj, commitObj) { |
880 | if (err) return cb(err) |
881 | self.getCommit(commitObj, function (err, commit) { |
882 | if (err) return cb(err) |
883 | // TODO: handle commits with multiple parents |
884 | var parentCommitId = commit.parents[0] |
885 | if (!parentCommitId) { |
886 | return cb(null, { |
887 | created: true, |
888 | hash: [null, blobObj.hash] |
889 | }) |
890 | } |
891 | self.getObjectMsg({ |
892 | obj: parentCommitId, |
893 | headMsgId: opts.msg, |
894 | type: 'commit', |
895 | }, function (err, parentMsg) { |
896 | if (err) return cb(err) |
897 | self.getObjectAtPath({ |
898 | msg: parentMsg.key, |
899 | obj: parentCommitId, |
900 | path: opts.path, |
901 | }, function (err, parentBlobObj) { |
902 | if (err) return cb(err) |
903 | if (!parentBlobObj) { |
904 | return cb(null, { |
905 | created: true, |
906 | hash: [null, blobObj.hash] |
907 | }) |
908 | } |
909 | return cb(null, { |
910 | hash: [parentBlobObj.hash, blobObj.hash] |
911 | }) |
912 | }) |
913 | }) |
914 | }) |
915 | }) |
916 | } |
917 | |
918 | SSBGit.prototype.getObjectAtPath = function (opts, cb) { |
919 | var self = this |
920 | // open the commit |
921 | self.openObject({ |
922 | obj: opts.obj, |
923 | msg: opts.msg, |
924 | }, function (err, obj) { |
925 | if (err) return cb(err) |
926 | // TODO: handle tag pointing to commit |
927 | self.getCommit(obj, function (err, commit) { |
928 | if (err) return cb(err) |
929 | if (!commit.tree) return cb(new Error('missing tree')) |
930 | self.getObjectMsg({ |
931 | obj: commit.tree, |
932 | headMsgId: commit.msg.key, |
933 | type: 'tree', |
934 | }, function (err, msg) { |
935 | if (err) return cb(err) |
936 | self.openObject({ |
937 | obj: commit.tree, |
938 | msg: msg.key, |
939 | }, function (err, tree) { |
940 | if (err) return cb(err) |
941 | self.getObjectAtPathFromTree({ |
942 | obj: tree, |
943 | msg: commit.msg.key, |
944 | parts: opts.path.split(/\/+/), |
945 | }, cb) |
946 | }) |
947 | }) |
948 | }) |
949 | }) |
950 | } |
951 | |
952 | SSBGit.prototype.getObjectAtPathFromTree = function (opts, cb) { |
953 | var name = opts.parts[0] |
954 | var self = this |
955 | pull( |
956 | self.readTree(opts.obj), |
957 | pull.filter(function (item) { |
958 | return item.name === name |
959 | }), |
960 | pull.take(1), |
961 | pull.collect(function (err, items) { |
962 | if (err) return cb(err) |
963 | var item = items[0] |
964 | if (!item) return cb() |
965 | self.getObjectMsg({ |
966 | obj: item.hash, |
967 | headMsgId: opts.obj.msg.key, |
968 | }, function (err, msg) { |
969 | if (err) return cb(err) |
970 | self.openObject({ |
971 | obj: item.hash, |
972 | msg: msg.key, |
973 | }, function (err, obj) { |
974 | if (err) return cb(err) |
975 | if (item.type === 'tree') { |
976 | return self.getObjectAtPathFromTree({ |
977 | obj: obj, |
978 | msg: msg.key, |
979 | parts: opts.parts.slice(1), |
980 | }, cb) |
981 | } |
982 | return cb(null, { |
983 | hash: item.hash, |
984 | obj: obj, |
985 | msg: msg, |
986 | }) |
987 | }) |
988 | }) |
989 | }) |
990 | ) |
991 | } |
992 | |
993 | module.exports = SSBGit |
994 | |
995 | SSBGit.merge = require('./lib/merge') |
996 | SSBGit.linearize = require('./lib/linearize') |
997 |
Built with git-ssb-web