aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/git.js262
-rw-r--r--lib/render-msg.js4
-rw-r--r--lib/serve.js182
-rw-r--r--lib/util.js6
4 files changed, 324 insertions, 130 deletions
diff --git a/lib/git.js b/lib/git.js
index 36e4587..048e2b3 100644
--- a/lib/git.js
+++ b/lib/git.js
@@ -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, '&lt;')
+ .replace(/>/g, '&gt;')
+}