diff options
author | cel <cel@f/6sQ6d2CMxRUhLpspgGIulDxDCwYD7DzFzPNr7u5AU=.ed25519> | 2017-05-28 16:09:34 -1000 |
---|---|---|
committer | cel <cel@f/6sQ6d2CMxRUhLpspgGIulDxDCwYD7DzFzPNr7u5AU=.ed25519> | 2017-05-28 18:30:28 -1000 |
commit | 0de03de70ddf0612355e332e7a8ed860555d6b36 (patch) | |
tree | 6a35e44656e310bdfe78d8dc46fd2bda36af1d0e | |
parent | d10fc1c7fbc410ec0c4773902251255582e33adc (diff) | |
download | patchfoo-0de03de70ddf0612355e332e7a8ed860555d6b36.tar.gz patchfoo-0de03de70ddf0612355e332e7a8ed860555d6b36.zip |
refactor git stuff
- parse commits
- link to git objects with specific message id
- open object before reading it
- cleanup debug things
-rw-r--r-- | lib/git.js | 262 | ||||
-rw-r--r-- | lib/render-msg.js | 4 | ||||
-rw-r--r-- | lib/serve.js | 182 | ||||
-rw-r--r-- | lib/util.js | 6 |
4 files changed, 324 insertions, 130 deletions
@@ -8,121 +8,166 @@ var Reader = require('pull-reader') var toPull = require('stream-to-pull-stream') var zlib = require('zlib') +var ObjectNotFoundError = u.customError('ObjectNotFoundError') + module.exports = Git function Git(app) { this.app = app + this.findObject = memo({ - cache: false, + cache: lru(5), asString: function (opts) { - return opts.id + opts.headMsgId + return opts.obj + opts.headMsgId } }, this._findObject.bind(this)) + + this.findObjectInMsg = memo({ + cache: lru(5), + asString: function (opts) { + return opts.obj + opts.msg + } + }, this._findObjectInMsg.bind(this)) + + this.getPackIndex = memo({ + cache: lru(4), + asString: JSON.stringify + }, this._getPackIndex.bind(this)) } +// open, read, buffer and callback an object Git.prototype.getObject = function (opts, cb) { - pull( - this.readObject(opts), - u.pullConcat(cb) - ) + var self = this + self.openObject(opts, function (err, obj) { + if (err) return cb(err) + pull( + self.readObject(obj), + u.pullConcat(cb) + ) + }) } // get a message that pushed an object -Git.prototype.getObjectMsg = function (opts) { +Git.prototype.getObjectMsg = function (opts, cb) { this.findObject(opts, function (err, loc) { if (err) return cb(err) cb(null, loc.msg) }) } -Git.prototype.readObject = function (opts) { +Git.prototype.openObject = function (opts, cb) { var self = this - return u.readNext(function (cb) { - self.findObject(opts, function (err, loc) { + self.findObjectInMsg(opts, function (err, loc) { + if (err) return cb(err) + self.app.ensureHasBlobs([loc.packLink], function (err) { if (err) return cb(err) - self.app.ensureHasBlobs([loc.packLink], function (err) { - if (err) return cb(err) - cb(null, pull( - self.app.readBlob(loc.packLink, {start: loc.offset, end: loc.next}), - self.decodeObject({ - type: opts.type, - length: opts.length, - packLink: loc.packLink, - idx: loc.idx, - }) - )) + cb(null, { + type: opts.type, + length: opts.length, + offset: loc.offset, + next: loc.next, + packLink: loc.packLink, + idx: loc.idx, + msg: loc.msg, }) }) }) } +Git.prototype.readObject = function (obj) { + return pull( + this.app.readBlob(obj.packLink, {start: obj.offset, end: obj.next}), + this.decodeObject({ + type: obj.type, + length: obj.length, + packLink: obj.packLink, + idx: obj.idx, + }) + ) +} + // find which packfile contains a git object, and where in the packfile it is // located Git.prototype._findObject = function (opts, cb) { + if (!opts.headMsgId) return cb(new TypeError('missing head message id')) + if (!opts.obj) return cb(new TypeError('missing object id')) var self = this - var objId = opts.id - var objIdBuf = new Buffer(objId, 'hex') + var objId = opts.obj self.findObjectMsgs(opts, function (err, msgs) { if (err) return cb(err) if (msgs.length === 0) - return cb(new Error('unable to find git object ' + objId)) - // if blobs may need to be fetched, try to ask the user about as many of them - // at one time as possible - var packidxs = [].concat.apply([], msgs.map(function (msg) { - var c = msg.value.content - var idxs = u.toArray(c.indexes).map(u.toLink) - return u.toArray(c.packs).map(u.toLink).map(function (pack, i) { - var idx = idxs[i] - if (pack && idx) return { - msg: msg, - packLink: pack, - idxLink: idx, - } - }) - })).filter(Boolean) - var blobLinks = packidxs.length === 1 - ? [packidxs[0].idxLink, packidxs[0].packLink] - : packidxs.map(function (packidx) { - return packidx.idxLink - }) - self.app.ensureHasBlobs(blobLinks, function (err) { - if (err) return cb(err) - pull( - pull.values(packidxs), - paramap(function (pack, cb) { - console.error('get idx', pack.idxLink) - self.getPackIndex(pack.idxLink, function (err, idx) { - if (err) return cb(err) - var offset = idx.find(objIdBuf) - // console.error('got idx', err, pack.idxId, offset) - if (!offset) return cb() - cb(null, { - offset: offset.offset, - next: offset.next, - packLink: pack.packLink, - idx: idx, - msg: pack.msg, - }) - }) - }, 4), - pull.filter(), - pull.take(1), - pull.collect(function (err, offsets) { + return cb(new ObjectNotFoundError('unable to find git object ' + objId)) + self.findObjectInMsgs(objId, msgs, cb) + }) +} + +Git.prototype._findObjectInMsg = function (opts, cb) { + if (!opts.msg) return cb(new TypeError('missing message id')) + if (!opts.obj) return cb(new TypeError('missing object id')) + var self = this + self.app.getMsgDecrypted(opts.msg, function (err, msg) { + if (err) return cb(err) + self.findObjectInMsgs(opts.obj, [msg], cb) + }) +} + +Git.prototype.findObjectInMsgs = function (objId, msgs, cb) { + var self = this + var objIdBuf = new Buffer(objId, 'hex') + // if blobs may need to be fetched, try to ask the user about as many of them + // at one time as possible + var packidxs = [].concat.apply([], msgs.map(function (msg) { + var c = msg.value.content + var idxs = u.toArray(c.indexes).map(u.toLink) + return u.toArray(c.packs).map(u.toLink).map(function (pack, i) { + var idx = idxs[i] + if (pack && idx) return { + msg: msg, + packLink: pack, + idxLink: idx, + } + }) + })).filter(Boolean) + var blobLinks = packidxs.length === 1 + ? [packidxs[0].idxLink, packidxs[0].packLink] + : packidxs.map(function (packidx) { + return packidx.idxLink + }) + self.app.ensureHasBlobs(blobLinks, function (err) { + if (err) return cb(err) + pull( + pull.values(packidxs), + paramap(function (pack, cb) { + self.getPackIndex(pack.idxLink, function (err, idx) { if (err) return cb(err) - if (offsets.length === 0) - return cb(new Error('unable to find git object ' + objId + - ' in ' + msgs.length + ' messages')) - cb(null, offsets[0]) + var offset = idx.find(objIdBuf) + if (!offset) return cb() + cb(null, { + offset: offset.offset, + next: offset.next, + packLink: pack.packLink, + idx: idx, + msg: pack.msg, + }) }) - ) - }) + }, 4), + pull.filter(), + pull.take(1), + pull.collect(function (err, offsets) { + if (err) return cb(err) + if (offsets.length === 0) + return cb(new ObjectNotFoundError('unable to find git object ' + + objId + ' in ' + msgs.length + ' messages')) + cb(null, offsets[0]) + }) + ) }) } // given an object id and ssb msg id, get a set of messages of which at least one pushed the object. Git.prototype.findObjectMsgs = function (opts, cb) { var self = this - var id = opts.id + var id = opts.obj var headMsgId = opts.headMsgId var ended = false var waiting = 0 @@ -144,7 +189,6 @@ Git.prototype.findObjectMsgs = function (opts, cb) { ;(function getMsg(id) { waiting++ - console.error('get msg', id) self.app.getMsgDecrypted(id, function (err, msg) { waiting-- if (ended) return @@ -159,7 +203,6 @@ Git.prototype.findObjectMsgs = function (opts, cb) { || (u.toArray(c.commits).some(objectMatches))) { // found the object return cbOnce(null, [msg]) - // console.error('found', msg.key) } else if (!c.object_ids) { // the object might be here maybeMsgs.push(msg) @@ -167,14 +210,13 @@ Git.prototype.findObjectMsgs = function (opts, cb) { // traverse the DAG to keep looking for the object u.toArray(c.repoBranch).filter(u.isRef).forEach(getMsg) if (waiting === 0) { - // console.error('trying messages', maybeMsgs.map(function (msg) { return msg.key})) cbOnce(null, maybeMsgs) } }) })(headMsgId) } -Git.prototype.getPackIndex = function (idxBlobLink, cb) { +Git.prototype._getPackIndex = function (idxBlobLink, cb) { pull(this.app.readBlob(idxBlobLink), packidx(cb)) } @@ -250,7 +292,6 @@ Git.prototype.decodeObject = function (opts) { }) function getObjectFromRefDelta(length, cb) { - console.error('read from ref delta') reader.read(20, function (end, sourceHash) { if (end) return cb(end) var inflatedReader = Reader() @@ -259,11 +300,9 @@ Git.prototype.decodeObject = function (opts) { if (err) return cb(err) readVarInt(inflatedReader, function (err, expectedTargetLength) { if (err) return cb(err) - // console.error('getting object', sourceHash) var offset = opts.idx.find(sourceHash) if (!offset) return cb(null, 'missing source object ' + sourcehash.toString('hex')) - console.error('get pack', opts.packLink, offset.offset, offset.next) var readSource = pull( self.app.readBlob(opts.packLink, { start: offset.offset, @@ -276,7 +315,6 @@ Git.prototype.decodeObject = function (opts) { idx: opts.idx }) ) - // console.error('patching', length, expectedTargetLength) cb(null, patchObject(inflatedReader, length, readSource, expectedTargetLength)) }) }) @@ -328,7 +366,6 @@ function readOffsetSize(cmd, reader, readCb) { function patchObject(deltaReader, deltaLength, readSource, targetLength) { var srcBuf var ended - // console.error('patching', deltaLength, targetLength) return u.readNext(function (cb) { pull(readSource, u.pullConcat(function (err, buf) { @@ -339,21 +376,15 @@ function patchObject(deltaReader, deltaLength, readSource, targetLength) { }) function read(abort, cb) { - // console.error('pa', abort, ended) if (ended) return cb(ended) deltaReader.read(1, function (end, dBuf) { - // console.error("read", end, dBuf) - // if (ended = end) return console.error('patched', deltaLength, targetLength, end), cb(end) if (ended = end) return cb(end) var cmd = dBuf[0] - // console.error('cmd', cmd & 0x80, cmd) if (cmd & 0x80) // skip a variable amount and then pass through a variable amount readOffsetSize(cmd, deltaReader, function (err, offset, size) { - // console.error('offset', err, offset, size) if (err) return earlyEnd(err) var buf = srcBuf.slice(offset, offset + size) - // console.error('buf', buf) cb(end, buf) }) else if (cmd) @@ -369,12 +400,59 @@ function patchObject(deltaReader, deltaLength, readSource, targetLength) { } } -Git.prototype.getCommit = function (opts, cb) { - this.getObject(opts, function (err, buf) { +var gitNameRegex = /^(.*) <(([^>@]*)(@[^>]*)?)> (.*) (.*)$/ +function parseName(line) { + var m = gitNameRegex.exec(line) + if (!m) return null + return { + name: m[1], + email: m[2], + localpart: m[3], + feed: u.isRef(m[4]) && m[4] || undefined, + date: new Date(m[5] * 1000), + tz: m[6], + } +} + +Git.prototype.getCommit = function (obj, cb) { + pull(this.readObject(obj), u.pullConcat(function (err, buf) { if (err) return cb(err) var commit = { - body: buf.toString('ascii') + msg: obj.msg, + parents: [], } + var authorLine, committerLine + var lines = buf.toString('utf8').split('\n') + for (var line; (line = lines.shift()); ) { + var parts = line.split(' ') + var prop = parts.shift() + var value = parts.join(' ') + switch (prop) { + case 'tree': + commit.tree = value + break + case 'parent': + commit.parents.push(value) + break + case 'author': + authorLine = value + break + case 'committer': + committerLine = value + break + case 'gpgsig': + var sigLines = [value] + while (lines[0] && lines[0][0] == ' ') + sigLines.push(lines.shift().slice(1)) + commit.gpgsig = sigLines.join('\n') + break + default: + return cb(new TypeError('unknown git object property ' + prop)) + } + } + commit.committer = parseName(committerLine) + if (authorLine !== committerLine) commit.author = parseName(authorLine) + commit.body = lines.join('\n') cb(null, commit) - }) + })) } diff --git a/lib/render-msg.js b/lib/render-msg.js index ef23b3f..72b8015 100644 --- a/lib/render-msg.js +++ b/lib/render-msg.js @@ -480,7 +480,7 @@ RenderMsg.prototype.gitUpdate = function (cb) { var id = self.c.refs[ref] var type = /^refs\/tags/.test(ref) ? 'tag' : 'commit' var path = id && ('/git/' + type + '/' + encodeURIComponent(id) - + '?head=' + encodeURIComponent(self.msg.key)) + + '?msg=' + encodeURIComponent(self.msg.key)) return h('li', ref.replace(/^refs\/(heads|tags)\//, ''), ': ', id ? h('a', {href: self.render.toUrl(path)}, h('code', id)) @@ -489,7 +489,7 @@ RenderMsg.prototype.gitUpdate = function (cb) { Array.isArray(self.c.commits) ? h('ul', self.c.commits.map(function (commit) { var path = '/git/commit/' + encodeURIComponent(commit.sha1) - + '?head=' + encodeURIComponent(self.msg.key) + + '?msg=' + encodeURIComponent(self.msg.key) return h('li', h('a', {href: self.render.toUrl(path)}, h('code', String(commit.sha1).substr(0, 8))), ' ', self.linkify(String(commit.title)), diff --git a/lib/serve.js b/lib/serve.js index 4e8c15d..f1ec351 100644 --- a/lib/serve.js +++ b/lib/serve.js @@ -1296,16 +1296,49 @@ Serve.prototype.gitRaw = function (rev) { self.respondSink(400, {'Content-Type': 'text/plain'}) ) } - - var headMsgId = self.query.head - pull( - self.app.git.readObject({ - id: rev, - headMsgId: headMsgId, - }), - catchTextError(), - self.respondSink(200, {'Content-type': 'text/plain'}) + if (!u.isRef(self.query.msg)) return pull( + ph('div.error', 'missing message id'), + self.wrapPage('git tree ' + rev), + self.respondSink(400) ) + + self.app.git.openObject({ + obj: rev, + msg: self.query.msg, + }, function (err, obj) { + if (err && err.name === 'BlobNotFoundError') + return self.askWantBlobs(err.links) + if (err) return pull( + pull.once(err.stack), + self.respondSink(400, {'Content-Type': 'text/plain'}) + ) + pull( + self.app.git.readObject(obj), + catchTextError(), + ident(function (type) { + type = type && mime.lookup(type) + if (type) self.res.setHeader('Content-Type', type) + self.res.setHeader('Cache-Control', 'public, max-age=315360000') + self.res.setHeader('etag', rev) + self.res.writeHead(200) + }), + self.respondSink() + ) + }) +} + +Serve.prototype.gitAuthorLink = function (author) { + if (author.feed) { + var myName = this.app.getNameSync(author.feed) + var sigil = author.name === author.localpart ? '@' : '' + return ph('a', { + href: this.app.render.toUrl(author.feed), + title: author.localpart + (myName ? ' (' + myName + ')' : '') + }, u.escapeHTML(sigil + author.name)) + } else { + return ph('a', {href: this.app.render.toUrl('mailto:' + author.email)}, + u.escapeHTML(author.name)) + } } Serve.prototype.gitCommit = function (rev) { @@ -1317,23 +1350,95 @@ Serve.prototype.gitCommit = function (rev) { self.respondSink(400) ) } + if (!u.isRef(self.query.msg)) return pull( + ph('div.error', 'missing message id'), + self.wrapPage('git commit ' + rev), + self.respondSink(400) + ) - self.app.git.getCommit({ - id: rev, - headMsgId: self.query.head, - }, function (err, commit) { + self.app.git.openObject({ + obj: rev, + msg: self.query.msg, + }, function (err, obj) { if (err && err.name === 'BlobNotFoundError') return self.askWantBlobs(err.links) if (err) return pull( pull.once(u.renderError(err).outerHTML), - self.wrapPage('git object ' + rev), + self.wrapPage('git commit ' + rev), self.respondSink(400) ) - pull( - pull.once(h('pre', self.app.render.linkify(commit.body)).outerHTML), - self.wrapPage('git object ' + rev), - self.respondSink(200) - ) + var msgDate = new Date(obj.msg.value.timestamp) + self.app.git.getCommit(obj, function (err, commit) { + var missingBlobs + if (err && err.name === 'BlobNotFoundError') + missingBlobs = err.links, err = null + if (err) return pull( + pull.once(u.renderError(err).outerHTML), + self.wrapPage('git commit ' + rev), + self.respondSink(400) + ) + pull( + ph('section', [ + ph('h3', ph('a', {href: ''}, rev)), + ph('div', [ + self.phIdLink(obj.msg.value.author), ' pushed ', + ph('a', { + href: self.app.render.toUrl(obj.msg.key), + title: msgDate.toLocaleString(), + }, htime(msgDate)) + ]), + missingBlobs ? self.askWantBlobsForm(missingBlobs) : [ + ph('div', [ + self.gitAuthorLink(commit.committer), + ' committed ', + ph('span', {title: commit.committer.date.toLocaleString()}, + htime(commit.committer.date)), + ' in ', commit.committer.tz + ]), + commit.author ? ph('div', [ + self.gitAuthorLink(commit.author), + ' authored ', + ph('span', {title: commit.author.date.toLocaleString()}, + htime(commit.author.date)), + ' in ', commit.author.tz + ]) : '', + commit.parents.length ? ph('div', ['parents: ', pull( + pull.values(commit.parents), + paramap(function (id, cb) { + self.app.git.getObjectMsg({ + obj: id, + headMsgId: obj.msg.key, + }, function (err, msg) { + var path = '/git/commit/' + id + + '?msg=' + encodeURIComponent(msg.key) + cb(null, [ph('code', ph('a', { + href: self.app.render.toUrl(path) + }, id.substr(0, 8))), ' ']) + }) + }, 4) + )]) : '', + commit.tree ? ph('div', ['tree: ', pull( + pull.once(commit.tree), + pull.asyncMap(function (id, cb) { + self.app.git.getObjectMsg({ + obj: id, + headMsgId: obj.msg.key, + }, function (err, msg) { + var path = '/git/tree/' + id + + '?msg=' + encodeURIComponent(msg.key) + cb(null, [ph('code', ph('a', { + href: self.app.render.toUrl(path) + }, id.substr(0, 8))), ' ']) + }) + }) + )]) : '', + h('pre', self.app.render.linkify(commit.body)).outerHTML, + ] + ]), + self.wrapPage('git commit ' + rev), + self.respondSink(missingBlobs ? 409 : 200) + ) + }) }) } @@ -1352,26 +1457,31 @@ Serve.prototype.wrapPublic = function (opts) { }) } +Serve.prototype.askWantBlobsForm = function (links) { + var self = this + return ph('form', {action: '', method: 'post'}, [ + ph('section', [ + ph('h3', 'Missing blobs'), + ph('p', 'The application needs these blobs to continue:'), + ph('table', links.map(u.toLink).map(function (link) { + if (!u.isRef(link.link)) return + return ph('tr', [ + ph('td', ph('code', link.link)), + ph('td', self.app.render.formatSize(link.size)), + ]) + })), + ph('input', {type: 'hidden', name: 'action', value: 'want-blobs'}), + ph('input', {type: 'hidden', name: 'blob_ids', + value: links.map(u.linkDest).join(',')}), + ph('p', ph('input', {type: 'submit', value: 'Want Blobs'})) + ]) + ]) +} + Serve.prototype.askWantBlobs = function (links) { var self = this pull( - ph('form', {action: '', method: 'post'}, [ - ph('section', [ - ph('h3', 'Missing blobs'), - ph('p', 'The application needs these blobs to continue:'), - ph('table', links.map(u.toLink).map(function (link) { - if (!u.isRef(link.link)) return - return ph('tr', [ - ph('td', ph('code', link.link)), - ph('td', self.app.render.formatSize(link.size)), - ]) - })), - ph('input', {type: 'hidden', name: 'action', value: 'want-blobs'}), - ph('input', {type: 'hidden', name: 'blob_ids', - value: links.map(u.linkDest).join(',')}), - ph('p', ph('input', {type: 'submit', value: 'Want Blobs'})) - ]) - ]), + self.askWantBlobsForm(links), self.wrapPage('missing blobs'), self.respondSink(409) ) diff --git a/lib/util.js b/lib/util.js index dbefa14..267df9f 100644 --- a/lib/util.js +++ b/lib/util.js @@ -134,3 +134,9 @@ u.customError = function (name) { return error } } + +u.escapeHTML = function (html) { + return html.toString('utf8') + .replace(/</g, '<') + .replace(/>/g, '>') +} |