var pull = require('pull-stream') var paramap = require('pull-paramap') var cat = require('pull-cat') var many = require('pull-many') var multicb = require('multicb') var GitRepo = require('pull-git-repo') var u = require('../../lib/util') var markdown = require('../../lib/markdown') var forms = require('../../lib/forms') var ssbRef = require('ssb-ref') module.exports = function (repoRoutes, web) { return new RepoPullReqRoutes(repoRoutes, web) } function RepoPullReqRoutes(repoRoutes, web) { this.repo = repoRoutes this.web = web } var P = RepoPullReqRoutes.prototype /* Pull Request */ P.serveRepoPullReq = function (req, repo, pr, path, postId) { var self = this var headRepo, authorLink var page = path[0] || 'activity' var title = u.escape(pr.title) + ' · %{author}/%{repo}' return self.repo.renderRepoPage(req, repo, 'pulls', null, title, cat([ pull.once('
' + '

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

' + '' + pr.id + ''), u.readOnce(function (cb) { var done = multicb({ pluck: 1, spread: true }) var gotHeadRepo = done() self.web.about.getName(pr.author, done()) var sameRepo = (pr.headRepo == pr.baseRepo) self.web.getRepo(pr.headRepo, function (err, headRepo) { if (err) return cb(err) self.web.getRepoName(headRepo.feed, headRepo.id, done()) self.web.about.getName(headRepo.feed, done()) gotHeadRepo(null, GitRepo(headRepo)) }) done(function (err, _headRepo, issueAuthorName, headRepoName, headRepoAuthorName) { if (err) return cb(err) headRepo = _headRepo authorLink = u.link([pr.author], issueAuthorName) var repoLink = u.link([pr.headRepo], headRepoName) var headRepoAuthorLink = u.link([headRepo.feed], headRepoAuthorName) var headRepoLink = u.link([headRepo.id], headRepoName) var headBranchLink = u.link([headRepo.id, 'tree', pr.headBranch]) var baseBranchLink = u.link([repo.id, 'tree', pr.baseBranch]) cb(null, '
' + '' + req._t(pr.open ? 'issue.state.Open' : 'issue.state.Closed') + ' ' + req._t('pullRequest.WantToMerge', { name: authorLink, base: '' + baseBranchLink + '', head: (sameRepo ? '' + headBranchLink + '' : '' + headRepoAuthorLink + ' / ' + headRepoLink + ' / ' + headBranchLink + '') }) + '
') }) }), u.nav([ [[pr.id], req._t('Discussion'), 'activity'], [[pr.id, 'commits'], req._t('Commits'), 'commits'], [[pr.id, 'files'], req._t('Files'), 'files'] ], page), u.readNext(function (cb) { if (page == 'commits') self.renderPullReqCommits(req, pr, repo, headRepo, cb) else if (page == 'files') self.renderPullReqFiles(req, pr, repo, headRepo, cb) else cb(null, self.renderPullReqActivity(req, pr, repo, headRepo, authorLink, postId)) }) ])) } P.renderPullReqCommits = function (req, pr, baseRepo, headRepo, cb) { var self = this self.web.pullReqs.getRevs(pr.id, function (err, revs) { if (err) return cb(err) if (!revs.head) return cb(null, pull.once('
' + req._t('pullRequest.NoCommits') + '
')) GitRepo.getMergeBase(baseRepo, revs.base, headRepo, revs.head, function (err, mergeBase) { if (err) return cb(err) cb(null, cat([ pull.once('
'), self.renderCommitLog(req, baseRepo, mergeBase, headRepo, revs.head), pull.once('
') ])) } ) }) } P.renderPullReqFiles = function (req, pr, baseRepo, headRepo, cb) { var self = this self.web.pullReqs.getRevs(pr.id, function (err, revs) { if (err) return cb(err) if (!revs.head) return cb(null, pull.once('
' + req._t('pullRequest.NoChanges') + '
')) GitRepo.getMergeBase(baseRepo, revs.base, headRepo, revs.head, function (err, mergeBase) { if (err) return cb(err) cb(null, cat([ pull.once('
'), self.repo.renderDiffStat(req, [baseRepo, headRepo], [mergeBase, revs.head]), pull.once('
') ])) } ) }) } P.renderPullReqActivity = function (req, pr, repo, headRepo, authorLink, postId) { var self = this var msgTimeLink = u.link([pr.id], new Date(pr.created_at).toLocaleString(req._locale)) var newestMsg = {key: pr.id, value: {timestamp: pr.created_at}} return cat([ u.readOnce(function (cb) { cb(null, '
' + authorLink + ' · ' + msgTimeLink + markdown(pr.text, repo) + '
') }), // render posts, edits, and updates pull( many([ self.web.ssb.links({ dest: pr.id, values: true }), pull( self.web.ssb.links({ dest: headRepo.id, 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 }) ) ]), pull.unique('key'), u.decryptMessages(self.web.ssb), self.web.addAuthorName(), pull.through(function (msg) { if (msg.value && msg.value.timestamp > newestMsg.value.timestamp && msg.value.content.root === pr.id) newestMsg = msg }), u.sortMsgs(), pull.map(function (item) { if (item.value.content.type == 'git-update') return self.renderBranchUpdate(req, pr, item) return self.repo.issues.renderIssueActivityMsg(req, repo, pr, req._t('pull request'), postId, item) }) ), !self.web.isPublic && pr.open && pull.once( '
' + '' + '

' + '
' + '

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

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

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

' + '
' +
      'git checkout ' + u.escape(pr.baseBranch) + '\n' +
      'git merge ' + u.escape(pr.headBranch) + '\n' +
      'git push ssb ' + u.escape(pr.baseBranch) +
      '
' + '
'), !self.web.isPublic && u.readOnce(function (cb) { cb(null, forms.issueComment(req, pr, repo, newestMsg.key, req._t('pull request'))) }) ]) } P.renderBranchUpdate = function (req, pr, msg) { var authorLink = u.link([msg.value.author], msg.authorName) var msgLink = u.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 = u.link([pr.headRepo, 'commit', rev], rev.substr(0, 8)) return '
' + req._t('NameUpdatedBranch', { name: authorLink, rev: '' + revLink + '' }) + ' · ' + msgLink + '
' } /* Compare changes */ P.branchMenu = function (repo, name, currentName) { return cat([ pull.once('') ]) } P.serveRepoCompare = function (req, repo) { var self = this var query = req._u.query var base var count = 0 var title = req._t('CompareChanges') + ' · %{author}/%{repo}' return self.repo.renderRepoPage(req, repo, 'pulls', null, title, cat([ pull.once('

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

' + '
' + '
'), pull.once(req._t('BaseBranch') + ': '), u.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, self.branchMenu(repo, 'base', base = ref || 'HEAD')) } }), pull.once('
' + req._t('ComparisonRepoBranch') + ':'), pull( self.repo.getForks(repo, true), pull.asyncMap(function (msg, cb) { self.web.getRepo(msg.key, function (err, repo) { if (err) return cb(err) cb(null, { msg: msg, repo: repo }) }) }), pull.map(renderFork), pull.flatten() ), pull.once('
' + ' ' + ': ' + ' / ' + '
'), pull.once('
'), u.readOnce(function (cb) { cb(null, '') }), pull.once('
') ])) function renderFork(fork) { return pull( fork.repo.refs({inherited: false}), 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 = u.link([fork.msg.key, 'tree', ref.name], ref.name) var authorLink = u.link([fork.msg.value.author], fork.msg.authorName) var repoLink = u.link([fork.msg.key], fork.msg.repoName) var value = fork.msg.key + ':' + ref.name return '
' + ' ' + authorLink + ' / ' + repoLink + ' / ' + branchLink + '
' }) ) } } P.serveRepoComparing = function (req, repo) { var self = this var query = req._u.query var baseBranch = query.base var headRepoId, headBranch if (query.head === 'other') { headRepoId = String(query.other_repo).replace(/^ssb:\/*/, '') headBranch = String(query.other_branch) } else if (query.head) { var s = String(query.head).split(':') headRepoId = s[0] headBranch = s[1] } if (!baseBranch) return self.web.serveRedirect(req, u.encodeLink([repo.id, 'compare'])) if (!ssbRef.isMsgId(headRepoId)) return self.web.serveError(req, new Error('bad repo id'), 400) var baseLink = u.link([repo.id, 'tree', baseBranch]) var headBranchLink = u.link([headRepoId, 'tree', headBranch]) var backHref = u.encodeLink([repo.id, 'compare']) + req._u.search var title = req._t(query.expand ? 'OpenPullRequest': 'ComparingChanges') var pageTitle = title + ' · %{author}/%{repo}' return self.repo.renderRepoPage(req, repo, 'pulls', null, pageTitle, cat([ pull.once('

' + title + '

'), u.readNext(function (cb) { self.web.getRepo(headRepoId, function (err, headRepo) { if (err) return cb(err) self.web.getRepoFullName(headRepo.feed, headRepo.id, function (err, repoName, authorName) { if (err) return cb(err) cb(null, renderRepoInfo(GitRepo(headRepo), repoName, authorName)) } ) }) }) ])) function renderRepoInfo(headRepo, headRepoName, headRepoAuthorName) { var authorLink = u.link([headRepo.feed], headRepoAuthorName) var repoLink = u.link([headRepoId], headRepoName) return cat([ pull.once('
' + req._t('Base') + ': ' + baseLink + '
' + req._t('Head') + ': ' + '' + authorLink + ' / ' + repoLink + ' / ' + headBranchLink + '' + '
' + (query.expand ? '
' + u.hiddenInputs({ action: 'new-pull', branch: baseBranch, head_repo: headRepoId, head_branch: headBranch }) + forms.post(req, repo, null, 8) + '' + '
' : self.web.isPublic ? '' : '
' + u.hiddenInputs({ base: baseBranch, head: query.head, other_repo: query.other_repo, other_branch: query.other_branch, }) + ' ' + '' + req._t('Back') + '' + '
') + '
' + ''), u.readNext(function (cb) { GitRepo.getMergeBase(repo, baseBranch, headRepo, headBranch, function (err, concestor) { if (err) return cb(err) cb(null, cat([ pull.once('
'), self.repo.renderDiffStat(req, [repo, headRepo], [concestor, headBranch]), pull.once('
' + '
'), self.renderCommitLog(req, repo, concestor, headRepo, headBranch), pull.once('
') ])) } ) }) ]) } } P.renderCommitLog = function (req, baseRepo, baseBranch, headRepo, headBranch) { return cat([ pull.once(''), u.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 }), u.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) + '
' + u.escape(commit.author.name) + '' + u.link(commitPath, commit.title) + '' + u.link(commitPath, commitIdShort, true) + '
') ]) }