git ssb

2+

cel / ssb-git



Tree: 502850b502fa9b6314bd583f93e5b47bee787c17

Files: 502850b502fa9b6314bd583f93e5b47bee787c17 / index.js

27297 bytesRaw
1var pull = require('pull-stream')
2var paramap = require('pull-paramap')
3var lru = require('hashlru')
4var memo = require('asyncmemo')
5var u = require('./lib/util')
6var packidx = require('pull-git-packidx-parser')
7var Reader = require('pull-reader')
8var toPull = require('stream-to-pull-stream')
9var zlib = require('zlib')
10var looper = require('looper')
11var multicb = require('multicb')
12var kvdiff = require('pull-kvdiff')
13
14var ObjectNotFoundError = u.customError('ObjectNotFoundError')
15
16var emptyBlobHash = 'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391'
17var emptyBlobHashBad = '77e98a59e190ce51ab7ec86deb24492d1434247e'
18
19var PGP_SIGNATURE = '-----BEGIN PGP SIGNATURE-----'
20var gpgSigBuffer = new Buffer('gpgsig ')
21
22function 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
63SSBGit.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
79SSBGit.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
98SSBGit.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
106SSBGit.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
114SSBGit.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
122SSBGit.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
137SSBGit.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
149SSBGit.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
156SSBGit.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
176SSBGit.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
190SSBGit.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
208function 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
246SSBGit.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
268SSBGit.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
281SSBGit.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
291SSBGit.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.
345SSBGit.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
399SSBGit.prototype.getPackIndex = function (idxBlobLink, cb) {
400 pull(this.readBlob(idxBlobLink), packidx(cb))
401}
402
403var objectTypes = [
404 'none', 'commit', 'tree', 'blob',
405 'tag', 'unused', 'ofs-delta', 'ref-delta'
406]
407
408function 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
435function 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
449function inflate(read) {
450 return toPull(zlib.createInflate())(read)
451}
452
453SSBGit.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
511function 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
551function 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
588var gitNameRegex = /^(.*) <(([^>@]*)(@[^>]*)?)> (.*) (.*)$/
589function 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
602SSBGit.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
648SSBGit.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
691function 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
705SSBGit.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
730SSBGit.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
747SSBGit.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
795SSBGit.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
819SSBGit.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
867SSBGit.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
918SSBGit.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
952SSBGit.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
993module.exports = SSBGit
994
995SSBGit.merge = require('./lib/merge')
996SSBGit.linearize = require('./lib/linearize')
997

Built with git-ssb-web