var fs = require('fs') var http = require('http') var path = require('path') var url = require('url') var qs = require('querystring') var util = require('util') var ref = require('ssb-ref') var pull = require('pull-stream') var ssbGit = require('ssb-git-repo') var toPull = require('stream-to-pull-stream') var cat = require('pull-cat') var Repo = require('pull-git-repo') var ssbAbout = require('./about') var ssbVotes = require('./votes') var i18n = require('./i18n') var marked = require('ssb-marked') var asyncMemo = require('asyncmemo') var multicb = require('multicb') var schemas = require('ssb-msg-schemas') var Issues = require('ssb-issues') var PullRequests = require('ssb-pull-requests') var paramap = require('pull-paramap') var gitPack = require('pull-git-pack') var Mentions = require('ssb-mentions') var Highlight = require('highlight.js') var JsDiff = require('diff') var many = require('pull-many') var hlCssPath = path.resolve(require.resolve('highlight.js'), '../../styles') // render links to git objects and ssb objects var blockRenderer = new marked.Renderer() blockRenderer.urltransform = function (url) { if (ref.isLink(url)) return encodeLink(url) if (/^[0-9a-f]{40}$/.test(url) && this.options.repo) return encodeLink([this.options.repo.id, 'commit', url]) return url } blockRenderer.image = function (href, title, text) { href = href.replace(/^&/, '&') var url if (ref.isBlobId(href)) url = encodeLink(href) else if (this.options.repo && this.options.rev && this.options.path) url = path.join('/', encodeURIComponent(this.options.repo.id), 'raw', this.options.rev, this.options.path.join('/'), href) else return text return '' + text + '' } function getExtension(filename) { return (/\.([^.]+)$/.exec(filename) || [,filename])[1] } function highlight(code, lang) { try { return lang ? Highlight.highlight(lang, code).value : Highlight.highlightAuto(code).value } catch(e) { if (/^Unknown language/.test(e.message)) return escapeHTML(code) throw e } } marked.setOptions({ gfm: true, mentions: true, tables: true, breaks: true, pedantic: false, sanitize: true, smartLists: true, smartypants: false, highlight: highlight, renderer: blockRenderer }) // hack to make git link mentions work var mdRules = new marked.InlineLexer(1, marked.defaults).rules mdRules.mention = /^(\s)?([@%&][A-Za-z0-9\._\-+=\/]*[A-Za-z0-9_\-+=\/]|[0-9a-f]{40})/ mdRules.text = /^[\s\S]+?(?=[\\' + text + '' } function linkify(text) { // regex is from ssb-ref return text.replace(/(@|%|&|&)[A-Za-z0-9\/+]{43}=\.[\w\d]+/g, function (str) { return '' + str + '' }) } function timestamp(time, req) { time = Number(time) var d = new Date(time) return '' + d.toLocaleString(req._locale) + '' } function pre(text) { return '
' + escapeHTML(text) + '
' } function json(obj) { return linkify(pre(JSON.stringify(obj, null, 2))) } function escapeHTML(str) { return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') } function table(props) { return function (read) { return cat([ pull.once(''), pull( read, pull.map(function (row) { return row ? '' + row.map(function (cell) { return '' + cell + '' }).join('') + '' : '' }) ), pull.once('') ]) } } function ul(props) { return function (read) { return cat([ pull.once(''), pull( read, pull.map(function (li) { return '
  • ' + li + '
  • ' }) ), pull.once('') ]) } } function nav(links, page, after) { return ['').join('') } function renderNameForm(req, enabled, id, name, action, inputId, title, header) { if (!inputId) inputId = action return '
    ' + (enabled ? '' + '' + '' + '' + ' ' + '' + header : header + '
    ' ) + '
    ' } function renderPostForm(req, repo, placeholder, rows) { return '' + '' + '' + '' + '
    ' + '' + '
    ' + '
    ' + '' } function hiddenInputs(values) { return Object.keys(values).map(function (key) { return '' }).join('') } function readNext(fn) { var next return function (end, cb) { if (next) return next(end, cb) fn(function (err, _next) { if (err) return cb(err) next = _next next(null, cb) }) } } function readOnce(fn) { var ended return function (end, cb) { fn(function (err, data) { if (err || ended) return cb(err || ended) ended = true cb(null, data) }) } } function paginate(onFirst, through, onLast, onEmpty) { var ended, last, first = true, queue = [] return function (read) { var mappedRead = through(function (end, cb) { if (ended = end) return read(ended, cb) if (queue.length) return cb(null, queue.shift()) read(null, function (end, data) { if (end) return cb(end) last = data cb(null, data) }) }) return function (end, cb) { var tmp if (ended) return cb(ended) if (ended = end) return read(ended, cb) if (first) return read(null, function (end, data) { if (ended = end) { if (end === true && onEmpty) return onEmpty(cb) return cb(ended) } first = false last = data queue.push(data) if (onFirst) onFirst(data, cb) else mappedRead(null, cb) }) mappedRead(null, function (end, data) { if (ended = end) { if (end === true && last) return onLast(last, cb) } cb(end, data) }) } } } function readObjectString(obj, cb) { pull(obj.read, pull.collect(function (err, bufs) { if (err) return cb(err) cb(null, Buffer.concat(bufs, obj.length).toString('utf8')) })) } function getRepoObjectString(repo, id, cb) { if (!id) return cb(null, '') repo.getObjectFromAny(id, function (err, obj) { if (err) return cb(err) readObjectString(obj, cb) }) } function compareMsgs(a, b) { return (a.value.timestamp - b.value.timestamp) || (a.key - b.key) } function pullSort(comparator) { return function (read) { return readNext(function (cb) { pull(read, pull.collect(function (err, items) { if (err) return cb(err) items.sort(comparator) cb(null, pull.values(items)) })) }) } } function sortMsgs() { return pullSort(compareMsgs) } function pullReverse() { return function (read) { return readNext(function (cb) { pull(read, pull.collect(function (err, items) { cb(err, items && pull.values(items.reverse())) })) }) } } function tryDecodeURIComponent(str) { if (!str || (str[0] == '%' && ref.isBlobId(str))) return str try { str = decodeURIComponent(str) } finally { return str } } function getRepoName(about, ownerId, repoId, cb) { about.getName({ owner: ownerId, target: repoId, toString: function () { // hack to fit two parameters into asyncmemo return ownerId + '/' + repoId } }, cb) } function getRepoFullName(about, author, repoId, cb) { var done = multicb({ pluck: 1, spread: true }) getRepoName(about, author, repoId, done()) about.getName(author, done()) done(cb) } function addAuthorName(about) { return paramap(function (msg, cb) { var author = msg && msg.value && msg.value.author if (!author) return cb(null, msg) about.getName(author, function (err, authorName) { msg.authorName = authorName cb(err, msg) }) }, 8) } function getMention(msg, id) { if (msg.key == id) return msg var mentions = msg.value.content.mentions if (mentions) for (var i = 0; i < mentions.length; i++) { var mention = mentions[i] if (mention.link == id) return mention } return null } var hasOwnProp = Object.prototype.hasOwnProperty function getContentType(filename) { var ext = getExtension(filename) return contentTypes[ext] || imgMimes[ext] || 'text/plain; charset=utf-8' } var contentTypes = { css: 'text/css' } function readReqForm(req, cb) { pull( toPull(req), pull.collect(function (err, bufs) { if (err) return cb(err) var data try { data = qs.parse(Buffer.concat(bufs).toString('ascii')) } catch(e) { return cb(e) } cb(null, data) }) ) } var issueCommentScript = '(' + function () { var $ = document.getElementById.bind(document) $('tab-links').style.display = 'block' $('preview-tab-link').onclick = function (e) { with (new XMLHttpRequest()) { open('POST', '', true) onload = function() { $('preview-tab').innerHTML = responseText } send('action=markdown' + '&repo=' + encodeURIComponent($('repo-id').value) + '&text=' + encodeURIComponent($('post-text').value)) } } }.toString() + ')()' var msgTypes = { 'git-repo': true, 'git-update': true, 'issue': true, 'pull-request': true } var imgMimes = { png: 'image/png', jpeg: 'image/jpeg', jpg: 'image/jpeg', gif: 'image/gif', tif: 'image/tiff', svg: 'image/svg+xml', bmp: 'image/bmp' } module.exports = function (opts, cb) { var ssb, reconnect, myId, getRepo, getVotes, getMsg, issues var about = function (id, cb) { cb(null, {name: id}) } var reqQueue = [] var isPublic = opts.public var ssbAppname = opts.appname || 'ssb' var addr = parseAddr(opts.listenAddr, {host: 'localhost', port: 7718}) http.createServer(onRequest).listen(addr.port, addr.host, onListening) var server = { setSSB: function (_ssb, _reconnect) { _ssb.whoami(function (err, feed) { if (err) throw err ssb = _ssb reconnect = _reconnect myId = feed.id about = ssbAbout(ssb, myId) while (reqQueue.length) onRequest.apply(this, reqQueue.shift()) getRepo = asyncMemo(function (id, cb) { getMsg(id, function (err, msg) { if (err) return cb(err) ssbGit.getRepo(ssb, {key: id, value: msg}, {live: true}, cb) }) }) getVotes = ssbVotes(ssb) getMsg = asyncMemo(ssb.get) issues = Issues.init(ssb) pullReqs = PullRequests.init(ssb) }) } } function onListening() { var host = ~addr.host.indexOf(':') ? '[' + addr.host + ']' : addr.host console.log('Listening on http://' + host + ':' + addr.port + '/') cb(null, server) } /* Serving a request */ function onRequest(req, res) { console.log(req.method, req.url) if (!ssb) return reqQueue.push(arguments) req._u = url.parse(req.url, true) var locale = req._u.query.locale || (/locale=([^;]*)/.exec(req.headers.cookie) || [])[1] var reqLocales = req.headers['accept-language'] i18n.pickCatalog(reqLocales, locale, function (err, t) { if (err) return pull(serveError(req, err, 500), serve(req, res)) req._t = t req._locale = t.locale pull(handleRequest(req), serve(req, res)) }) } function serve(req, res) { return pull( pull.filter(function (data) { if (Array.isArray(data)) { res.writeHead.apply(res, data) return false } return true }), toPull(res) ) } function handleRequest(req) { var path = req._u.pathname.slice(1) var dirs = ref.isLink(path) ? [path] : path.split(/\/+/).map(tryDecodeURIComponent) var dir = dirs[0] if (req.method == 'POST') { if (isPublic) return serveBuffer(405, req._t('error.POSTNotAllowed')) return readNext(function (cb) { readReqForm(req, function (err, data) { if (err) return cb(null, serveError(req, err, 400)) if (!data) return cb(null, serveError(req, new ParamError(req._t('error.MissingData')), 400)) switch (data.action) { case 'fork-prompt': return cb(null, serveRedirect(req, encodeLink([data.id, 'fork']))) case 'fork': if (!data.id) return cb(null, serveError(req, new ParamError(req._t('error.MissingId')), 400)) return ssbGit.createRepo(ssb, {upstream: data.id}, function (err, repo) { if (err) return cb(null, serveError(req, err)) cb(null, serveRedirect(req, encodeLink(repo.id))) }) case 'vote': var voteValue = +data.value || 0 if (!data.id) return cb(null, serveError(req, new ParamError(req._t('error.MissingId')), 400)) var msg = schemas.vote(data.id, voteValue) return ssb.publish(msg, function (err) { if (err) return cb(null, serveError(req, err)) cb(null, serveRedirect(req, req.url)) }) case 'repo-name': if (!data.id) return cb(null, serveError(req, new ParamError(req._t('error.MissingId')), 400)) if (!data.name) return cb(null, serveError(req, new ParamError(req._t('error.MissingName')), 400)) var msg = schemas.name(data.id, data.name) return ssb.publish(msg, function (err) { if (err) return cb(null, serveError(req, err)) cb(null, serveRedirect(req, req.url)) }) case 'issue-title': if (!data.id) return cb(null, serveError(req, new ParamError(req._t('error.MissingId')), 400)) if (!data.name) return cb(null, serveError(req, new ParamError(req._t('error.MissingName')), 400)) var msg = Issues.schemas.edit(data.id, {title: data.name}) return ssb.publish(msg, function (err) { if (err) return cb(null, serveError(req, err)) cb(null, serveRedirect(req, req.url)) }) case 'comment': if (!data.id) return cb(null, serveError(req, new ParamError(req._t('error.MissingId')), 400)) var msg = schemas.post(data.text, data.id, data.branch || data.id) msg.issue = data.issue msg.repo = data.repo if (data.open != null) Issues.schemas.opens(msg, data.id) if (data.close != null) Issues.schemas.closes(msg, data.id) var mentions = Mentions(data.text) if (mentions.length) msg.mentions = mentions return ssb.publish(msg, function (err) { if (err) return cb(null, serveError(req, err)) cb(null, serveRedirect(req, req.url)) }) case 'new-issue': var msg = Issues.schemas.new(dir, data.title, data.text) var mentions = Mentions(data.text) if (mentions.length) msg.mentions = mentions return ssb.publish(msg, function (err, msg) { if (err) return cb(null, serveError(req, err)) cb(null, serveRedirect(req, encodeLink(msg.key))) }) case 'new-pull': var msg = PullRequests.schemas.new(dir, data.branch, data.head_repo, data.head_branch, data.title, data.text) var mentions = Mentions(data.text) if (mentions.length) msg.mentions = mentions return ssb.publish(msg, function (err, msg) { if (err) return cb(null, serveError(req, err)) cb(null, serveRedirect(req, encodeLink(msg.key))) }) case 'markdown': return cb(null, serveMarkdown(data.text, {id: data.repo})) default: cb(null, serveBuffer(400, req._t('error.UnknownAction', data))) } }) }) } if (dir == '') return serveIndex(req) else if (dir == 'search') return serveSearch(req) else if (ref.isBlobId(dir)) return serveBlob(req, dir) else if (ref.isMsgId(dir)) return serveMessage(req, dir, dirs.slice(1)) else if (ref.isFeedId(dir)) return serveUserPage(req, dir, dirs.slice(1)) else if (dir == 'static') return serveFile(req, dirs) else if (dir == 'highlight') return serveFile(req, [hlCssPath].concat(dirs.slice(1)), true) else return serve404(req) } function serveFile(req, dirs, outside) { var filename = path.resolve.apply(path, [__dirname].concat(dirs)) // prevent escaping base dir if (!outside && filename.indexOf('../') === 0) return serveBuffer(403, req._t("error.403Forbidden")) return readNext(function (cb) { fs.stat(filename, function (err, stats) { cb(null, err ? err.code == 'ENOENT' ? serve404(req) : serveBuffer(500, err.message) : 'if-modified-since' in req.headers && new Date(req.headers['if-modified-since']) >= stats.mtime ? pull.once([304]) : stats.isDirectory() ? serveBuffer(403, req._t('error.DirectoryNotListable')) : cat([ pull.once([200, { 'Content-Type': getContentType(filename), 'Content-Length': stats.size, 'Last-Modified': stats.mtime.toGMTString() }]), toPull(fs.createReadStream(filename)) ])) }) }) } function servePlainError(code, msg) { return pull.values([ [code, { 'Content-Length': Buffer.byteLength(msg), 'Content-Type': 'text/plain; charset=utf-8' }], msg ]) } function serveBuffer(code, buf, contentType, headers) { headers = headers || {} headers['Content-Type'] = contentType || 'text/plain; charset=utf-8' headers['Content-Length'] = Buffer.byteLength(buf) return pull.values([ [code, headers], buf ]) } function serve404(req) { return serveBuffer(404, req._t("error.404NotFound")) } function serveRedirect(req, path) { return serveBuffer(302, '' + '' + req._t('Redirect') + '' + '

    ' + req._t('Continue') + '

    ' + '', 'text/html; charset=utf-8', {Location: path}) } function serveMarkdown(text, repo) { return serveBuffer(200, markdown(text, repo), 'text/html; charset=utf-8') } function renderError(err, tag) { tag = tag || 'h3' return '<' + tag + '>' + err.name + '' + '
    ' + escapeHTML(err.stack) + '
    ' } function renderTry(read) { var ended return function (end, cb) { if (ended) return cb(ended) read(end, function (err, data) { if (err === true) cb(true) else if (err) { ended = true cb(null, renderError(err, 'h3')) } else cb(null, data) }) } } function serveTemplate(req, title, code, read) { if (read === undefined) return serveTemplate.bind(this, req, title, code) var q = req._u.query.q && escapeHTML(req._u.query.q) || '' var app = 'git ssb' if (req._t) app = req._t(app) return cat([ pull.values([ [code || 200, { 'Content-Type': 'text/html' }], '', '' + (title || app) + '', '', '', '\n', '', '
    ' + '

    ' + app + '' + (ssbAppname != 'ssb' ? ' ' + ssbAppname + '' : '') + ' ' + '' + '

    ', '
    ', '
    ']), renderTry(read), pull.once('
    ') ]) } function serveError(req, err, status) { if (err.message == 'stream is closed') reconnect() return pull( pull.once(renderError(err, 'h2')), serveTemplate(req, err.name, status || 500) ) } function renderObjectData(obj, filename, repo, rev, path) { var ext = getExtension(filename) return readOnce(function (cb) { readObjectString(obj, function (err, buf) { buf = buf.toString('utf8') if (err) return cb(err) cb(null, (ext == 'md' || ext == 'markdown') ? markdown(buf, {repo: repo, rev: rev, path: path}) : renderCodeTable(buf, ext)) }) }) } function renderCodeTable(buf, ext) { return '
    ' +
          highlight(buf, ext).split('\n').map(function (line, i) {
            i++
            return '' +
              '' +
              ''
          }).join('') +
          '
    ' + '' + i + '' + line + '
    ' } /* Feed */ function renderFeed(req, feedId, filter) { var query = req._u.query var opts = { reverse: !query.forwards, lt: query.lt && +query.lt || Date.now(), gt: query.gt && +query.gt, id: feedId } return pull( feedId ? ssb.createUserStream(opts) : ssb.createFeedStream(opts), pull.filter(function (msg) { return msg.value.content.type in msgTypes }), typeof filter == 'function' ? filter(opts) : filter, pull.take(20), addAuthorName(about), query.forwards && pullReverse(), paginate( function (first, cb) { if (!query.lt && !query.gt) return cb(null, '') var gt = feedId ? first.value.sequence : first.value.timestamp + 1 query.gt = gt query.forwards = 1 delete query.lt cb(null, '' + req._t('Newer') + '') }, paramap(renderFeedItem.bind(null, req), 8), function (last, cb) { query.lt = feedId ? last.value.sequence : last.value.timestamp - 1 delete query.gt delete query.forwards cb(null, '' + req._t('Older') + '') }, function (cb) { if (query.forwards) { delete query.gt delete query.forwards query.lt = opts.gt + 1 } else { delete query.lt query.gt = opts.lt - 1 query.forwards = 1 } cb(null, '' + req._t(query.forwards ? 'Older' : 'Newer') + '') } ) ) } function renderFeedItem(req, msg, cb) { var c = msg.value.content var msgLink = link([msg.key], new Date(msg.value.timestamp).toLocaleString(req._locale)) var author = msg.value.author var authorLink = link([msg.value.author], msg.authorName) switch (c.type) { case 'git-repo': var done = multicb({ pluck: 1, spread: true }) getRepoName(about, author, msg.key, done()) if (c.upstream) { return getMsg(c.upstream, function (err, upstreamMsg) { if (err) return cb(null, serveError(req, err)) getRepoName(about, upstreamMsg.author, c.upstream, done()) done(function (err, repoName, upstreamName) { cb(null, '
    ' + msgLink + '
    ' + req._t('Forked', { name: authorLink, upstream: link([c.upstream], upstreamName), repo: link([msg.key], repoName) }) + '
    ') }) }) } else { return done(function (err, repoName) { if (err) return cb(err) var repoLink = link([msg.key], repoName) cb(null, '
    ' + msgLink + '
    ' + req._t('CreatedRepo', { name: authorLink, repo: repoLink }) + '
    ') }) } case 'git-update': return getRepoName(about, author, c.repo, function (err, repoName) { if (err) return cb(err) var repoLink = link([c.repo], repoName) cb(null, '
    ' + msgLink + '
    ' + req._t('Pushed', { name: authorLink, repo: repoLink }) + '
    ') }) case 'issue': case 'pull-request': var issueLink = link([msg.key], c.title) return getMsg(c.project, function (err, projectMsg) { if (err) return cb(null, serveRepoNotFound(req, c.repo, err)) getRepoName(about, projectMsg.author, c.project, function (err, repoName) { if (err) return cb(err) var repoLink = link([c.project], repoName) cb(null, '
    ' + msgLink + '
    ' + req._t('OpenedIssue', { name: authorLink, type: req._t(c.type == 'pull-request' ? 'pull request' : 'issue.'), title: issueLink, project: repoLink }) + '
    ') }) }) case 'about': return cb(null, '
    ' + msgLink + '
    ' + req._t('Named', { author: authorLink, target: '' + escapeHTML(c.about) + '', name: link([c.about], c.name) }) + '
    ') default: return cb(null, json(msg)) } } /* Index */ function serveIndex(req) { return serveTemplate(req)(renderFeed(req)) } function serveUserPage(req, feedId, dirs) { switch (dirs[0]) { case undefined: case '': case 'activity': return serveUserActivity(req, feedId) case 'repos': return serveUserRepos(req, feedId) } } function renderUserPage(req, feedId, page, body) { return serveTemplate(req, feedId)(cat([ readOnce(function (cb) { about.getName(feedId, function (err, name) { cb(null, '

    ' + link([feedId], name) + '' + feedId + '

    ' + nav([ [[feedId], req._t('Activity'), 'activity'], [[feedId, 'repos'], req._t('Repos'), 'repos'] ], page)) }) }), body, ])) } function serveUserActivity(req, feedId) { return renderUserPage(req, feedId, 'activity', renderFeed(req, feedId)) } function serveUserRepos(req, feedId) { return renderUserPage(req, feedId, 'repos', pull( ssb.messagesByType({ type: 'git-repo', reverse: true }), pull.filter(function (msg) { return msg.value.author == feedId }), pull.take(20), paramap(function (msg, cb) { getRepoName(about, feedId, msg.key, function (err, repoName) { if (err) return cb(err) cb(null, '
    ' + link([msg.key], repoName) + '
    ') }) }, 8) )) } /* Message */ function serveMessage(req, id, path) { return readNext(function (cb) { ssb.get(id, function (err, msg) { if (err) return cb(null, serveError(req, err)) var c = msg.content || {} switch (c.type) { case 'git-repo': return getRepo(id, function (err, repo) { if (err) return cb(null, serveError(req, err)) cb(null, serveRepoPage(req, Repo(repo), path)) }) case 'git-update': return getRepo(c.repo, function (err, repo) { if (err) return cb(null, serveRepoNotFound(req, c.repo, err)) cb(null, serveRepoUpdate(req, Repo(repo), id, msg, path)) }) case 'issue': return getRepo(c.project, function (err, repo) { if (err) return cb(null, serveRepoNotFound(req, c.project, err)) issues.get(id, function (err, issue) { if (err) return cb(null, serveError(req, err)) cb(null, serveRepoIssue(req, Repo(repo), issue, path)) }) }) case 'pull-request': return getRepo(c.repo, function (err, repo) { if (err) return cb(null, serveRepoNotFound(req, c.project, err)) pullReqs.get(id, function (err, pr) { if (err) return cb(null, serveError(req, err)) cb(null, serveRepoPullReq(req, Repo(repo), pr, path)) }) }) case 'issue-edit': if (ref.isMsgId(c.issue)) { return pullReqs.get(c.issue, function (err, issue) { if (err) return cb(err) var serve = issue.msg.value.content.type == 'pull-request' ? serveRepoPullReq : serveRepoIssue getRepo(issue.project, function (err, repo) { if (err) { if (!repo) return cb(null, serveRepoNotFound(req, c.repo, err)) return cb(null, serveError(req, err)) } cb(null, serve(req, Repo(repo), issue, path, id)) }) }) } // fallthrough case 'post': if (ref.isMsgId(c.issue) && ref.isMsgId(c.repo)) { // comment on an issue var done = multicb({ pluck: 1, spread: true }) getRepo(c.repo, done()) pullReqs.get(c.issue, done()) return done(function (err, repo, issue) { if (err) { if (!repo) return cb(null, serveRepoNotFound(req, c.repo, err)) return cb(null, serveError(req, err)) } var serve = issue.msg.value.content.type == 'pull-request' ? serveRepoPullReq : serveRepoIssue cb(null, serve(req, Repo(repo), issue, path, id)) }) } else if (ref.isMsgId(c.root)) { // comment on issue from patchwork? return getMsg(c.root, function (err, root) { if (err) return cb(null, serveError(req, err)) var repoId = root.content.repo || root.content.project if (!ref.isMsgId(repoId)) return cb(null, serveGenericMessage(req, id, msg, path)) getRepo(repoId, function (err, repo) { if (err) return cb(null, serveError(req, err)) switch (root.content && root.content.type) { case 'issue': return issues.get(c.root, function (err, issue) { if (err) return cb(null, serveError(req, err)) return cb(null, serveRepoIssue(req, Repo(repo), issue, path, id)) }) case 'pull-request': pullReqs.get(c.root, function (err, pr) { if (err) return cb(null, serveError(req, err)) return cb(null, serveRepoPullReq(req, Repo(repo), pr, path, id)) }) } }) }) } // fallthrough default: if (ref.isMsgId(c.repo)) return getRepo(c.repo, function (err, repo) { if (err) return cb(null, serveRepoNotFound(req, c.repo, err)) cb(null, serveRepoSomething(req, Repo(repo), id, msg, path)) }) else return cb(null, serveGenericMessage(req, id, msg, path)) } }) }) } function serveGenericMessage(req, id, msg, path) { return serveTemplate(req, id)(pull.once( '

    ' + link([id]) + '

    ' + json(msg) + '
    ')) } /* Repo */ function serveRepoPage(req, repo, path) { var defaultBranch = 'master' var query = req._u.query if (query.rev != null) { // Allow navigating revs using GET query param. // Replace the branch in the path with the rev query value path[0] = path[0] || 'tree' path[1] = query.rev req._u.pathname = encodeLink([repo.id].concat(path)) delete req._u.query.rev delete req._u.search return serveRedirect(req, url.format(req._u)) } // get branch return path[1] ? serveRepoPage2(req, repo, path) : readNext(function (cb) { // TODO: handle this in pull-git-repo or ssb-git-repo repo.getSymRef('HEAD', true, function (err, ref) { if (err) return cb(err) repo.resolveRef(ref, function (err, rev) { path[1] = rev ? ref : null cb(null, serveRepoPage2(req, repo, path)) }) }) }) } function serveRepoPage2(req, repo, path) { var branch = path[1] var filePath = path.slice(2) switch (path[0]) { case undefined: case '': return serveRepoTree(req, repo, branch, []) case 'activity': return serveRepoActivity(req, repo, branch) case 'commits': return serveRepoCommits(req, repo, branch) case 'commit': return serveRepoCommit(req, repo, path[1]) case 'tree': return serveRepoTree(req, repo, branch, filePath) case 'blob': return serveRepoBlob(req, repo, branch, filePath) case 'raw': return serveRepoRaw(req, repo, branch, filePath) case 'digs': return serveRepoDigs(req, repo) case 'fork': return serveRepoForkPrompt(req, repo) case 'forks': return serveRepoForks(req, repo) case 'issues': switch (path[1]) { case 'new': if (filePath.length == 0) return serveRepoNewIssue(req, repo) break default: return serveRepoIssues(req, repo, filePath) } case 'pulls': return serveRepoPullReqs(req, repo) case 'compare': return serveRepoCompare(req, repo) case 'comparing': return serveRepoComparing(req, repo) default: return serve404(req) } } function serveRepoNotFound(req, id, err) { return serveTemplate(req, req._t('error.RepoNotFound'), 404)(pull.values([ '

    ' + req._t('error.RepoNotFound') + '

    ', '

    ' + req._t('error.RepoNameNotFound') + '

    ', '
    ' + escapeHTML(err.stack) + '
    ' ])) } function renderRepoPage(req, repo, page, branch, body) { var gitUrl = 'ssb://' + repo.id var gitLink = '' var digsPath = [repo.id, 'digs'] var done = multicb({ pluck: 1, spread: true }) getRepoName(about, repo.feed, repo.id, done()) about.getName(repo.feed, done()) getVotes(repo.id, done()) if (repo.upstream) { getRepoName(about, repo.upstream.feed, repo.upstream.id, done()) about.getName(repo.upstream.feed, done()) } return readNext(function (cb) { done(function (err, repoName, authorName, votes, upstreamName, upstreamAuthorName) { if (err) return cb(null, serveError(req, err)) var upvoted = votes.upvoters[myId] > 0 var upstreamLink = !repo.upstream ? '' : link([repo.upstream]) cb(null, serveTemplate(req, repo.id)(cat([ pull.once( '
    ' + '
    ' + '' + (isPublic ? '' : '' + '') + ' ' + '' + link(digsPath, votes.upvotes) + ' ' + (isPublic ? '' : '') + ' ' + link([repo.id, 'forks'], '+', false, ' title="' + req._t('Forks') + '"') + '
    ' + renderNameForm(req, !isPublic, repo.id, repoName, 'repo-name', null, req._t('repo.Rename'), '

    ' + link([repo.feed], authorName) + ' / ' + link([repo.id], repoName) + '

    ') + '
    ' + (repo.upstream ? '' + req._t('ForkedFrom', { repo: link([repo.upstream.feed], upstreamAuthorName) + '/' + link([repo.upstream.id], upstreamName) }) + '' : '') + nav([ [[repo.id], req._t('Code'), 'code'], [[repo.id, 'activity'], req._t('Activity'), 'activity'], [[repo.id, 'commits', branch||''], req._t('Commits'), 'commits'], [[repo.id, 'issues'], req._t('Issues'), 'issues'], [[repo.id, 'pulls'], req._t('PullRequests'), 'pulls'] ], page, gitLink)), body ]))) }) }) } function serveEmptyRepo(req, repo) { if (repo.feed != myId) return renderRepoPage(req, repo, 'code', null, pull.once( '
    ' + '

    ' + req._t('EmptyRepo') + '

    ' + '
    ')) var gitUrl = 'ssb://' + repo.id return renderRepoPage(req, repo, 'code', null, pull.once( '
    ' + '

    ' + req._t('initRepo.GettingStarted') + '

    ' + '

    ' + req._t('initRepo.CreateNew') + '

    ' +
          'touch ' + req._t('initRepo.README') + '.md\n' +
          'git init\n' +
          'git add ' + req._t('initRepo.README') + '.md\n' +
          'git commit -m "' + req._t('initRepo.InitialCommit') + '"\n' +
          'git remote add origin ' + gitUrl + '\n' +
          'git push -u origin master
    \n' + '

    ' + req._t('initRepo.PushExisting') + '

    \n' + '
    git remote add origin ' + gitUrl + '\n' +
          'git push -u origin master
    ' + '
    ')) } function serveRepoTree(req, repo, rev, path) { if (!rev) return serveEmptyRepo(req, repo) var type = repo.isCommitHash(rev) ? 'Tree' : 'Branch' return renderRepoPage(req, repo, 'code', rev, cat([ pull.once('
    ' + '

    ' + req._t(type) + ': ' + rev + ' '), revMenu(req, repo, rev), pull.once('

    '), type == 'Branch' && renderRepoLatest(req, repo, rev), pull.once('
    '), renderRepoTree(req, repo, rev, path), pull.once('
    '), renderRepoReadme(req, repo, rev, path) ])) } /* Search */ function serveSearch(req) { var q = String(req._u.query.q || '') if (!q) return serveIndex(req) var qId = q.replace(/^ssb:\/*/, '') if (ref.type(qId)) return serveRedirect(req, encodeURI(qId)) var search = new RegExp(q, 'i') return serveTemplate(req, req._t('Search') + ' · ' + q, 200)( renderFeed(req, null, function (opts) { opts.type == 'about' return function (read) { return pull( many([ getRepoNames(opts), read ]), pull.filter(function (msg) { var c = msg.value.content return ( search.test(msg.key) || c.text && search.test(c.text) || c.name && search.test(c.name) || c.title && search.test(c.title)) }) ) } }) ) } function getRepoNames(opts) { return pull( ssb.messagesByType({ type: 'about', reverse: opts.reverse, lt: opts.lt, gt: opts.gt, }), pull.filter(function (msg) { return '%' == String(msg.value.content.about)[0] && msg.value.content.name }) ) } /* Repo activity */ function serveRepoActivity(req, repo, branch) { return renderRepoPage(req, repo, 'activity', branch, cat([ pull.once('

    ' + req._t('Activity') + '

    '), pull( ssb.links({ dest: repo.id, source: repo.feed, rel: 'repo', values: true, reverse: true }), pull.map(renderRepoUpdate.bind(this, req, repo)) ), readOnce(function (cb) { var done = multicb({ pluck: 1, spread: true }) about.getName(repo.feed, done()) getMsg(repo.id, done()) done(function (err, authorName, msg) { if (err) return cb(err) renderFeedItem(req, { key: repo.id, value: msg, authorName: authorName }, cb) }) }) ])) } function renderRepoUpdate(req, repo, msg, full) { var c = msg.value.content if (c.type != 'git-update') { return '' // return renderFeedItem(msg, cb) // TODO: render post, issue, pull-request } var refs = c.refs ? Object.keys(c.refs).map(function (ref) { return {name: ref, value: c.refs[ref]} }) : [] var numObjects = c.objects ? Object.keys(c.objects).length : 0 var dateStr = new Date(msg.value.timestamp).toLocaleString(req._locale) return '
    ' + link([msg.key], dateStr) + '
    ' + (numObjects ? req._t('PushedObjects', numObjects) + '
    ': '') + refs.map(function (update) { var name = escapeHTML(update.name) if (!update.value) { return req._t('DeletedBranch', {branch: name}) } else { var commitLink = link([repo.id, 'commit', update.value]) return name + ' → ' + commitLink } }).join('
    ') + '
    ' } /* Repo commits */ function serveRepoCommits(req, repo, branch) { var query = req._u.query return renderRepoPage(req, repo, 'commits', branch, cat([ pull.once('

    ' + req._t('Commits') + '

    '), pull( repo.readLog(query.start || branch), pull.take(20), paramap(repo.getCommitParsed.bind(repo), 8), paginate( !query.start ? '' : function (first, cb) { cb(null, '…') }, pull.map(renderCommit.bind(this, req, repo)), function (commit, cb) { cb(null, commit.parents && commit.parents[0] ? '' + req._t('Older') + '' : '') } ) ) ])) } function renderCommit(req, repo, commit) { var commitPath = [repo.id, 'commit', commit.id] var treePath = [repo.id, 'tree', commit.id] return '
    ' + '' + link(commitPath, commit.title) + '
    ' + '' + commit.id + ' ' + link(treePath, req._t('Tree')) + '
    ' + escapeHTML(commit.author.name) + ' · ' + commit.author.date.toLocaleString(req._locale) + (commit.separateAuthor ? '
    ' + req._t('CommittedOn', { name: escapeHTML(commit.committer.name), date: commit.committer.date.toLocaleString(req._locale) }) : '') + '
    ' } /* Branch menu */ function formatRevOptions(currentName) { return function (name) { var htmlName = escapeHTML(name) return '' } } function formatRevType(req, type) { return ( type == 'heads' ? req._t('Branches') : type == 'tags' ? req._t('Tags') : type) } function revMenu(req, repo, currentName) { return readOnce(function (cb) { repo.getRefNames(function (err, refs) { if (err) return cb(err) cb(null, '') }) }) } function branchMenu(repo, name, currentName) { return cat([ pull.once('') ]) } /* Repo tree */ function renderRepoLatest(req, repo, rev) { return readOnce(function (cb) { repo.getCommitParsed(rev, function (err, commit) { if (err) return cb(err) var commitPath = [repo.id, 'commit', commit.id] cb(null, req._t('Latest') + ': ' + '' + link(commitPath, commit.title) + '
    ' + '' + commit.id + '
    ' + req._t('CommittedOn', { name: escapeHTML(commit.committer.name), date: commit.committer.date.toLocaleString(req._locale) }) + (commit.separateAuthor ? '
    ' + req._t('AuthoredOn', { name: escapeHTML(commit.author.name), date: commit.author.date.toLocaleString(req._locale) }) : '')) }) }) } // breadcrumbs function linkPath(basePath, path) { path = path.slice() var last = path.pop() return path.map(function (dir, i) { return link(basePath.concat(path.slice(0, i+1)), dir) }).concat(last).join(' / ') } function renderRepoTree(req, repo, rev, path) { var pathLinks = path.length === 0 ? '' : ': ' + linkPath([repo.id, 'tree'], [rev].concat(path)) return cat([ pull.once('

    ' + req._t('Files') + pathLinks + '

    '), pull( repo.readDir(rev, path), pull.map(function (file) { var type = (file.mode === 040000) ? 'tree' : (file.mode === 0160000) ? 'commit' : 'blob' if (type == 'commit') return [ '🖈', '' + escapeHTML(file.name) + ''] var filePath = [repo.id, type, rev].concat(path, file.name) return ['' + (type == 'tree' ? '📁' : '📄') + '', link(filePath, file.name)] }), table('class="files"') ) ]) } /* Repo readme */ function renderRepoReadme(req, repo, branch, path) { return readNext(function (cb) { pull( repo.readDir(branch, path), pull.filter(function (file) { return /readme(\.|$)/i.test(file.name) }), pull.take(1), pull.collect(function (err, files) { if (err) return cb(null, pull.empty()) var file = files[0] if (!file) return cb(null, pull.once(path.length ? '' : '

    ' + req._t('NoReadme') + '

    ')) repo.getObjectFromAny(file.id, function (err, obj) { if (err) return cb(err) cb(null, cat([ pull.once('

    ' + escapeHTML(file.name) + '


    '), renderObjectData(obj, file.name, repo, branch, path), pull.once('
    ') ])) }) }) ) }) } /* Repo commit */ function serveRepoCommit(req, repo, rev) { return renderRepoPage(req, repo, null, rev, cat([ readNext(function (cb) { repo.getCommitParsed(rev, function (err, commit) { if (err) return cb(err) var commitPath = [repo.id, 'commit', commit.id] var treePath = [repo.id, 'tree', commit.id] cb(null, cat([pull.once( '

    ' + link(commitPath, req._t('CommitRev', {rev: rev})) + '

    ' + '
    ' + '
    ' + link(treePath, req._t('BrowseFiles')) + '
    ' + '

    ' + linkify(escapeHTML(commit.title)) + '

    ' + (commit.body ? linkify(pre(commit.body)) : '') + (commit.separateAuthor ? req._t('AuthoredOn', { name: escapeHTML(commit.author.name), date: commit.author.date.toLocaleString(req._locale) }) + '
    ' : '') + req._t('CommittedOn', { name: escapeHTML(commit.committer.name), date: commit.committer.date.toLocaleString(req._locale) }) + '
    ' + commit.parents.map(function (id) { return req._t('Parent') + ': ' + link([repo.id, 'commit', id], id) }).join('
    ') + '
    ' + '

    ' + req._t('FilesChanged') + '

    '), // TODO: show diff from all parents (merge commits) renderDiffStat(req, [repo, repo], [commit.parents[0], commit.id]), pull.once('
    ') ])) }) }) ])) } /* Diff stat */ function renderDiffStat(req, repos, treeIds) { if (treeIds.length == 0) treeIds = [null] var id = treeIds[0] var lastI = treeIds.length - 1 var oldTree = treeIds[0] var changedFiles = [] return cat([ pull( Repo.diffTrees(repos, treeIds, true), pull.map(function (item) { var filename = escapeHTML(item.filename = item.path.join('/')) var oldId = item.id && item.id[0] var newId = item.id && item.id[lastI] var oldMode = item.mode && item.mode[0] var newMode = item.mode && item.mode[lastI] var action = !oldId && newId ? req._t('action.added') : oldId && !newId ? req._t('action.deleted') : oldMode != newMode ? req._t('action.changedMode', { old: oldMode.toString(8), new: newMode.toString(8) }) : req._t('changed') if (item.id) changedFiles.push(item) var blobsPath = item.id[1] ? [repos[1].id, 'blob', treeIds[1]] : [repos[0].id, 'blob', treeIds[0]] var rawsPath = item.id[1] ? [repos[1].id, 'raw', treeIds[1]] : [repos[0].id, 'raw', treeIds[0]] item.blobPath = blobsPath.concat(item.path) item.rawPath = rawsPath.concat(item.path) var fileHref = item.id ? '#' + encodeURIComponent(item.path.join('/')) : encodeLink(item.blobPath) return ['' + filename + '', action] }), table() ), pull( pull.values(changedFiles), paramap(function (item, cb) { var extension = getExtension(item.filename) if (extension in imgMimes) { var filename = escapeHTML(item.filename) return cb(null, '
    ' +
                  '' +
                  '' +
                  '
    ' + filename + '
    ' + filename + '
    ') } var done = multicb({ pluck: 1, spread: true }) getRepoObjectString(repos[0], item.id[0], done()) getRepoObjectString(repos[1], item.id[lastI], done()) done(function (err, strOld, strNew) { if (err) return cb(err) cb(null, htmlLineDiff(req, item.filename, item.filename, strOld, strNew, encodeLink(item.blobPath))) }) }, 4) ) ]) } function htmlLineDiff(req, filename, anchor, oldStr, newStr, blobHref) { var diff = JsDiff.structuredPatch('', '', oldStr, newStr) var groups = diff.hunks.map(function (hunk) { var oldLine = hunk.oldStart var newLine = hunk.newStart var header = '' + '@@ -' + oldLine + ',' + hunk.oldLines + ' ' + '+' + newLine + ',' + hunk.newLines + ' @@' + '' return [header].concat(hunk.lines.map(function (line) { var s = line[0] if (s == '\\') return var html = highlight(line, getExtension(filename)) var trClass = s == '+' ? 'diff-new' : s == '-' ? 'diff-old' : '' var lineNums = [s == '+' ? '' : oldLine++, s == '-' ? '' : newLine++] var id = [filename].concat(lineNums).join('-') return '' + lineNums.map(function (num) { return '' + (num ? '' + num + '' : '') + '' }).join('') + '' + html + '' })) }) return '
    ' +
          '' +
          [].concat.apply([], groups).join('') +
          '
    ' + filename + '' + '' + req._t('View') + ' ' + '
    ' } /* An unknown message linking to a repo */ function serveRepoSomething(req, repo, id, msg, path) { return renderRepoPage(req, repo, null, null, pull.once('

    ' + link([id]) + '

    ' + json(msg) + '
    ')) } /* Repo update */ function objsArr(objs) { return Array.isArray(objs) ? objs : Object.keys(objs).map(function (sha1) { var obj = Object.create(objs[sha1]) obj.sha1 = sha1 return obj }) } function serveRepoUpdate(req, repo, id, msg, path) { var raw = req._u.query.raw != null if (raw) return renderRepoPage(req, repo, 'activity', null, pull.once( '' + req._t('Info') + '' + '

    ' + req._t('Update') + '

    ' + '
    ' + json({key: id, value: msg}) + '
    ')) // convert packs to old single-object style if (msg.content.indexes) { for (var i = 0; i < msg.content.indexes.length; i++) { msg.content.packs[i] = { pack: {link: msg.content.packs[i].link}, idx: msg.content.indexes[i] } } } return renderRepoPage(req, repo, 'activity', null, cat([ pull.values([ '' + req._t('Data') + '', '

    ' + req._t('Update') + '

    ', renderRepoUpdate(req, repo, {key: id, value: msg}, true) ].concat(msg.content.objects ? ['

    ' + req._t('Objects') + '

    '].concat( objsArr(msg.content.objects).map(renderObject.bind(null, req))) : [], msg.content.packs ? [ '

    ' + req._t('Packs') + '

    ' ].concat(msg.content.packs.map(renderPack.bind(null, req))) : [])), msg.content.packs && cat([ pull.once('

    ' + req._t('Commits') + '

    '), pull( pull.values(msg.content.packs), pull.asyncMap(function (pack, cb) { var done = multicb({ pluck: 1, spread: true }) getBlob(req, pack.pack.link, done()) getBlob(req, pack.idx.link, done()) done(function (err, readPack, readIdx) { if (err) return cb(renderError(err)) cb(null, gitPack.decodeWithIndex(repo, readPack, readIdx)) }) }), pull.flatten(), pull.asyncMap(function (obj, cb) { if (obj.type == 'commit') Repo.getCommitParsed(obj, cb) else pull(obj.read, pull.drain(null, cb)) }), pull.filter(), pull.map(function (commit) { return renderCommit(req, repo, commit) }) ) ]) ])) } function renderObject(req, obj) { return '
    ' + obj.type + ' ' + link([obj.link], obj.sha1) + '
    ' + req._t('NumBytes', obj.length) + '
    ' } function renderPack(req, info) { return '
    ' + (info.pack ? req._t('Pack') + ': ' + link([info.pack.link]) + '
    ' : '') + (info.idx ? req._t('Index') + ': ' + link([info.idx.link]) : '') + '
    ' } /* Blob */ function serveRepoBlob(req, repo, rev, path) { return readNext(function (cb) { repo.getFile(rev, path, function (err, object) { if (err) return cb(null, serveBlobNotFound(req, repo.id, err)) var type = repo.isCommitHash(rev) ? 'Tree' : 'Branch' var pathLinks = path.length === 0 ? '' : ': ' + linkPath([repo.id, 'tree'], [rev].concat(path)) var rawFilePath = [repo.id, 'raw', rev].concat(path) var dirPath = path.slice(0, path.length-1) var filename = path[path.length-1] var extension = getExtension(filename) cb(null, renderRepoPage(req, repo, 'code', rev, cat([ pull.once('
    ' + '

    ' + req._t(type) + ': ' + rev + ' '), revMenu(req, repo, rev), pull.once('

    '), type == 'Branch' && renderRepoLatest(req, repo, rev), pull.once('
    ' + '

    ' + req._t('Files') + pathLinks + '

    ' + '
    ' + object.length + ' bytes' + '' + link(rawFilePath, req._t('Raw')) + '' + '
    ' + '
    '), extension in imgMimes ? pull.once('' + escapeHTML(filename) + '') : renderObjectData(object, filename, repo, rev, dirPath), pull.once('
    ') ]))) }) }) } function serveBlobNotFound(req, repoId, err) { return serveTemplate(req, req._t('error.BlobNotFound'), 404)(pull.once( '

    ' + req._t('error.BlobNotFound') + '

    ' + '

    ' + req._t('error.BlobNotFoundInRepo', { repo: link([repoId]) }) + '

    ' + '
    ' + escapeHTML(err.stack) + '
    ' )) } /* Raw blob */ function serveRepoRaw(req, repo, branch, path) { return readNext(function (cb) { repo.getFile(branch, path, function (err, object) { if (err) return cb(null, serveBuffer(404, req._t('error.BlobNotFound'))) var extension = getExtension(path[path.length-1]) var contentType = imgMimes[extension] cb(null, pull(object.read, serveRaw(object.length, contentType))) }) }) } function serveRaw(length, contentType) { var headers = { 'Content-Type': contentType || 'text/plain; charset=utf-8', 'Cache-Control': 'max-age=31536000' } if (length != null) headers['Content-Length'] = length return function (read) { return cat([pull.once([200, headers]), read]) } } function getBlob(req, key, cb) { ssb.blobs.want(key, function (err, got) { if (err) cb(err) else if (!got) cb(new Error(req._t('error.MissingBlob', {key: key}))) else cb(null, ssb.blobs.get(key)) }) } function serveBlob(req, key) { return readNext(function (cb) { getBlob(req, key, function (err, read) { if (err) cb(null, serveError(req, err)) else if (!read) cb(null, serve404(req)) else cb(null, serveRaw()(read)) }) }) } /* Digs */ function serveRepoDigs(req, repo) { return readNext(function (cb) { getVotes(repo.id, function (err, votes) { cb(null, renderRepoPage(req, repo, null, null, cat([ pull.once('

    ' + req._t('Digs') + '

    ' + '
    ' + req._t('Total') + ': ' + votes.upvotes + '
    '), pull( pull.values(Object.keys(votes.upvoters)), paramap(function (feedId, cb) { about.getName(feedId, function (err, name) { if (err) return cb(err) cb(null, link([feedId], name)) }) }, 8), ul() ), pull.once('
    ') ]))) }) }) } /* Forks */ function getForks(repo, includeSelf) { return pull( cat([ includeSelf && readOnce(function (cb) { getMsg(repo.id, function (err, value) { cb(err, value && {key: repo.id, value: value}) }) }), ssb.links({ dest: repo.id, values: true, rel: 'upstream' }) ]), pull.filter(function (msg) { return msg.value.content && msg.value.content.type == 'git-repo' }), paramap(function (msg, cb) { getRepoFullName(about, msg.value.author, msg.key, function (err, repoName, authorName) { if (err) return cb(err) cb(null, { key: msg.key, value: msg.value, repoName: repoName, authorName: authorName }) }) }, 8) ) } function serveRepoForks(req, repo) { var hasForks return renderRepoPage(req, repo, null, null, cat([ pull.once('

    ' + req._t('Forks') + '

    '), pull( getForks(repo), pull.map(function (msg) { hasForks = true return '
    ' + link([msg.value.author], msg.authorName) + ' / ' + link([msg.key], msg.repoName) + '' + timestamp(msg.value.timestamp, req) + '
    ' }) ), readOnce(function (cb) { cb(null, hasForks ? '' : req._t('NoForks')) }) ])) } function serveRepoForkPrompt(req, repo) { return renderRepoPage(req, repo, null, null, pull.once( '
    ' + '

    ' + req._t('ForkRepoPrompt') + '

    ' + '

    ' + hiddenInputs({ id: repo.id }) + '' + ' ' + '

    ' )) } /* Issues */ function serveRepoIssues(req, repo, path) { var numIssues = 0 var state = req._u.query.state || 'open' return renderRepoPage(req, repo, 'issues', null, cat([ pull.once( (isPublic ? '' : '
    ' + '' + '
    ') + '

    ' + req._t('Issues') + '

    ' + nav([ ['?', req._t('state.Open'), 'open'], ['?state=closed', req._t('state.Closed'), 'closed'], ['?state=all', req._t('state.All'), 'all'] ], state)), pull( issues.createFeedStream({ project: repo.id }), pull.filter(function (issue) { return state == 'all' ? true : (state == 'closed') == !issue.open }), pull.map(function (issue) { numIssues++ var state = (issue.open ? 'open' : 'closed') var stateStr = req._t(issue.open ? 'state.Open' : 'state.Closed') return '
    ' + ' ' + '' + escapeHTML(issue.title) + '' + new Date(issue.created_at).toLocaleString(req._locale) + '' + '' + '
    ' }) ), readOnce(function (cb) { cb(null, numIssues > 0 ? '' : '

    ' + req._t('NoIssues') + '

    ') }) ])) } /* Pull Requests */ function serveRepoPullReqs(req, repo) { var count = 0 var state = req._u.query.state || 'open' return renderRepoPage(req, repo, 'pulls', null, cat([ pull.once( (isPublic ? '' : '
    ' + '' + '
    ') + '

    ' + req._t('PullRequests') + '

    ' + nav([ ['?', req._t('state.Open'), 'open'], ['?state=closed', req._t('state.Closed'), 'closed'], ['?state=all', req._t('state.All'), 'all'] ], state)), pull( pullReqs.list({ repo: repo.id, open: {open: true, closed: false}[state] }), pull.map(function (issue) { count++ var state = (issue.open ? 'open' : 'closed') var stateStr = req._t(issue.open ? 'state.Open' : 'state.Closed') return '
    ' + ' ' + '' + escapeHTML(issue.title) + '' + new Date(issue.created_at).toLocaleString(req._locale) + '' + '' + '
    ' }) ), readOnce(function (cb) { cb(null, count > 0 ? '' : '

    ' + req._t('NoPullRequests') + '

    ') }) ])) } /* New Issue */ function serveRepoNewIssue(req, repo, issueId, path) { return renderRepoPage(req, repo, 'issues', null, pull.once( '

    ' + req._t('issue.New') + '

    ' + '
    ' + '' + '

    ' + renderPostForm(req, repo, req._t('Description'), 8) + '' + '
    ')) } /* Issue */ function serveRepoIssue(req, repo, issue, path, postId) { var isAuthor = (myId == issue.author) || (myId == repo.feed) var newestMsg = {key: issue.id, value: {timestamp: issue.created_at}} return renderRepoPage(req, repo, 'issues', null, cat([ pull.once( renderNameForm(req, !isPublic, issue.id, issue.title, 'issue-title', null, req._t('issue.Rename'), '

    ' + link([issue.id], issue.title) + '

    ') + '' + issue.id + '' + '
    ' + (issue.open ? '' + req._t('state.Open') + '' : '' + req._t('state.Closed') + '')), readOnce(function (cb) { about.getName(issue.author, function (err, authorName) { if (err) return cb(err) var authorLink = link([issue.author], authorName) cb(null, req._t('issue.Opened', {name: authorLink, datetime: timestamp(issue.created_at, req)})) }) }), pull.once('
    ' + markdown(issue.text, repo) + '
    '), // render posts and edits pull( ssb.links({ dest: issue.id, values: true }), pull.unique('key'), addAuthorName(about), sortMsgs(), pull.through(function (msg) { if (msg.value.timestamp > newestMsg.value.timestamp) newestMsg = msg }), pull.map(renderIssueActivityMsg.bind(null, req, repo, issue, req._t('issue.'), postId)) ), isPublic ? pull.empty() : readOnce(function (cb) { cb(null, renderIssueCommentForm(req, issue, repo, newestMsg.key, isAuthor, req._t('issue.'))) }) ])) } function renderIssueActivityMsg(req, repo, issue, type, postId, msg) { var authorLink = link([msg.value.author], msg.authorName) var msgHref = encodeLink(msg.key) + '#' + encodeURIComponent(msg.key) var msgTimeLink = '' + new Date(msg.value.timestamp).toLocaleString(req._locale) + '' var c = msg.value.content switch (c.type) { case 'post': if (c.root == issue.id) { var changed = issues.isStatusChanged(msg, issue) return '
    ' + (msg.key == postId ? '
    ' : '') + '' + msg.key + ' ' + (changed == null ? authorLink : req._t( changed ? 'issue.Reopened' : 'issue.Closed', {name: authorLink, type: type})) + ' · ' + msgTimeLink + (msg.key == postId ? '
    ' : '') + markdown(c.text, repo) + '
    ' } else { var text = c.text || (c.type + ' ' + msg.key) return '
    ' + req._t('issue.MentionedIn', { name: authorLink, type: type, post: '' + String(text).substr(0, 140) + '' }) + '
    ' } case 'issue': case 'pull-request': return '
    ' + req._t('issue.MentionedIn', { name: authorLink, type: type, post: link([msg.key], String(c.title || msg.key).substr(0, 140)) }) + '
    ' case 'issue-edit': return '
    ' + (msg.key == postId ? '
    ' : '') + (c.title == null ? '' : req._t('issue.Renamed', { name: authorLink, type: type, name: '' + escapeHTML(c.title) + '' })) + ' · ' + msgTimeLink + (msg.key == postId ? '
    ' : '') + '
    ' case 'git-update': var mention = issues.getMention(msg, issue) if (mention) { var commitLink = link([repo.id, 'commit', mention.object], mention.label || mention.object) return '
    ' + req._t(mention.open ? 'issue.Reopened' : 'issue.Closed', { name: authorLink, type: type }) + ' · ' + msgTimeLink + '
    ' + commitLink + '
    ' } else if ((mention = getMention(msg, issue.id))) { var commitLink = link(mention.object ? [repo.id, 'commit', mention.object] : [msg.key], mention.label || mention.object || msg.key) return '
    ' + req._t('issue.Mentioned', { name: authorLink, type: type }) + ' · ' + msgTimeLink + '
    ' + commitLink + '
    ' } else { // fallthrough } default: return '
    ' + authorLink + ' · ' + msgTimeLink + json(c) + '
    ' } } function renderIssueCommentForm(req, issue, repo, branch, isAuthor, type) { return '
    ' + '' + '' + '' + '' + '' + renderPostForm(req, repo) + '' + (isAuthor ? '' : '') + '
    ' } /* Pull Request */ function serveRepoPullReq(req, repo, pr, path, postId) { var headRepo, authorLink var page = path[0] || 'activity' return renderRepoPage(req, repo, 'pulls', null, cat([ pull.once('
    ' + renderNameForm(req, !isPublic, pr.id, pr.title, 'issue-title', null, req._t('pullRequest.Rename'), '

    ' + link([pr.id], pr.title) + '

    ') + '' + pr.id + ''), readOnce(function (cb) { var done = multicb({ pluck: 1, spread: true }) var gotHeadRepo = done() about.getName(pr.author, done()) var sameRepo = (pr.headRepo == pr.baseRepo) getRepo(pr.headRepo, function (err, headRepo) { if (err) return cb(err) getRepoName(about, headRepo.feed, headRepo.id, done()) about.getName(headRepo.feed, done()) gotHeadRepo(null, Repo(headRepo)) }) done(function (err, _headRepo, issueAuthorName, headRepoName, headRepoAuthorName) { if (err) return cb(err) headRepo = _headRepo authorLink = link([pr.author], issueAuthorName) var repoLink = link([pr.headRepo], headRepoName) var headRepoAuthorLink = link([headRepo.feed], headRepoAuthorName) var headRepoLink = link([headRepo.id], headRepoName) var headBranchLink = link([headRepo.id, 'tree', pr.headBranch]) var baseBranchLink = link([repo.id, 'tree', pr.baseBranch]) cb(null, '
    ' + '' + req._t(pr.open ? 'state.Open' : 'state.Closed') + ' ' + req._t('pullRequest.WantToMerge', { name: authorLink, base: '' + baseBranchLink + '', head: (sameRepo ? '' + headBranchLink + '' : '' + headRepoAuthorLink + ' / ' + headRepoLink + ' / ' + headBranchLink + '') }) + '
    ') }) }), pull.once( nav([ [[pr.id], req._t('Discussion'), 'activity'], [[pr.id, 'commits'], req._t('Commits'), 'commits'], [[pr.id, 'files'], req._t('Files'), 'files'] ], page)), readNext(function (cb) { if (page == 'commits') cb(null, renderPullReqCommits(req, pr, repo, headRepo)) else if (page == 'files') cb(null, renderPullReqFiles(req, pr, repo, headRepo)) else cb(null, renderPullReqActivity(req, pr, repo, headRepo, authorLink, postId)) }) ])) } function renderPullReqCommits(req, pr, baseRepo, headRepo) { return cat([ pull.once('
    '), renderCommitLog(req, baseRepo, pr.baseBranch, headRepo, pr.headBranch), pull.once('
    ') ]) } function renderPullReqFiles(req, pr, baseRepo, headRepo) { return cat([ pull.once('
    '), renderDiffStat(req, [baseRepo, headRepo], [pr.baseBranch, pr.headBranch]), pull.once('
    ') ]) } function renderPullReqActivity(req, pr, repo, headRepo, authorLink, postId) { var msgTimeLink = link([pr.id], new Date(pr.created_at).toLocaleString(req._locale)) var newestMsg = {key: pr.id, value: {timestamp: pr.created_at}} var isAuthor = (myId == pr.author) || (myId == repo.feed) return cat([ readOnce(function (cb) { cb(null, '
    ' + authorLink + ' · ' + msgTimeLink + markdown(pr.text, repo) + '
    ') }), // render posts, edits, and updates pull( many([ ssb.links({ dest: pr.id, values: true }), readNext(function (cb) { cb(null, pull( ssb.links({ dest: headRepo.id, source: headRepo.feed, rel: 'repo', values: true, reverse: true }), pull.take(function (link) { return link.value.timestamp > pr.created_at }), pull.filter(function (link) { return link.value.content.type == 'git-update' && ('refs/heads/' + pr.headBranch) in link.value.content.refs }) )) }) ]), addAuthorName(about), pull.unique('key'), pull.through(function (msg) { if (msg.value.timestamp > newestMsg.value.timestamp) newestMsg = msg }), sortMsgs(), pull.map(function (item) { if (item.value.content.type == 'git-update') return renderBranchUpdate(req, pr, item) return renderIssueActivityMsg(req, repo, pr, req._t('pull request'), postId, item) }) ), !isPublic && isAuthor && pr.open && pull.once( '
    ' + '' + '

    ' + '
    ' + '

    ' + req._t('mergeInstructions.CheckOut') + '

    ' + '
    ' +
            'git fetch ssb://' + escapeHTML(pr.headRepo) + ' ' +
              escapeHTML(pr.headBranch) + '\n' +
            'git checkout -b ' + escapeHTML(pr.headBranch) + ' FETCH_HEAD' +
            '
    ' + '

    ' + req._t('mergeInstructions.MergeAndPush') + '

    ' + '
    ' +
            'git checkout ' + escapeHTML(pr.baseBranch) + '\n' +
            'git merge ' + escapeHTML(pr.headBranch) + '\n' +
            'git push ssb ' + escapeHTML(pr.baseBranch) +
            '
    ' + '
    '), !isPublic && readOnce(function (cb) { cb(null, renderIssueCommentForm(req, pr, repo, newestMsg.key, isAuthor, req._t('pull request'))) }) ]) } function renderBranchUpdate(req, pr, msg) { var authorLink = link([msg.value.author], msg.authorName) var msgLink = link([msg.key], new Date(msg.value.timestamp).toLocaleString(req._locale)) var rev = msg.value.content.refs['refs/heads/' + pr.headBranch] if (!rev) return '
    ' + req._t('NameDeletedBranch', { name: authorLink, branch: '' + pr.headBranch + '' }) + ' · ' + msgLink + '
    ' var revLink = link([pr.headRepo, 'commit', rev], rev.substr(0, 8)) return '
    ' + req._t('NameUpdatedBranch', { name: authorLink, rev: '' + revLink + '' }) + ' · ' + msgLink + '
    ' } /* Compare changes */ function serveRepoCompare(req, repo) { var query = req._u.query var base var count = 0 return renderRepoPage(req, repo, 'pulls', null, cat([ pull.once('

    ' + req._t('CompareChanges') + '

    ' + '
    ' + '
    '), pull.once(req._t('BaseBranch') + ': '), readNext(function (cb) { if (query.base) gotBase(null, query.base) else repo.getSymRef('HEAD', true, gotBase) function gotBase(err, ref) { if (err) return cb(err) cb(null, branchMenu(repo, 'base', base = ref || 'HEAD')) } }), pull.once('
    ' + req._t('ComparisonRepoBranch') + ':'), pull( getForks(repo, true), pull.asyncMap(function (msg, cb) { getRepo(msg.key, function (err, repo) { if (err) return cb(err) cb(null, { msg: msg, repo: repo }) }) }), pull.map(renderFork), pull.flatten() ), pull.once('
    '), readOnce(function (cb) { cb(null, count == 0 ? req._t('NoBranches') : '') }), pull.once('
    ') ])) function renderFork(fork) { return pull( fork.repo.refs(), pull.map(function (ref) { var m = /^refs\/([^\/]*)\/(.*)$/.exec(ref.name) || [,ref.name] return { type: m[1], name: m[2], value: ref.value } }), pull.filter(function (ref) { return ref.type == 'heads' && !(ref.name == base && fork.msg.key == repo.id) }), pull.map(function (ref) { var branchLink = link([fork.msg.key, 'tree', ref.name], ref.name) var authorLink = link([fork.msg.value.author], fork.msg.authorName) var repoLink = link([fork.msg.key], fork.msg.repoName) var value = fork.msg.key + ':' + ref.name count++ return '
    ' + ' ' + authorLink + ' / ' + repoLink + ' / ' + branchLink + '
    ' }) ) } } function serveRepoComparing(req, repo) { var query = req._u.query var baseBranch = query.base var s = (query.head || '').split(':') if (!s || !baseBranch) return serveRedirect(req, encodeLink([repo.id, 'compare'])) var headRepoId = s[0] var headBranch = s[1] var baseLink = link([repo.id, 'tree', baseBranch]) var headBranchLink = link([headRepoId, 'tree', headBranch]) var backHref = encodeLink([repo.id, 'compare']) + req._u.search return renderRepoPage(req, repo, 'pulls', null, cat([ pull.once('

    ' + req._t(query.expand ? 'OpenPullRequest': 'ComparingChanges') + '

    '), readNext(function (cb) { getRepo(headRepoId, function (err, headRepo) { if (err) return cb(err) getRepoFullName(about, headRepo.feed, headRepo.id, function (err, repoName, authorName) { if (err) return cb(err) cb(null, renderRepoInfo(Repo(headRepo), repoName, authorName)) } ) }) }) ])) function renderRepoInfo(headRepo, headRepoName, headRepoAuthorName) { var authorLink = link([headRepo.feed], headRepoAuthorName) var repoLink = link([headRepoId], headRepoName) return cat([ pull.once('
    ' + req._t('Base') + ': ' + baseLink + '
    ' + req._t('Head') + ': ' + '' + authorLink + ' / ' + repoLink + ' / ' + headBranchLink + '' + '
    ' + (query.expand ? '
    ' + hiddenInputs({ action: 'new-pull', branch: baseBranch, head_repo: headRepoId, head_branch: headBranch }) + '' + renderPostForm(req, repo, req._t('Description'), 8) + '' + '
    ' : '
    ' + hiddenInputs({ base: baseBranch, head: query.head }) + ' ' + '' + req._t('Back') + '' + '
    ') + '
    ' + '' + '
    '), renderDiffStat(req, [repo, headRepo], [baseBranch, headBranch]), pull.once('
    ' + '
    '), renderCommitLog(req, repo, baseBranch, headRepo, headBranch), pull.once('
    ') ]) } } function renderCommitLog(req, baseRepo, baseBranch, headRepo, headBranch) { return cat([ pull.once(''), readNext(function (cb) { baseRepo.resolveRef(baseBranch, function (err, baseBranchRev) { if (err) return cb(err) var currentDay return cb(null, pull( headRepo.readLog(headBranch), pull.take(function (rev) { return rev != baseBranchRev }), pullReverse(), paramap(headRepo.getCommitParsed.bind(headRepo), 8), pull.map(function (commit) { var commitPath = [headRepo.id, 'commit', commit.id] var commitIdShort = '' + commit.id.substr(0, 8) + '' var day = Math.floor(commit.author.date / 86400000) var dateRow = day == currentDay ? '' : '' currentDay = day return dateRow + '' + '' + '' + '' + '' }) )) }) }), pull.once('
    ' + commit.author.date.toLocaleDateString(req._locale) + '
    ' + escapeHTML(commit.author.name) + '' + link(commitPath, commit.title) + '' + link(commitPath, commitIdShort, true) + '
    ') ]) } }