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('
'),
pull(
repo.refs(),
pull.map(function (ref) {
var m = ref.name.match(/^refs\/([^\/]*)\/(.*)$/) || [,, ref.name]
return m[1] == 'heads' && m[2]
}),
pull.filter(Boolean),
u.pullSort(),
pull.map(this.repo.formatRevOptions(currentName))
),
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') + ' ' +
'
')
]))
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 ? '
'
: self.web.isPublic ? ''
: '
') +
'
' +
'
'),
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 ? '' :
'' +
commit.author.date.toLocaleDateString(req._locale) +
' '
currentDay = day
return dateRow + ' ' +
'' + u.escape(commit.author.name) + ' ' +
'' +
u.link(commitPath, commit.title) + ' ' +
'' + u.link(commitPath, commitIdShort, true) + ' ' +
' '
})
))
})
}),
pull.once('
')
])
}