var url = require('url') var pull = require('pull-stream') var once = pull.once var cat = require('pull-cat') var paramap = require('pull-paramap') var multicb = require('multicb') var JsDiff = require('diff') var GitRepo = require('pull-git-repo') var gitPack = require('pull-git-pack') var u = require('../util') var paginate = require('pull-paginate') var markdown = require('../markdown') var forms = require('../forms') var ssbRef = require('ssb-ref') var zlib = require('zlib') var toPull = require('stream-to-pull-stream') var h = require('pull-hyperscript') var getObjectMsgId = require('../../lib/obj-msg-id') function extend(obj, props) { for (var k in props) obj[k] = props[k] return obj } module.exports = function (web) { return new RepoRoutes(web) } function RepoRoutes(web) { this.web = web this.issues = require('./issues')(this, web) this.pulls = require('./pulls')(this, web) } var R = RepoRoutes.prototype function getRepoObjectString(repo, id, mode, cb) { if (!id) return cb(null, '') if (mode == 0160000) return cb(null, 'Subproject commit ' + id) repo.getObjectFromAny(id, function (err, obj) { if (err) return cb(err) u.readObjectString(obj, cb) }) } /* Repo */ R.getLineCommentThreads = function (req, repo, updateId, commitId, filename, cb) { var self = this var sbot = self.web.ssb var lineCommentThreads = {} pull( sbot.backlinks ? sbot.backlinks.read({ query: [ {$filter: { dest: updateId, value: { content: { type: 'line-comment', repo: repo.id, updateId: updateId, commitId: commitId, filePath: filename } } }} ] }) : pull( sbot.links({ dest: updateId, rel: 'updateId', values: true }), pull.filter(function (msg) { var c = msg && msg.value && msg.value.content return c && c.type === 'line-comment' && c.updateId === updateId && c.commitId === commitId && c.filePath === filename }) ), paramap(function (msg, cb) { pull( self.renderThread(req, repo, msg), pull.collect(function (err, parts) { if (err) return cb(err) cb(null, { line: msg.value.content.line, html: parts.join(''), }) }) ) }, 4), pull.drain(function (thread) { lineCommentThreads[thread.line] = thread.html }, function (err) { if (err) return cb(err) cb(null, lineCommentThreads) }) ) } R.renderThread = function (req, repo, msg) { var newestMsg = msg var root = msg.key var self = this return h('div', [ pull( cat([ pull.once(msg), self.web.ssb.backlinks ? self.web.ssb.backlinks.read({ query: [ {$filter: { dest: root }} ] }) : self.web.ssb.links({ dest: root, values: true }), ]), pull.unique('key'), u.decryptMessages(self.web.ssb), u.readableMessages(), self.web.addAuthorName(), u.sortMsgs(), pull.filter(function (msg) { var c = msg && msg.value && msg.value.content return c && ( (c.type === 'post' && c.root === root) || msg.key === root) }), pull.through(function (msg) { // TODO: correctly calculate the thread branches if (msg.value && msg.value.timestamp > newestMsg.value.timestamp) newestMsg = msg }), pull.map(function (msg) { return self.renderLineComment(req, repo, msg) }) ), self.web.isPublic ? '' : pull.once(forms.lineCommentReply(req, root, newestMsg.key)) ]) } R.renderLineComment = function (req, repo, msg) { var c = msg && msg.value && msg.value.content var id = u.msgIdToDomId(msg.key) return h('section', {class: 'collapse', id: id}, [ h('div', [ u.link([msg.value.author], msg.authorName), ' ', h('tt', {class: 'right-bar item-id'}, msg.key), ' · ', h('a', {href: u.encodeLink(msg.key) + '#' + id}, new Date(msg.value.timestamp).toLocaleString(req._locale)), ]), markdown(c.text, repo) ]) } R.serveRepoPage = function (req, repo, path) { var self = this 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 = u.encodeLink([repo.id].concat(path)) delete req._u.query.rev delete req._u.search return self.web.serveRedirect(req, url.format(req._u)) } // get branch return path[1] ? R_serveRepoPage2.call(self, req, repo, path) : u.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, R_serveRepoPage2.call(self, req, repo, path)) }) }) }) } function R_serveRepoPage2(req, repo, path) { var branch = path[1] var filePath = path.slice(2) switch (path[0]) { case undefined: case '': return this.serveRepoTree(req, repo, branch, []) case 'activity': return this.serveRepoActivity(req, repo, branch) case 'commits': return this.serveRepoCommits(req, repo, branch) case 'commit': return this.serveRepoCommit(req, repo, path[1], filePath) case 'tag': return this.serveRepoTag(req, repo, branch, filePath) case 'tree': return this.serveRepoTree(req, repo, branch, filePath) case 'blob': return this.serveRepoBlob(req, repo, branch, filePath) case 'raw': return this.serveRepoRaw(req, repo, branch, filePath) case 'digs': return this.serveRepoDigs(req, repo) case 'fork': return this.serveRepoForkPrompt(req, repo) case 'forks': return this.serveRepoForks(req, repo) case 'issues': switch (path[1]) { case 'new': if (filePath.length == 0) return this.issues.serveRepoNewIssue(req, repo) break default: return this.issues.serveRepoIssues(req, repo, false) } case 'pulls': return this.issues.serveRepoIssues(req, repo, true) case 'compare': return this.pulls.serveRepoCompare(req, repo) case 'comparing': return this.pulls.serveRepoComparing(req, repo) case 'info': switch (path[1]) { case 'refs': return this.serveRepoRefs(req, repo) default: return this.web.serve404(req) } case 'objects': switch (path[1]) { case 'info': switch (path[2]) { case 'packs': return this.serveRepoPacksInfo(req, repo) default: return this.web.serve404(req) } case 'pack': return this.serveRepoPack(req, repo, filePath.join('/')) default: var hash = path[1] + path[2] if (hash.length === 40) { return this.serveRepoObject(req, repo, hash) } return this.web.serve404(req) } case 'HEAD': return this.serveRepoHead(req, repo) default: return this.web.serve404(req) } } R.serveRepoNotFound = function (req, id, err) { return this.web.serveTemplate(req, req._t('error.RepoNotFound'), 404) (pull.values([ '

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

', '

' + req._t('error.RepoIdNotFound', id) + '

', '
' + u.escape(err.stack) + '
' ])) } R.serveRepoTemplate = function (req, repo, page, branch, titleTemplate, body) { var self = this var digsPath = [repo.id, 'digs'] var done = multicb({ pluck: 1, spread: true }) self.web.getRepoName(repo.feed, repo.id, done()) self.web.about.getName(repo.feed, done()) self.web.getVotes(repo.id, done()) if (repo.upstream) { self.web.getRepoName(repo.upstream.feed, repo.upstream.id, done()) self.web.about.getName(repo.upstream.feed, done()) } return u.readNext(function (cb) { done(function (err, repoName, authorName, votes, upstreamName, upstreamAuthorName) { if (err) return cb(null, self.web.serveError(req, err)) var upvoted = votes.upvoters[self.web.myId] > 0 var upstreamLink = !repo.upstream ? '' : u.link([repo.upstream]) var title = titleTemplate ? titleTemplate .replace(/%\{repo\}/g, repoName) .replace(/%\{author\}/g, authorName) : (authorName ? authorName + '/' : '') + repoName var isPublic = self.web.isPublic var isLocal = !isPublic cb(null, self.web.serveTemplate(req, title)(cat([ h('div', {class: 'repo-title'}, [ h('form', {class: 'right-bar', action: '', method: 'post'}, [ h('strong', {class: 'ml2 mr1'}, u.link(digsPath, votes.upvotes)), h('button', extend( {class: 'btn', name: 'action', value: 'vote'}, isPublic ? {disabled: 'disabled'} : {type: 'submit'} ), [ h('i', '✌ '), h('span', req._t(isLocal && upvoted ? 'Undig' : 'Dig')) ] ), u.when(isLocal, () => cat([ h('input', {type: 'hidden', name: 'value', value: (upvoted ? '0' : '1')}), h('input', {type: 'hidden', name: 'id', value: u.escape(repo.id)}) ])), h('a', {href: u.encodeLink([repo.id, 'forks']), title: req._t('Forks'), class: 'ml2 mr1'}, '+'), u.when(isLocal, () => h('button', {class: 'btn', type: 'submit', name: 'action', value: 'fork-prompt'}, [ h('i', '⑂ '), once(req._t('Fork')) ]) ) ]), forms.name(req, isLocal, repo.id, repoName, 'repo-name', null, req._t('repo.Rename'), h('h2', {class: 'bgslash'}, (authorName ? u.link([repo.feed], authorName, false, 'class="repo-author"') + ' / ' : '') + u.link([repo.id], repoName) + (repo.private ? ' ' + u.privateIcon(req) : '')) ), ]), u.when(repo.upstream, () => h('small', {class: 'bgslash'}, req._t('ForkedFrom', { repo: `${u.link([repo.upstream.feed], upstreamAuthorName)} / ${u.link([repo.upstream.id], upstreamName)}` })) ), u.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'], self.web.indexCache ? req._t('IssuesN', { count: self.web.indexCache.getIssuesCount(repo.id, '…') }) : req._t('Issues'), 'issues'], [[repo.id, 'pulls'], self.web.indexCache ? req._t('PullRequestsN', { count: self.web.indexCache.getPRsCount(repo.id, '…') }) : req._t('PullRequests'), 'pulls'] ], page), body ]) )) }) }) } R.renderEmptyRepo = function (req, repo) { if (repo.feed != this.web.myId) return h('section', [ h('h3', req._t('EmptyRepo')) ]) var gitUrl = 'ssb://' + repo.id return h('section', [ h('h3', req._t('initRepo.GettingStarted')), h('h4', req._t('initRepo.CreateNew')), preInitRepo(req, gitUrl), h('h4', req._t('initRepo.PushExisting')), preRemote(gitUrl) ]) } var preInitRepo = (req, gitUrl) => h('pre', `touch ${req._t('initRepo.README')}.md git init git add ${req._t('initRepo.README')}.md git commit -m ${req._t('initRepo.InitialCommit')} git remote add origin ${gitUrl} git push -u origin master`) var preRemote = (gitUrl) => h('pre', `git remote add origin ${gitUrl} git push -u origin master`) R.serveRepoTree = function (req, repo, rev, path) { var type = repo.isCommitHash(rev) ? 'Tree' : 'Branch' var title = (path.length ? `${path.join('/')} · ` : '') + '%{author}/%{repo}' + (repo.head == `refs/heads/${rev}` ? '' : `@${rev}`) var gitUrl = 'ssb://' + repo.id var host = req.headers.host || '127.0.0.1:7718' var targetpath = '/' + encodeURIComponent(repo.id) var httpUrl = 'http://' + encodeURI(host) + targetpath var cloneUrls = '
' + '' + '
' + '' + '' + '
' + '
' return this.serveRepoTemplate(req, repo, 'code', rev, title, u.readNext((cb) => { if (!rev) return cb(null, this.renderEmptyRepo(req, repo)) repo.getLatestAvailableRev(rev, 10e3, (err, revGot, numSkipped) => { if (err) return cb(err) cb(null, cat([ h('section', {class: 'branch-info light-grey', method: 'get'}, [ h('form', {action: '', method: 'get'}, [ h('div', {class: 'rev-menu-line'}, [ h('span', `${req._t(type)}: `), this.revMenu(req, repo, rev) ]), cloneUrls ]), u.when(numSkipped > 0, () => h('div', {class: 'missing-blobs-warning mt2'}, h('em', req._t('missingBlobsWarning', numSkipped)) ) ) ]), h('section', {class: 'files'}, renderRepoTree(req, repo, revGot, path, type)), this.renderRepoReadme(req, repo, revGot, path) ])) }) })) } /* Repo activity */ R.serveRepoActivity = function (req, repo, branch) { var self = this var title = req._t('Activity') + ' · %{author}/%{repo}' return self.serveRepoTemplate(req, repo, 'activity', branch, title, cat([ h('h3', req._t('Activity')), pull( self.web.ssb.backlinks ? self.web.ssb.backlinks.read({ query: [ {$filter: { dest: repo.id, value: { content: { repo: repo.id } } }} ] }) : self.web.ssb.links({ dest: repo.id, rel: 'repo', values: true }), pull.unique('key'), u.decryptMessages(self.web.ssb), u.sortMsgs(true), pull.asyncMap(renderRepoUpdate.bind(self, req, repo, false)) ), u.readOnce(function (cb) { var done = multicb({ pluck: 1, spread: true }) self.web.about.getName(repo.feed, done()) self.web.getMsg(repo.id, done()) done(function (err, authorName, msg) { if (err) return cb(err) self.web.renderFeedItem(req, { key: repo.id, value: msg.value, authorName: authorName }, cb) }) }) ])) } function renderRepoUpdate(req, repo, full, msg, cb) { var c = msg.value.content if (c.type != 'git-update') { return cb(null, '') // return renderFeedItem(msg, cb) // TODO: render post, issue, pull-request } var branches = [] var tags = [] if (c.refs) for (var name in c.refs) { var m = name.match(/^refs\/(heads|tags)\/(.*)$/) || [,, name] ;(m[1] == 'tags' ? tags : branches) .push({name: m[2], value: c.refs[name]}) } var numObjects = c.objects ? Object.keys(c.objects).length : 0 var dateStr = new Date(msg.value.timestamp).toLocaleString(req._locale) this.web.about.getName(msg.value.author, function (err, name) { if (err) return cb(err) cb(null, '
' + u.link([msg.key], dateStr) + '
' + u.link([msg.value.author], name) + '
' + branches.map(function (update) { if (!update.value) { return '' + u.escape(update.name) + '
' } else { var commitLink = u.link([repo.id, 'commit', update.value]) var branchLink = u.link([repo.id, 'tree', update.name]) return branchLink + ' → ' + commitLink + '
' } }).join('') + tags.map(function (update) { return update.value ? u.link([repo.id, 'tag', update.value], update.name) : '' + u.escape(update.name) + '' }).join(', ') + '
') }) } /* Repo commits */ R.serveRepoCommits = function (req, repo, branch) { var query = req._u.query var title = req._t('Commits') + ' · %{author}/%{repo}' return this.serveRepoTemplate(req, repo, 'commits', branch, title, 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 '
' + '' + u.link(commitPath, commit.title) + '
' + '' + commit.id + ' ' + u.link(treePath, req._t('Tree')) + '
' + u.escape(commit.author.name) + ' · ' + commit.author.date.toLocaleString(req._locale) + (commit.separateAuthor ? '
' + req._t('CommittedOn', { name: u.escape(commit.committer.name), date: commit.committer.date.toLocaleString(req._locale) }) : '') + '
' } /* Branch menu */ R.formatRevOptions = function (currentName) { return function (name) { var htmlName = u.escape(name) return '' } } R.formatRevType = function(req, type) { return ( type == 'heads' ? req._t('Branches') : type == 'tags' ? req._t('Tags') : type) } R.revMenu = function (req, repo, currentName) { var self = this return u.readOnce(function (cb) { repo.getRefNames(function (err, refs) { if (err) return cb(err) cb(null, '') }) }) } /* Repo tree */ function renderRepoLatest(req, repo, rev) { if (!rev) return pull.empty() return u.readOnce(function (cb) { repo.getCommitParsed(rev, function (err, commit) { if (err) return cb(err) var commitPath = [repo.id, 'commit', commit.id] var actor = commit.separateAuthor ? 'author' : 'committer' var actionKey = actor.slice(0,1).toUpperCase() + actor.slice(1) + 'ReleasedCommit' cb(null, '' + req._t(actionKey, { name: u.escape(commit[actor].name), commitName: u.link(commitPath, commit.title) }) + '' + '' + req._t('LatestOn', { commitId: commit.id.slice(0, 7), date: commit[actor].date.toLocaleString(req._locale) }) + '' ) }) }) } // breadcrumbs function linkPath(basePath, path) { path = path.slice() var last = path.pop() return path.map(function (dir, i) { return u.link(basePath.concat(path.slice(0, i+1)), dir) }).concat(last).join(' / ') } function renderRepoTree(req, repo, rev, path, type) { var source = repo.readDir(rev,path) var pathLinks = path.length === 0 ? '' : ': ' + linkPath([repo.id, 'tree'], [rev].concat(path)) var location = once('') if (path.length !== 0) { var link = linkPath([repo.id, 'tree'], [rev].concat(path)) location = h('div', {class: 'fileLocation'}, `${req._t('Files')}: ${link}`) } return cat([ location, h('table', {class: "files w-100", cellspacing: "0"}, cat([ u.when(type === 'Branch', () => h('thead', h('tr', h('td', {colspan: '2'}, [ renderRepoLatest(req, repo, rev) ]))) ), u.sourceMap(source, file => h('tr', [ h('td', [ h('i', fileIcon(file)) ]), h('td', u.link(filePath(file), file.name)) ]) ) ])) ]) function fileIcon(file) { return fileType(file) === 'tree' ? '📁' : '📄' } function filePath(file) { var type = fileType(file) return [repo.id, type, rev].concat(path, file.name) } function fileType(file) { if (file.mode === 040000) return 'tree' else if (file.mode === 0160000) return 'commit' else return 'blob' } } /* Repo readme */ R.renderRepoReadme = function (req, repo, branch, path) { var self = this return u.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('
' + '
' + file.name + '
'), self.web.renderObjectData(obj, file.name, repo, branch, path), pull.once('
') ])) }) }) ) }) } /* Repo commit */ R.serveRepoCommit = function (req, repo, rev, filePath) { // TODO: use filePath argument var self = this return u.readNext(function (cb) { repo.getCommitParsed(rev, function (err, commit) { if (err) return cb(null, self.serveRepoTemplate(req, repo, null, rev, `%{author}/%{repo}@${rev}`, pull.once(self.web.renderError(err)))) getObjectMsgId(repo, commit.id, function (err, objMsgId) { if (err) return cb(null, self.serveRepoTemplate(req, repo, null, rev, `%{author}/%{repo}@${rev}`, pull.once(self.web.renderError(err)))) var commitPath = [repo.id, 'commit', commit.id] var treePath = [repo.id, 'tree', commit.id] var title = u.escape(commit.title) + ' · ' + '%{author}/%{repo}@' + commit.id.substr(0, 8) cb(null, self.serveRepoTemplate(req, repo, null, rev, title, cat([ pull.once( '

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

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

' + u.linkify(u.escape(commit.title)) + '

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

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

'), // TODO: show diff from all parents (merge commits) self.renderDiffStat(req, [repo, repo], [commit.parents[0], commit.id], commit.id, objMsgId), pull.once('
') ]))) }) }) }) } /* Repo tag */ R.serveRepoTag = function (req, repo, rev, path) { var self = this return u.readNext(function (cb) { repo.getTagParsed(rev, function (err, tag) { if (err) { if (/Expected tag, got commit/.test(err.message)) { req._u.pathname = u.encodeLink([repo.id, 'commit', rev].concat(path)) return cb(null, self.web.serveRedirect(req, url.format(req._u))) } return cb(null, self.web.serveError(req, err)) } var title = req._t('TagName', { tag: u.escape(tag.tag) }) + ' · %{author}/%{repo}' var body = (tag.title + '\n\n' + tag.body.replace(/-----BEGIN PGP SIGNATURE-----\n[^.]*?\n-----END PGP SIGNATURE-----\s*$/, '')).trim() var date = tag.tagger.date cb(null, self.serveRepoTemplate(req, repo, 'tags', tag.object, title, pull.once( '
' + '

' + u.link([repo.id, 'tag', rev], tag.tag) + '

' + req._t('TaggedOn', { name: u.escape(tag.tagger.name), date: date && date.toLocaleString(req._locale) }) + '
' + u.link([repo.id, tag.type, tag.object]) + u.linkify(u.pre(body)) + '
'))) }) }) } /* Diff stat */ R.renderDiffStat = function (req, repos, treeIds, commit, updateId) { var self = this if (treeIds.length == 0) treeIds = [null] var id = treeIds[0] var lastI = treeIds.length - 1 var oldTree = treeIds[0] var changedFiles = [] var source = GitRepo.diffTrees(repos, treeIds, true) return cat([ h('table', u.sourceMap(source, item => { var filename = u.escape(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('/')) : u.encodeLink(item.blobPath) return h('tr', [ h('td', [ h('a', {href: fileHref}, filename) ]), h('td', action) ]) })), pull( pull.values(changedFiles), paramap(function (item, cb) { var extension = u.getExtension(item.filename) if (extension in u.imgMimes) { var filename = u.escape(item.filename) return cb(null, '
' +
            '' +
            '' +
            '
' + filename + '
' + filename + '
') } var done = multicb({ pluck: 1, spread: true }) var mode0 = item.mode && item.mode[0] var modeI = item.mode && item.mode[lastI] var isSubmodule = (modeI == 0160000) var repo = repos[1] getRepoObjectString(repos[0], item.id[0], mode0, done()) getRepoObjectString(repos[1], item.id[lastI], modeI, done()) self.getLineCommentThreads(req, repo, updateId, commit, item.filename, done()) done(function (err, strOld, strNew, lineCommentThreads) { if (err) return cb(err) cb(null, htmlLineDiff(req, repo, updateId, commit, item.filename, item.filename, strOld, strNew, u.encodeLink(item.blobPath), !isSubmodule, lineCommentThreads)) }) }, 4) ) ]) } function htmlLineDiff(req, repo, updateId, commit, filename, anchor, oldStr, newStr, blobHref, showViewLink, lineCommentThreads) { return '
' + '' + (oldStr.length + newStr.length > 200000 ? '' : tableDiff(req, repo, updateId, commit, oldStr, newStr, filename, lineCommentThreads)) + '
' + filename + (showViewLink === false ? '' : '' + '' + req._t('View') + ' ' + '') + '
' + req._t('diff.TooLarge') + '
' + req._t('diff.OldFileSize', {bytes: oldStr.length}) + '
' + req._t('diff.NewFileSize', {bytes: newStr.length}) + '
' } function tableDiff(req, repo, updateId, commit, oldStr, newStr, filename, lineCommentThreads) { var query = req._u.query 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 = u.highlight(line, u.getExtension(filename)) var trClass = s == '+' ? 'diff-new' : s == '-' ? 'diff-old' : '' var lineNums = [s == '+' ? '' : oldLine++, s == '-' ? '' : newLine++] var id = [filename].concat(lineNums).join('-') var newLineNum = lineNums[lineNums.length-1] return '' + lineNums.map(function (num, i) { var idEnc = encodeURIComponent(id) return '' + (num ? '' + num + '' + (updateId && i === lineNums.length-1 && s !== '-' ? // TODO: use a more descriptive icon for the comment action ' ' : '') : '') + '' }).join('') + '' + html + '' + (lineCommentThreads[newLineNum] ? '' + lineCommentThreads[newLineNum] + '' : commit && query.comment === id ? '' + forms.lineComment(req, repo, updateId, commit, filename, newLineNum) + '' : '') })) }) return [].concat.apply([], groups).join('') } /* An unknown message linking to a repo */ R.serveRepoSomething = function (req, repo, id, msg, path) { return this.serveRepoTemplate(req, repo, null, null, null, pull.once('

' + u.link([id]) + '

' + u.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 }) } R.serveRepoUpdate = function (req, repo, msg, path) { var self = this var raw = req._u.query.raw != null var title = req._t('Update') + ' · %{author}/%{repo}' var c = msg.value.content if (raw) return self.serveRepoTemplate(req, repo, 'activity', null, title, pull.once( '' + req._t('Info') + '' + '

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

' + '
' + u.json(msg) + '
')) // convert packs to old single-object style if (c.indexes) { for (var i = 0; i < c.indexes.length; i++) { c.packs[i] = { pack: {link: c.packs[i].link}, idx: c.indexes[i] } } } var commits = cat([ c.objects && pull( pull.values(c.objects), pull.filter(function (obj) { return obj.type == 'commit' }), paramap(function (obj, cb) { self.web.getBlob(req, obj.link || obj.key, function (err, readObject) { if (err) return cb(err) GitRepo.getCommitParsed({read: readObject}, cb) }) }, 8) ), c.packs && pull( pull.values(c.packs), paramap(function (pack, cb) { var done = multicb({ pluck: 1, spread: true }) self.web.getBlob(req, pack.pack.link, done()) self.web.getBlob(req, pack.idx.link, done()) done(function (err, readPack, readIdx) { if (err) return cb(self.web.renderError(err)) cb(null, gitPack.decodeWithIndex(repo, readPack, readIdx)) }) }, 4), pull.flatten(), pull.asyncMap(function (obj, cb) { if (obj.type == 'commit') GitRepo.getCommitParsed(obj, cb) else pull(obj.read, pull.drain(null, cb)) }), pull.filter() ) ]) return self.serveRepoTemplate(req, repo, 'activity', null, title, cat([ pull.once('' + req._t('Data') + '' + '

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

'), pull( pull.once(msg), pull.asyncMap(renderRepoUpdate.bind(self, req, repo, true)) ), (c.objects || c.packs) && pull.once('

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

'), pull(commits, pull.map(function (commit) { return renderCommit(req, repo, commit) })) ])) } /* Blob */ R.serveRepoBlob = function (req, repo, rev, path) { var self = this return u.readNext(function (cb) { repo.getFile(rev, path, function (err, object) { if (err) return cb(null, self.web.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 = u.getExtension(filename) var title = (path.length ? path.join('/') + ' · ' : '') + '%{author}/%{repo}' + (repo.head == 'refs/heads/' + rev ? '' : '@' + rev) cb(null, self.serveRepoTemplate(req, repo, 'code', rev, title, cat([ pull.once('
' + '

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

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

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

' + '
' + object.length + ' bytes' + '' + u.link(rawFilePath, req._t('Raw')) + '' + '
' + '
'), extension in u.imgMimes ? pull.once('' + u.escape(filename) + '') : self.web.renderObjectData(object, filename, repo, rev, dirPath), pull.once('
') ]))) }) }) } /* Raw blob */ R.serveRepoRaw = function (req, repo, branch, path) { var self = this return u.readNext(function (cb) { repo.getFile(branch, path, function (err, object) { if (err) return cb(null, self.web.serveBuffer(404, req._t('error.BlobNotFound'))) var extension = u.getExtension(path[path.length-1]) var contentType = u.imgMimes[extension] cb(null, pull(object.read, self.web.serveRaw(object.length, contentType))) }) }) } /* Digs */ R.serveRepoDigs = function serveRepoDigs (req, repo) { var self = this return u.readNext(cb => { var title = req._t('Digs') + ' · %{author}/%{repo}' self.web.getVotes(repo.id, (err, votes) => { cb(null, self.serveRepoTemplate(req, repo, null, null, title, h('section', [ h('h3', req._t('Digs')), h('div', `${req._t('Total')}: ${votes.upvotes}`), h('ul', u.paraSourceMap(Object.keys(votes.upvoters), (feedId, cb) => { self.web.about.getName(feedId, (err, name) => { cb(null, h('li', u.link([feedId], name))) }) })) ]) )) }) }) } /* Forks */ R.getForks = function (repo, includeSelf) { var self = this return pull( cat([ includeSelf && pull.once(repo.id), // get downstream repos pull( self.web.ssb.backlinks ? self.web.ssb.backlinks.read({ query: [ {$filter: { dest: repo.id, value: { content: { type: 'git-repo', upstream: repo.id, } } }}, {$map: 'key'} ] }) : pull( self.web.ssb.links({ dest: repo.id, rel: 'upstream' }), pull.map('key') ) ), // look for other repos that previously had pull requests to this one pull( self.web.ssb.backlinks ? self.web.ssb.backlinks.read({ query: [ {$filter: { dest: repo.id, value: { content: { type: 'pull-request', project: repo.id, } } }} ] }) : pull( self.web.ssb.links({ dest: repo.id, values: true, rel: 'project' }), u.decryptMessages(self.web.ssb), pull.filter(function (msg) { var c = msg && msg.value && msg.value.content return c && c.type == 'pull-request' }) ), pull.map(function (msg) { return msg.value.content.head_repo }) ) ]), pull.unique(), paramap(function (key, cb) { if (key && key[0] === '#') return cb(null, {key: key, value: { content: { type: 'git-repo', } }}) self.web.getMsg(key, cb) }, 4), u.decryptMessages(self.web.ssb), pull.filter(function (msg) { var c = msg && msg.value && msg.value.content return c && c.type == 'git-repo' }), paramap(function (msg, cb) { self.web.getRepoFullName(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) ) } R.serveRepoForks = function (req, repo) { var hasForks var title = req._t('Forks') + ' · %{author}/%{repo}' return this.serveRepoTemplate(req, repo, null, null, title, cat([ pull.once('

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

'), pull( this.getForks(repo), pull.map(function (msg) { hasForks = true return '
' + u.link([msg.value.author], msg.authorName) + ' / ' + u.link([msg.key], msg.repoName) + '' + u.timestamp(msg.value.timestamp, req) + '
' }) ), u.readOnce(function (cb) { cb(null, hasForks ? '' : req._t('NoForks')) }) ])) } R.serveRepoForkPrompt = function (req, repo) { var title = req._t('Fork') + ' · %{author}/%{repo}' return this.serveRepoTemplate(req, repo, null, null, title, pull.once( '
' + '

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

' + '

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

' )) } R.serveIssueOrPullRequest = function (req, repo, issue, path, id) { return issue.msg.value.content.type == 'pull-request' ? this.pulls.serveRepoPullReq(req, repo, issue, path, id) : this.issues.serveRepoIssue(req, repo, issue, path, id) } function getRepoLastMod(repo, cb) { repo.getState(function (err, state) { if (err) return cb(err) var lastMod = new Date(Math.max.apply(Math, state.refs.map(function (ref) { return ref.link.value.timestamp }))) || new Date() cb(null, lastMod) }) } R.serveRepoRefs = function (req, repo) { var self = this return u.readNext(function (cb) { getRepoLastMod(repo, function (err, lastMod) { if (err) return cb(null, self.web.serveError(req, err, 500)) if (u.ifModifiedSince(req, lastMod)) { return cb(null, pull.once([304])) } repo.getState(function (err, state) { if (err) return cb(null, self.web.serveError(req, err, 500)) var buf = state.refs.sort(function (a, b) { return a.name > b.name ? 1 : a.name < b.name ? -1 : 0 }).map(function (ref) { return ref.hash + '\t' + ref.name + '\n' }).join('') cb(null, pull.values([[200, { 'Content-Type': 'text/plain; charset=utf-8', 'Content-Length': Buffer.byteLength(buf), 'Last-Modified': lastMod.toGMTString() }], buf])) }) }) }) } R.serveRepoObject = function (req, repo, sha1) { var self = this if (!/[0-9a-f]{20}/.test(sha1)) return pull.once([401]) return u.readNext(function (cb) { repo.getObjectFromAny(sha1, function (err, obj) { if (err) return cb(null, pull.once([404])) cb(null, cat([ pull.once([200, { 'Content-Type': 'application/x-git-loose-object', 'Cache-Control': 'max-age=31536000' }]), pull( cat([ pull.values([obj.type, ' ', obj.length.toString(10), '\0']), obj.read ]), toPull(zlib.createDeflate()) ) ])) }) }) } R.serveRepoHead = function (req, repo) { var self = this return u.readNext(function (cb) { repo.getHead(function (err, name) { if (err) return cb(null, pull.once([500])) return cb(null, self.web.serveBuffer(200, 'ref: ' + name)) }) }) } R.serveRepoPacksInfo = function (req, repo) { var self = this return u.readNext(function (cb) { getRepoLastMod(repo, function (err, lastMod) { if (err) return cb(null, self.web.serveError(req, err, 500)) if (u.ifModifiedSince(req, lastMod)) { return cb(null, pull.once([304])) } cb(null, cat([ pull.once([200, { 'Content-Type': 'text/plain; charset=utf-8', 'Last-Modified': lastMod.toGMTString() }]), pull( repo.packs(), pull.map(function (pack) { var sha1 = pack.sha1 if (!sha1) { // make up a sha1 and hope git doesn't notice var packId = new Buffer(pack.packId.substr(1, 44), 'base64') sha1 = packId.slice(0, 20).toString('hex') } return 'P pack-' + sha1 + '.pack\n' }) ) ])) }) }) } R.serveRepoPack = function (req, repo, name) { var m = name.match(/^pack-(.*)\.(pack|idx)$/) if (!m) return pull.once([400]) var hex; try { hex = new Buffer(m[1], 'hex') } catch(e) { return pull.once([400]) } var self = this return u.readNext(function (cb) { pull( repo.packs(), pull.filter(function (pack) { var sha1 = pack.sha1 ? new Buffer(pack.sha1, 'hex') : new Buffer(pack.packId.substr(1, 44), 'base64').slice(0, 20) return sha1.equals(hex) }), pull.take(1), pull.collect(function (err, packs) { if (err) return console.error(err), cb(null, pull.once([500])) if (packs.length < 1) return cb(null, pull.once([404])) var pack = packs[0] if (m[2] === 'pack') { repo.getPackfile(pack.packId, function (err, read) { if (err) return cb(err) cb(null, pull(read, self.web.serveRaw(null, 'application/x-git-packed-objects') )) }) } if (m[2] === 'idx') { repo.getPackIndex(pack.idxId, function (err, read) { if (err) return cb(err) cb(null, pull(read, self.web.serveRaw(null, 'application/x-git-packed-objects-toc') )) }) } }) ) }) }