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 = '' +
'
' +
'SSB ' +
'HTTP ' +
' ' +
'
' +
' ' +
'' +
'
' +
'
'
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 '' + htmlName + ' '
}
}
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, '' +
Object.keys(refs).map(function (group) {
return '' +
refs[group].map(self.formatRevOptions(currentName)).join('') +
' '
}).join('') +
' ' +
' ')
})
})
}
/* 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 + ' ' +
' ' +
'
')
}
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 '' +
'' + filename +
(showViewLink === false ? '' :
'' +
'' + req._t('View') + ' ' +
' ') +
' ' +
(oldStr.length + newStr.length > 200000
? '' + req._t('diff.TooLarge') + ' ' +
req._t('diff.OldFileSize', {bytes: oldStr.length}) + ' ' +
req._t('diff.NewFileSize', {bytes: newStr.length}) + ' '
: tableDiff(req, repo, updateId, commit, oldStr, newStr, filename, lineCommentThreads)) +
'
'
}
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 = ''
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('Update') + ' ' +
''))
// 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('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(''),
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(' ')
: 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(
''
))
}
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')
))
})
}
})
)
})
}