diff options
-rw-r--r-- | lib/git.js | 74 | ||||
-rw-r--r-- | lib/serve.js | 262 |
2 files changed, 335 insertions, 1 deletions
@@ -10,6 +10,12 @@ var zlib = require('zlib') var ObjectNotFoundError = u.customError('ObjectNotFoundError') +var types = { + blob: true, + commit: true, + tree: true, +} + module.exports = Git function Git(app) { @@ -456,3 +462,71 @@ Git.prototype.getCommit = function (obj, cb) { cb(null, commit) })) } + +Git.prototype.getTag = function (obj, cb) { + pull(this.readObject(obj), u.pullConcat(function (err, buf) { + if (err) return cb(err) + var tag = { + msg: obj.msg, + } + var authorLine, tagterLine + 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 'object': + tag.object = value + break + case 'type': + if (!types[value]) + return cb(new TypeError('unknown git object type ' + type)) + tag.type = value + break + case 'tag': + tag.tag = value + break + case 'tagger': + tag.tagger = parseName(value) + break + default: + return cb(new TypeError('unknown git object property ' + prop)) + } + } + tag.body = lines.join('\n') + cb(null, tag) + })) +} + +function readCString(reader, cb) { + var chars = [] + reader.read(1, function next(err, ch) { + if (err) return cb(err) + if (ch[0] === 0) return cb(null, Buffer.concat(chars).toString('utf8')) + chars.push(ch) + reader.read(1, next) + }) +} + +Git.prototype.readTree = function (obj) { + var reader = Reader() + reader(this.readObject(obj)) + return function (abort, cb) { + if (abort) return reader.abort(abort, cb) + readCString(reader, function (err, str) { + if (err) return cb(err) + var parts = str.split(' ') + var mode = parseInt(parts[0], 8) + var name = parts.slice(1).join(' ') + reader.read(20, function (err, hash) { + if (err) return cb(err) + cb(null, { + name: name, + mode: mode, + hash: hash.toString('hex') + }) + }) + }) + } +} diff --git a/lib/serve.js b/lib/serve.js index f1ec351..64aa82b 100644 --- a/lib/serve.js +++ b/lib/serve.js @@ -1282,7 +1282,9 @@ Serve.prototype.git = function (url) { var m = /^\/?([^\/]*)\/?(.*)?$/.exec(url) switch (m[1]) { case 'commit': return this.gitCommit(m[2]) - case 'tag': return this.gitCommit(m[2]) + case 'tag': return this.gitTag(m[2]) + case 'tree': return this.gitTree(m[2]) + case 'blob': return this.gitBlob(m[2]) case 'raw': return this.gitRaw(m[2]) default: return this.respond(404, 'Not found') } @@ -1442,6 +1444,264 @@ Serve.prototype.gitCommit = function (rev) { }) } +Serve.prototype.gitTag = function (rev) { + var self = this + if (!/[0-9a-f]{24}/.test(rev)) { + return pull( + ph('div.error', 'rev is not a git object id'), + self.wrapPage('git'), + self.respondSink(400) + ) + } + if (!u.isRef(self.query.msg)) return pull( + ph('div.error', 'missing message id'), + self.wrapPage('git tag ' + 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(u.renderError(err).outerHTML), + self.wrapPage('git tag ' + rev), + self.respondSink(400) + ) + var msgDate = new Date(obj.msg.value.timestamp) + self.app.git.getTag(obj, function (err, tag) { + 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 tag ' + 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(tag.tagger), + ' tagged ', + ph('span', {title: tag.tagger.date.toLocaleString()}, + htime(tag.tagger.date)), + ' in ', tag.tagger.tz + ]), + tag.type, ' ', + pull( + pull.once(tag.object), + pull.asyncMap(function (id, cb) { + self.app.git.getObjectMsg({ + obj: id, + type: tag.type, + headMsgId: obj.msg.key, + }, function (err, msg) { + var path = '/git/' + tag.type + '/' + id + + '?msg=' + encodeURIComponent(msg.key) + cb(null, [ph('code', ph('a', { + href: self.app.render.toUrl(path) + }, id.substr(0, 8))), ' ']) + }) + }) + ), ' ', + ph('code', u.escapeHTML(tag.tag)), + h('pre', self.app.render.linkify(tag.body)).outerHTML, + ] + ]), + self.wrapPage('git tag ' + rev), + self.respondSink(missingBlobs ? 409 : 200) + ) + }) + }) +} + +Serve.prototype.gitTree = function (rev) { + var self = this + if (!/[0-9a-f]{24}/.test(rev)) { + return pull( + ph('div.error', 'rev is not a git object id'), + self.wrapPage('git'), + self.respondSink(400) + ) + } + 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) { + 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 tree ' + rev), + self.respondSink(400) + ) + var msgDate = new Date(obj.msg.value.timestamp) + pull( + ph('section', [ + ph('h3', ph('a', {href: ''}, rev)), + ph('div', [ + self.phIdLink(obj.msg.value.author), ' ', + ph('a', { + href: self.app.render.toUrl(obj.msg.key), + title: msgDate.toLocaleString(), + }, htime(msgDate)) + ]), + missingBlobs ? self.askWantBlobsForm(missingBlobs) : ph('table', [ + pull( + self.app.git.readTree(obj), + paramap(function (file, cb) { + self.app.git.getObjectMsg({ + obj: file.hash, + headMsgId: obj.msg.key, + }, function (err, msg) { + if (err && err.name === 'ObjectNotFoundError') return cb(null, file) + if (err) return cb(err) + file.msg = msg + cb(null, file) + }) + }, 8), + pull.map(function (item) { + var type = item.mode === 0040000 ? 'tree' : + item.mode === 0160000 ? 'commit' : 'blob' + var path = '/git/' + type + '/' + item.hash + + '?msg=' + encodeURIComponent(item.msg.key) + var fileDate = new Date(item.msg.value.timestamp) + return ph('tr', [ + ph('td', + ph('a', {href: self.app.render.toUrl(path)}, + item.name + (type === 'tree' ? '/' : ''))), + item.msg ? ph('td', + self.phIdLink(item.msg.value.author)) : '', + item.msg ? ph('td', + ph('a', { + href: self.app.render.toUrl(item.msg.key), + title: fileDate.toLocaleString(), + }, htime(fileDate)) + ) : '', + ]) + }) + ) + ]), + ]), + self.wrapPage('git tree ' + rev), + self.respondSink(missingBlobs ? 409 : 200) + ) + }) +} + +Serve.prototype.gitBlob = function (rev) { + var self = this + if (!/[0-9a-f]{24}/.test(rev)) { + return pull( + ph('div.error', 'rev is not a git object id'), + self.wrapPage('git'), + self.respondSink(400) + ) + } + if (!u.isRef(self.query.msg)) return pull( + ph('div.error', 'missing message id'), + self.wrapPage('git object ' + rev), + self.respondSink(400) + ) + + self.app.getMsgDecrypted(self.query.msg, function (err, msg) { + if (err) return pull( + pull.once(u.renderError(err).outerHTML), + self.wrapPage('git object ' + rev), + self.respondSink(400) + ) + var msgDate = new Date(msg.value.timestamp) + self.app.git.openObject({ + obj: rev, + msg: msg.key, + }, function (err, obj) { + 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 object ' + rev), + self.respondSink(400) + ) + pull( + ph('section', [ + ph('h3', ph('a', {href: ''}, rev)), + ph('div', [ + self.phIdLink(msg.value.author), ' ', + ph('a', { + href: self.app.render.toUrl(msg.key), + title: msgDate.toLocaleString(), + }, htime(msgDate)) + ]), + missingBlobs ? self.askWantBlobsForm(missingBlobs) : pull( + self.app.git.readObject(obj), + self.wrapBinary({ + rawUrl: self.app.render.toUrl('/git/raw/' + rev + + '?msg=' + encodeURIComponent(msg.key)) + }) + ), + ]), + self.wrapPage('git blob ' + rev), + self.respondSink(200) + ) + }) + }) +} + +// wrap a binary source and render it or turn into an embed +Serve.prototype.wrapBinary = function (opts) { + var self = this + return function (read) { + var readRendered, type + read = ident(function (ext) { + type = ext && mime.lookup(ext) || 'text/plain' + })(read) + return function (abort, cb) { + if (readRendered) return readRendered(abort, cb) + if (abort) return read(abort, cb) + if (!type) read(null, function (end, buf) { + if (end) return cb(end) + if (!type) return cb(new Error('unable to get type')) + readRendered = pickSource(type, cat([pull.once(buf), read])) + readRendered(null, cb) + }) + } + } + function pickSource(type, read) { + if (/^image\//.test(type)) { + read(true, function (err) { + if (err && err !== true) console.trace(err) + }) + return ph('img', { + src: opts.rawUrl + }) + } + return ph('pre', pull.map(function (buf) { + return h('div', + self.app.render.linkify(buf.toString('utf8')) + ).innerHTML + })(read)) + } +} + Serve.prototype.wrapPublic = function (opts) { var self = this return u.hyperwrap(function (thread, cb) { |