git ssb

30+

cel / git-ssb-web



Commit e2ff84468233c659a81da3317c19bb5e5e0bd932

Make into a sbot plugin

Close %HlakiED4D23eBRVJwT7h9fwJzWX55J06zH+rcmugR/g=.sha256
Charles Lehner committed on 4/21/2016, 8:28:40 PM
Parent: 46e89b23632bc4247e6cd362012340b9380c10f0

Files changed

index.jschanged
server.jschanged
index.jsView
@@ -380,37 +380,8 @@
380380 return str
381381 }
382382 }
383383
384-function getRepoName(about, ownerId, repoId, cb) {
385- about.getName({
386- owner: ownerId,
387- target: repoId,
388- toString: function () {
389- // hack to fit two parameters into asyncmemo
390- return ownerId + '/' + repoId
391- }
392- }, cb)
393-}
394-
395-function getRepoFullName(about, author, repoId, cb) {
396- var done = multicb({ pluck: 1, spread: true })
397- getRepoName(about, author, repoId, done())
398- about.getName(author, done())
399- done(cb)
400-}
401-
402-function addAuthorName(about) {
403- return paramap(function (msg, cb) {
404- var author = msg && msg.value && msg.value.author
405- if (!author) return cb(null, msg)
406- about.getName(author, function (err, authorName) {
407- msg.authorName = authorName
408- cb(err, msg)
409- })
410- }, 8)
411-}
412-
413384 function getMention(msg, id) {
414385 if (msg.key == id) return msg
415386 var mentions = msg.value.content.mentions
416387 if (mentions) for (var i = 0; i < mentions.length; i++) {
@@ -480,1260 +451,1323 @@
480451 svg: 'image/svg+xml',
481452 bmp: 'image/bmp'
482453 }
483454
484-module.exports = function (opts, cb) {
485- var ssb, reconnect, myId, getRepo, getVotes, getMsg, issues
486- var about = function (id, cb) { cb(null, {name: id}) }
487- var reqQueue = []
488- var isPublic = opts.public
489- var ssbAppname = opts.appname || 'ssb'
455+var _httpServer
490456
491- var addr = parseAddr(opts.listenAddr, {host: 'localhost', port: 7718})
492- http.createServer(onRequest).listen(addr.port, addr.host, onListening)
457+module.exports = {
458+ name: 'git-ssb-web',
459+ version: require('./package').version,
460+ manifest: {},
461+ init: function (ssb, config, reconnect) {
462+ // close existing server. when scuttlebot plugins get a deinit method, we
463+ // will close it in that instead it
464+ if (_httpServer)
465+ _httpServer.close()
493466
494- var server = {
495- setSSB: function (_ssb, _reconnect) {
496- _ssb.whoami(function (err, feed) {
497- if (err) throw err
498- ssb = _ssb
499- reconnect = _reconnect
500- myId = feed.id
501- about = ssbAbout(ssb, myId)
502- while (reqQueue.length)
503- onRequest.apply(this, reqQueue.shift())
504- getRepo = asyncMemo(function (id, cb) {
505- getMsg(id, function (err, msg) {
506- if (err) return cb(err)
507- ssbGit.getRepo(ssb, {key: id, value: msg}, {live: true}, cb)
508- })
509- })
510- getVotes = ssbVotes(ssb)
511- getMsg = asyncMemo(ssb.get)
512- issues = Issues.init(ssb)
513- pullReqs = PullRequests.init(ssb)
514- })
515- }
516- }
467+ var web = new GitSSBWeb(ssb, config, reconnect)
468+ _httpSserver = web.httpServer
517469
518- function onListening() {
519- var host = ~addr.host.indexOf(':') ? '[' + addr.host + ']' : addr.host
520- console.log('Listening on http://' + host + ':' + addr.port + '/')
521- cb(null, server)
470+ return {}
522471 }
472+}
523473
524- /* Serving a request */
474+function GitSSBWeb(ssb, config, reconnect) {
475+ this.ssb = ssb
476+ this.config = config
477+ this.reconnect = reconnect
525478
526- function onRequest(req, res) {
527- console.log(req.method, req.url)
528- if (!ssb) return reqQueue.push(arguments)
529- req._u = url.parse(req.url, true)
530- var locale = req._u.query.locale ||
531- (/locale=([^;]*)/.exec(req.headers.cookie) || [])[1]
532- var reqLocales = req.headers['accept-language']
533- i18n.pickCatalog(reqLocales, locale, function (err, t) {
534- if (err) return pull(serveError(req, err, 500), serve(req, res))
535- req._t = t
536- req._locale = t.locale
537- pull(handleRequest(req), serve(req, res))
479+ this.ssbAppname = config.appname || 'ssb'
480+ this.isPublic = config.public
481+ this.getVotes = ssbVotes(ssb)
482+ this.getMsg = asyncMemo(ssb.get)
483+ this.issues = Issues.init(ssb)
484+ this.pullReqs = PullRequests.init(ssb)
485+ this.getRepo = asyncMemo(function (id, cb) {
486+ this.getMsg(id, function (err, msg) {
487+ if (err) return cb(err)
488+ ssbGit.getRepo(ssb, {key: id, value: msg}, {live: true}, cb)
538489 })
539- }
490+ })
540491
541- function serve(req, res) {
542- return pull(
543- pull.filter(function (data) {
544- if (Array.isArray(data)) {
545- res.writeHead.apply(res, data)
546- return false
547- }
548- return true
549- }),
550- toPull(res)
551- )
552- }
492+ this.about = function (id, cb) { cb(null, {name: id}) }
493+ ssb.whoami(function (err, feed) {
494+ this.myId = feed.id
495+ this.about = ssbAbout(ssb, this.myId)
496+ }.bind(this))
553497
554- function handleRequest(req) {
555- var path = req._u.pathname.slice(1)
556- var dirs = ref.isLink(path) ? [path] :
557- path.split(/\/+/).map(tryDecodeURIComponent)
558- var dir = dirs[0]
498+ var webConfig = config['git-ssb-web'] || {}
499+ var addr = parseAddr(config.listenAddr, {
500+ host: webConfig.host || 'localhost',
501+ port: webConfig.port || 7718
502+ })
503+ this.listen(addr.host, addr.port)
504+}
559505
560- if (req.method == 'POST') {
561- if (isPublic)
562- return serveBuffer(405, req._t('error.POSTNotAllowed'))
563- return readNext(function (cb) {
564- readReqForm(req, function (err, data) {
565- if (err) return cb(null, serveError(req, err, 400))
566- if (!data) return cb(null, serveError(req,
567- new ParamError(req._t('error.MissingData')), 400))
506+var G = GitSSBWeb.prototype
568507
569- switch (data.action) {
570- case 'fork-prompt':
571- return cb(null, serveRedirect(req,
572- encodeLink([data.id, 'fork'])))
508+G.listen = function (host, port) {
509+ this.httpServer = http.createServer(G_onRequest.bind(this))
510+ this.httpServer.listen(port, host, function () {
511+ var hostName = ~host.indexOf(':') ? '[' + host + ']' : host
512+ console.log('Listening on http://' + hostName + ':' + port + '/')
513+ })
514+}
573515
574- case 'fork':
575- if (!data.id)
576- return cb(null, serveError(req,
577- new ParamError(req._t('error.MissingId')), 400))
578- return ssbGit.createRepo(ssb, {upstream: data.id},
579- function (err, repo) {
580- if (err) return cb(null, serveError(req, err))
581- cb(null, serveRedirect(req, encodeLink(repo.id)))
582- })
516+G.getRepoName = function (ownerId, repoId, cb) {
517+ this.about.getName({
518+ owner: ownerId,
519+ target: repoId,
520+ toString: function () {
521+ // hack to fit two parameters into asyncmemo
522+ return ownerId + '/' + repoId
523+ }
524+ }, cb)
525+}
583526
584- case 'vote':
585- var voteValue = +data.value || 0
586- if (!data.id)
587- return cb(null, serveError(req,
588- new ParamError(req._t('error.MissingId')), 400))
589- var msg = schemas.vote(data.id, voteValue)
590- return ssb.publish(msg, function (err) {
591- if (err) return cb(null, serveError(req, err))
592- cb(null, serveRedirect(req, req.url))
593- })
527+G.getRepoFullName = function (author, repoId, cb) {
528+ var done = multicb({ pluck: 1, spread: true })
529+ this.getRepoName(author, repoId, done())
530+ this.about.getName(author, done())
531+ done(cb)
532+}
594533
595- case 'repo-name':
596- if (!data.id)
597- return cb(null, serveError(req,
598- new ParamError(req._t('error.MissingId')), 400))
599- if (!data.name)
600- return cb(null, serveError(req,
601- new ParamError(req._t('error.MissingName')), 400))
602- var msg = schemas.name(data.id, data.name)
603- return ssb.publish(msg, function (err) {
604- if (err) return cb(null, serveError(req, err))
605- cb(null, serveRedirect(req, req.url))
606- })
534+G.addAuthorName = function () {
535+ var about = this.about
536+ return paramap(function (msg, cb) {
537+ var author = msg && msg.value && msg.value.author
538+ if (!author) return cb(null, msg)
539+ about.getName(author, function (err, authorName) {
540+ msg.authorName = authorName
541+ cb(err, msg)
542+ })
543+ }, 8)
544+}
607545
608- case 'issue-title':
609- if (!data.id)
610- return cb(null, serveError(req,
611- new ParamError(req._t('error.MissingId')), 400))
612- if (!data.name)
613- return cb(null, serveError(req,
614- new ParamError(req._t('error.MissingName')), 400))
615- var msg = Issues.schemas.edit(data.id, {title: data.name})
616- return ssb.publish(msg, function (err) {
617- if (err) return cb(null, serveError(req, err))
618- cb(null, serveRedirect(req, req.url))
619- })
546+/* Serving a request */
620547
621- case 'comment':
622- if (!data.id)
623- return cb(null, serveError(req,
624- new ParamError(req._t('error.MissingId')), 400))
625- var msg = schemas.post(data.text, data.id, data.branch || data.id)
626- msg.issue = data.issue
627- msg.repo = data.repo
628- if (data.open != null)
629- Issues.schemas.opens(msg, data.id)
630- if (data.close != null)
631- Issues.schemas.closes(msg, data.id)
632- var mentions = Mentions(data.text)
633- if (mentions.length)
634- msg.mentions = mentions
635- return ssb.publish(msg, function (err) {
636- if (err) return cb(null, serveError(req, err))
637- cb(null, serveRedirect(req, req.url))
638- })
548+function serve(req, res) {
549+ return pull(
550+ pull.filter(function (data) {
551+ if (Array.isArray(data)) {
552+ res.writeHead.apply(res, data)
553+ return false
554+ }
555+ return true
556+ }),
557+ toPull(res)
558+ )
559+}
639560
640- case 'new-issue':
641- var msg = Issues.schemas.new(dir, data.title, data.text)
642- var mentions = Mentions(data.text)
643- if (mentions.length)
644- msg.mentions = mentions
645- return ssb.publish(msg, function (err, msg) {
646- if (err) return cb(null, serveError(req, err))
647- cb(null, serveRedirect(req, encodeLink(msg.key)))
648- })
561+function G_onRequest(req, res) {
562+ console.log(req.method, req.url)
563+ req._u = url.parse(req.url, true)
564+ var locale = req._u.query.locale ||
565+ (/locale=([^;]*)/.exec(req.headers.cookie) || [])[1]
566+ var reqLocales = req.headers['accept-language']
567+ i18n.pickCatalog(reqLocales, locale, function (err, t) {
568+ if (err) return pull(this.serveError(req, err, 500), serve(req, res))
569+ req._t = t
570+ req._locale = t.locale
571+ pull(this.handleRequest(req), serve(req, res))
572+ }.bind(this))
573+}
649574
650- case 'new-pull':
651- var msg = PullRequests.schemas.new(dir, data.branch,
652- data.head_repo, data.head_branch, data.title, data.text)
653- var mentions = Mentions(data.text)
654- if (mentions.length)
655- msg.mentions = mentions
656- return ssb.publish(msg, function (err, msg) {
657- if (err) return cb(null, serveError(req, err))
658- cb(null, serveRedirect(req, encodeLink(msg.key)))
575+G.handleRequest = function (req) {
576+ var path = req._u.pathname.slice(1)
577+ var dirs = ref.isLink(path) ? [path] :
578+ path.split(/\/+/).map(tryDecodeURIComponent)
579+ var dir = dirs[0]
580+
581+ if (req.method == 'POST')
582+ return this.handlePOST(req, dir)
583+
584+ if (dir == '')
585+ return this.serveIndex(req)
586+ else if (dir == 'search')
587+ return this.serveSearch(req)
588+ else if (ref.isBlobId(dir))
589+ return this.serveBlob(req, dir)
590+ else if (ref.isMsgId(dir))
591+ return this.serveMessage(req, dir, dirs.slice(1))
592+ else if (ref.isFeedId(dir))
593+ return this.serveUserPage(req, dir, dirs.slice(1))
594+ else if (dir == 'static')
595+ return this.serveFile(req, dirs)
596+ else if (dir == 'highlight')
597+ return this.serveFile(req, [hlCssPath].concat(dirs.slice(1)), true)
598+ else
599+ return this.serve404(req)
600+}
601+
602+G.handlePOST = function (req, dir) {
603+ var self = this
604+ if (self.isPublic)
605+ return self.serveBuffer(405, req._t('error.POSTNotAllowed'))
606+ return readNext(function (cb) {
607+ readReqForm(req, function (err, data) {
608+ if (err) return cb(null, self.serveError(req, err, 400))
609+ if (!data) return cb(null, self.serveError(req,
610+ new ParamError(req._t('error.MissingData')), 400))
611+
612+ switch (data.action) {
613+ case 'fork-prompt':
614+ return cb(null, self.serveRedirect(req,
615+ encodeLink([data.id, 'fork'])))
616+
617+ case 'fork':
618+ if (!data.id)
619+ return cb(null, self.serveError(req,
620+ new ParamError(req._t('error.MissingId')), 400))
621+ return ssbGit.createRepo(self.ssb, {upstream: data.id},
622+ function (err, repo) {
623+ if (err) return cb(null, self.serveError(req, err))
624+ cb(null, self.serveRedirect(req, encodeLink(repo.id)))
659625 })
660626
661- case 'markdown':
662- return cb(null, serveMarkdown(data.text, {id: data.repo}))
627+ case 'vote':
628+ var voteValue = +data.value || 0
629+ if (!data.id)
630+ return cb(null, self.serveError(req,
631+ new ParamError(req._t('error.MissingId')), 400))
632+ var msg = schemas.vote(data.id, voteValue)
633+ return self.ssb.publish(msg, function (err) {
634+ if (err) return cb(null, self.serveError(req, err))
635+ cb(null, self.serveRedirect(req, req.url))
636+ })
663637
664- default:
665- cb(null, serveBuffer(400, req._t('error.UnknownAction', data)))
666- }
638+ case 'repo-name':
639+ if (!data.id)
640+ return cb(null, self.serveError(req,
641+ new ParamError(req._t('error.MissingId')), 400))
642+ if (!data.name)
643+ return cb(null, self.serveError(req,
644+ new ParamError(req._t('error.MissingName')), 400))
645+ var msg = schemas.name(data.id, data.name)
646+ return self.ssb.publish(msg, function (err) {
647+ if (err) return cb(null, self.serveError(req, err))
648+ cb(null, self.serveRedirect(req, req.url))
667649 })
668- })
669- }
670650
671- if (dir == '')
672- return serveIndex(req)
673- else if (dir == 'search')
674- return serveSearch(req)
675- else if (ref.isBlobId(dir))
676- return serveBlob(req, dir)
677- else if (ref.isMsgId(dir))
678- return serveMessage(req, dir, dirs.slice(1))
679- else if (ref.isFeedId(dir))
680- return serveUserPage(req, dir, dirs.slice(1))
681- else if (dir == 'static')
682- return serveFile(req, dirs)
683- else if (dir == 'highlight')
684- return serveFile(req, [hlCssPath].concat(dirs.slice(1)), true)
685- else
686- return serve404(req)
687- }
651+ case 'issue-title':
652+ if (!data.id)
653+ return cb(null, self.serveError(req,
654+ new ParamError(req._t('error.MissingId')), 400))
655+ if (!data.name)
656+ return cb(null, self.serveError(req,
657+ new ParamError(req._t('error.MissingName')), 400))
658+ var msg = Issues.schemas.edit(data.id, {title: data.name})
659+ return self.ssb.publish(msg, function (err) {
660+ if (err) return cb(null, self.serveError(req, err))
661+ cb(null, self.serveRedirect(req, req.url))
662+ })
688663
689- function serveFile(req, dirs, outside) {
690- var filename = path.resolve.apply(path, [__dirname].concat(dirs))
691- // prevent escaping base dir
692- if (!outside && filename.indexOf('../') === 0)
693- return serveBuffer(403, req._t("error.403Forbidden"))
664+ case 'comment':
665+ if (!data.id)
666+ return cb(null, self.serveError(req,
667+ new ParamError(req._t('error.MissingId')), 400))
668+ var msg = schemas.post(data.text, data.id, data.branch || data.id)
669+ msg.issue = data.issue
670+ msg.repo = data.repo
671+ if (data.open != null)
672+ Issues.schemas.reopens(msg, data.id)
673+ if (data.close != null)
674+ Issues.schemas.closes(msg, data.id)
675+ var mentions = Mentions(data.text)
676+ if (mentions.length)
677+ msg.mentions = mentions
678+ return self.ssb.publish(msg, function (err) {
679+ if (err) return cb(null, self.serveError(req, err))
680+ cb(null, self.serveRedirect(req, req.url))
681+ })
694682
695- return readNext(function (cb) {
696- fs.stat(filename, function (err, stats) {
697- cb(null, err ?
698- err.code == 'ENOENT' ? serve404(req)
699- : serveBuffer(500, err.message)
700- : 'if-modified-since' in req.headers &&
701- new Date(req.headers['if-modified-since']) >= stats.mtime ?
702- pull.once([304])
703- : stats.isDirectory() ?
704- serveBuffer(403, req._t('error.DirectoryNotListable'))
705- : cat([
706- pull.once([200, {
707- 'Content-Type': getContentType(filename),
708- 'Content-Length': stats.size,
709- 'Last-Modified': stats.mtime.toGMTString()
710- }]),
711- toPull(fs.createReadStream(filename))
712- ]))
713- })
683+ case 'new-issue':
684+ var msg = Issues.schemas.new(dir, data.title, data.text)
685+ var mentions = Mentions(data.text)
686+ if (mentions.length)
687+ msg.mentions = mentions
688+ return self.ssb.publish(msg, function (err, msg) {
689+ if (err) return cb(null, self.serveError(req, err))
690+ cb(null, self.serveRedirect(req, encodeLink(msg.key)))
691+ })
692+
693+ case 'new-pull':
694+ var msg = PullRequests.schemas.new(dir, data.branch,
695+ data.head_repo, data.head_branch, data.title, data.text)
696+ var mentions = Mentions(data.text)
697+ if (mentions.length)
698+ msg.mentions = mentions
699+ return self.ssb.publish(msg, function (err, msg) {
700+ if (err) return cb(null, self.serveError(req, err))
701+ cb(null, self.serveRedirect(req, encodeLink(msg.key)))
702+ })
703+
704+ case 'markdown':
705+ return cb(null, self.serveMarkdown(data.text, {id: data.repo}))
706+
707+ default:
708+ cb(null, self.serveBuffer(400, req._t('error.UnknownAction', data)))
709+ }
714710 })
715- }
711+ })
712+}
716713
717- function servePlainError(code, msg) {
718- return pull.values([
719- [code, {
720- 'Content-Length': Buffer.byteLength(msg),
721- 'Content-Type': 'text/plain; charset=utf-8'
722- }],
723- msg
724- ])
725- }
714+G.serveFile = function (req, dirs, outside) {
715+ var filename = path.resolve.apply(path, [__dirname].concat(dirs))
716+ // prevent escaping base dir
717+ if (!outside && filename.indexOf('../') === 0)
718+ return this.serveBuffer(403, req._t("error.403Forbidden"))
726719
727- function serveBuffer(code, buf, contentType, headers) {
728- headers = headers || {}
729- headers['Content-Type'] = contentType || 'text/plain; charset=utf-8'
730- headers['Content-Length'] = Buffer.byteLength(buf)
731- return pull.values([
732- [code, headers],
733- buf
734- ])
735- }
720+ return readNext(function (cb) {
721+ fs.stat(filename, function (err, stats) {
722+ cb(null, err ?
723+ err.code == 'ENOENT' ? this.serve404(req)
724+ : this.serveBuffer(500, err.message)
725+ : 'if-modified-since' in req.headers &&
726+ new Date(req.headers['if-modified-since']) >= stats.mtime ?
727+ pull.once([304])
728+ : stats.isDirectory() ?
729+ this.serveBuffer(403, req._t('error.DirectoryNotListable'))
730+ : cat([
731+ pull.once([200, {
732+ 'Content-Type': getContentType(filename),
733+ 'Content-Length': stats.size,
734+ 'Last-Modified': stats.mtime.toGMTString()
735+ }]),
736+ toPull(fs.createReadStream(filename))
737+ ]))
738+ }.bind(this))
739+ }.bind(this))
740+}
736741
737- function serve404(req) {
738- return serveBuffer(404, req._t("error.404NotFound"))
739- }
742+G.serveBuffer = function (code, buf, contentType, headers) {
743+ headers = headers || {}
744+ headers['Content-Type'] = contentType || 'text/plain; charset=utf-8'
745+ headers['Content-Length'] = Buffer.byteLength(buf)
746+ return pull.values([
747+ [code, headers],
748+ buf
749+ ])
750+}
740751
741- function serveRedirect(req, path) {
742- return serveBuffer(302,
743- '<!doctype><html><head>' +
744- '<title>' + req._t('Redirect') + '</title></head><body>' +
745- '<p><a href="' + escapeHTML(path) + '">' +
746- req._t('Continue') + '</a></p>' +
747- '</body></html>', 'text/html; charset=utf-8', {Location: path})
748- }
752+G.serve404 = function (req) {
753+ return this.serveBuffer(404, req._t("error.404NotFound"))
754+}
749755
750- function serveMarkdown(text, repo) {
751- return serveBuffer(200, markdown(text, repo), 'text/html; charset=utf-8')
752- }
756+G.serveRedirect = function (req, path) {
757+ return this.serveBuffer(302,
758+ '<!doctype><html><head>' +
759+ '<title>' + req._t('Redirect') + '</title></head><body>' +
760+ '<p><a href="' + escapeHTML(path) + '">' +
761+ req._t('Continue') + '</a></p>' +
762+ '</body></html>', 'text/html; charset=utf-8', {Location: path})
763+}
753764
754- function renderError(err, tag) {
755- tag = tag || 'h3'
756- return '<' + tag + '>' + err.name + '</' + tag + '>' +
757- '<pre>' + escapeHTML(err.stack) + '</pre>'
758- }
765+G.serveMarkdown = function (text, repo) {
766+ return this.serveBuffer(200, markdown(text, repo),
767+ 'text/html; charset=utf-8')
768+}
759769
760- function renderTry(read) {
761- var ended
762- return function (end, cb) {
763- if (ended) return cb(ended)
764- read(end, function (err, data) {
765- if (err === true)
766- cb(true)
767- else if (err) {
768- ended = true
769- cb(null, renderError(err, 'h3'))
770- } else
771- cb(null, data)
772- })
773- }
774- }
770+function renderError(err, tag) {
771+ tag = tag || 'h3'
772+ return '<' + tag + '>' + err.name + '</' + tag + '>' +
773+ '<pre>' + escapeHTML(err.stack) + '</pre>'
774+}
775775
776- function serveTemplate(req, title, code, read) {
777- if (read === undefined) return serveTemplate.bind(this, req, title, code)
778- var q = req._u.query.q && escapeHTML(req._u.query.q) || ''
779- var app = 'git ssb'
780- if (req._t) app = req._t(app)
781- return cat([
782- pull.values([
783- [code || 200, {
784- 'Content-Type': 'text/html'
785- }],
786- '<!doctype html><html><head><meta charset=utf-8>',
787- '<title>' + (title || app) + '</title>',
788- '<link rel=stylesheet href="/static/styles.css"/>',
789- '<link rel=stylesheet href="/highlight/github.css"/>',
790- '</head>\n',
791- '<body>',
792- '<header><form action="/search" method="get">' +
793- '<h1><a href="/">' + app + '' +
794- (ssbAppname != 'ssb' ? ' <sub>' + ssbAppname + '</sub>' : '') +
795- '</a> ' +
796- '<input class="search-bar" name="q" size="60"' +
797- ' placeholder="🔍" value="' + q + '" />' +
798- '</h1>',
799- '</form></header>',
800- '<article>']),
801- renderTry(read),
802- pull.once('<hr/></article></body></html>')
803- ])
776+function renderTry(read) {
777+ var ended
778+ return function (end, cb) {
779+ if (ended) return cb(ended)
780+ read(end, function (err, data) {
781+ if (err === true)
782+ cb(true)
783+ else if (err) {
784+ ended = true
785+ cb(null, renderError(err, 'h3'))
786+ } else
787+ cb(null, data)
788+ })
804789 }
790+}
805791
806- function serveError(req, err, status) {
807- if (err.message == 'stream is closed')
808- reconnect()
809- return pull(
810- pull.once(renderError(err, 'h2')),
811- serveTemplate(req, err.name, status || 500)
812- )
813- }
792+G.serveTemplate = function (req, title, code, read) {
793+ if (read === undefined)
794+ return this.serveTemplate.bind(this, req, title, code)
795+ var q = req._u.query.q && escapeHTML(req._u.query.q) || ''
796+ var app = 'git ssb'
797+ var appName = this.ssbAppname
798+ if (req._t) app = req._t(app)
799+ return cat([
800+ pull.values([
801+ [code || 200, {
802+ 'Content-Type': 'text/html'
803+ }],
804+ '<!doctype html><html><head><meta charset=utf-8>',
805+ '<title>' + (title || app) + '</title>',
806+ '<link rel=stylesheet href="/static/styles.css"/>',
807+ '<link rel=stylesheet href="/highlight/github.css"/>',
808+ '</head>\n',
809+ '<body>',
810+ '<header><form action="/search" method="get">' +
811+ '<h1><a href="/">' + app + '' +
812+ (appName != 'ssb' ? ' <sub>' + appName + '</sub>' : '') +
813+ '</a> ' +
814+ '<input class="search-bar" name="q" size="60"' +
815+ ' placeholder="🔍" value="' + q + '" />' +
816+ '</h1>',
817+ '</form></header>',
818+ '<article>']),
819+ renderTry(read),
820+ pull.once('<hr/></article></body></html>')
821+ ])
822+}
814823
815- function renderObjectData(obj, filename, repo, rev, path) {
816- var ext = getExtension(filename)
817- return readOnce(function (cb) {
818- readObjectString(obj, function (err, buf) {
819- buf = buf.toString('utf8')
820- if (err) return cb(err)
821- cb(null, (ext == 'md' || ext == 'markdown')
822- ? markdown(buf, {repo: repo, rev: rev, path: path})
823- : renderCodeTable(buf, ext))
824- })
824+G.serveError = function (req, err, status) {
825+ if (err.message == 'stream is closed')
826+ this.reconnect && this.reconnect()
827+ return pull(
828+ pull.once(renderError(err, 'h2')),
829+ this.serveTemplate(req, err.name, status || 500)
830+ )
831+}
832+
833+function renderObjectData(obj, filename, repo, rev, path) {
834+ var ext = getExtension(filename)
835+ return readOnce(function (cb) {
836+ readObjectString(obj, function (err, buf) {
837+ buf = buf.toString('utf8')
838+ if (err) return cb(err)
839+ cb(null, (ext == 'md' || ext == 'markdown')
840+ ? markdown(buf, {repo: repo, rev: rev, path: path})
841+ : renderCodeTable(buf, ext))
825842 })
826- }
843+ })
844+}
827845
828- function renderCodeTable(buf, ext) {
829- return '<pre><table class="code">' +
830- highlight(buf, ext).split('\n').map(function (line, i) {
831- i++
832- return '<tr id="L' + i + '">' +
833- '<td class="code-linenum">' + '<a href="#L' + i + '">' + i + '</td>' +
834- '<td class="code-text">' + line + '</td></tr>'
835- }).join('') +
836- '</table></pre>'
837- }
846+function renderCodeTable(buf, ext) {
847+ return '<pre><table class="code">' +
848+ highlight(buf, ext).split('\n').map(function (line, i) {
849+ i++
850+ return '<tr id="L' + i + '">' +
851+ '<td class="code-linenum">' + '<a href="#L' + i + '">' + i + '</td>' +
852+ '<td class="code-text">' + line + '</td></tr>'
853+ }).join('') +
854+ '</table></pre>'
855+}
838856
839- /* Feed */
857+/* Feed */
840858
841- function renderFeed(req, feedId, filter) {
842- var query = req._u.query
843- var opts = {
844- reverse: !query.forwards,
845- lt: query.lt && +query.lt || Date.now(),
846- gt: query.gt && +query.gt,
847- id: feedId
848- }
849- return pull(
850- feedId ? ssb.createUserStream(opts) : ssb.createFeedStream(opts),
851- pull.filter(function (msg) {
852- var c = msg.value.content
853- return c.type in msgTypes
854- || (c.type == 'post' && c.repo && c.issue)
855- }),
856- typeof filter == 'function' ? filter(opts) : filter,
857- pull.take(20),
858- addAuthorName(about),
859- query.forwards && pullReverse(),
860- paginate(
861- function (first, cb) {
862- if (!query.lt && !query.gt) return cb(null, '')
863- var gt = feedId ? first.value.sequence : first.value.timestamp + 1
864- query.gt = gt
865- query.forwards = 1
866- delete query.lt
867- cb(null, '<a href="?' + qs.stringify(query) + '">' +
868- req._t('Newer') + '</a>')
869- },
870- paramap(renderFeedItem.bind(null, req), 8),
871- function (last, cb) {
872- query.lt = feedId ? last.value.sequence : last.value.timestamp - 1
859+G.renderFeed = function (req, feedId, filter) {
860+ var query = req._u.query
861+ var opts = {
862+ reverse: !query.forwards,
863+ lt: query.lt && +query.lt || Date.now(),
864+ gt: query.gt ? +query.gt : -Infinity,
865+ id: feedId
866+ }
867+ return pull(
868+ feedId ? this.ssb.createUserStream(opts) : this.ssb.createFeedStream(opts),
869+ pull.filter(function (msg) {
870+ var c = msg.value.content
871+ return c.type in msgTypes
872+ || (c.type == 'post' && c.repo && c.issue)
873+ }),
874+ typeof filter == 'function' ? filter(opts) : filter,
875+ pull.take(20),
876+ this.addAuthorName(),
877+ query.forwards && pullReverse(),
878+ paginate(
879+ function (first, cb) {
880+ if (!query.lt && !query.gt) return cb(null, '')
881+ var gt = feedId ? first.value.sequence : first.value.timestamp + 1
882+ query.gt = gt
883+ query.forwards = 1
884+ delete query.lt
885+ cb(null, '<a href="?' + qs.stringify(query) + '">' +
886+ req._t('Newer') + '</a>')
887+ },
888+ paramap(this.renderFeedItem.bind(this, req), 8),
889+ function (last, cb) {
890+ query.lt = feedId ? last.value.sequence : last.value.timestamp - 1
891+ delete query.gt
892+ delete query.forwards
893+ cb(null, '<a href="?' + qs.stringify(query) + '">' +
894+ req._t('Older') + '</a>')
895+ },
896+ function (cb) {
897+ if (query.forwards) {
873898 delete query.gt
874899 delete query.forwards
875- cb(null, '<a href="?' + qs.stringify(query) + '">' +
876- req._t('Older') + '</a>')
877- },
878- function (cb) {
879- if (query.forwards) {
880- delete query.gt
881- delete query.forwards
882- query.lt = opts.gt + 1
883- } else {
884- delete query.lt
885- query.gt = opts.lt - 1
886- query.forwards = 1
887- }
888- cb(null, '<a href="?' + qs.stringify(query) + '">' +
889- req._t(query.forwards ? 'Older' : 'Newer') + '</a>')
900+ query.lt = opts.gt + 1
901+ } else {
902+ delete query.lt
903+ query.gt = opts.lt - 1
904+ query.forwards = 1
890905 }
891- )
906+ cb(null, '<a href="?' + qs.stringify(query) + '">' +
907+ req._t(query.forwards ? 'Older' : 'Newer') + '</a>')
908+ }
892909 )
893- }
910+ )
911+}
894912
895- function renderFeedItem(req, msg, cb) {
896- var c = msg.value.content
897- var msgLink = link([msg.key],
898- new Date(msg.value.timestamp).toLocaleString(req._locale))
899- var author = msg.value.author
900- var authorLink = link([msg.value.author], msg.authorName)
901- switch (c.type) {
902- case 'git-repo':
903- var done = multicb({ pluck: 1, spread: true })
904- getRepoName(about, author, msg.key, done())
905- if (c.upstream) {
906- return getMsg(c.upstream, function (err, upstreamMsg) {
907- if (err) return cb(null, serveError(req, err))
908- getRepoName(about, upstreamMsg.author, c.upstream, done())
909- done(function (err, repoName, upstreamName) {
910- cb(null, '<section class="collapse">' + msgLink + '<br>' +
911- req._t('Forked', {
912- name: authorLink,
913- upstream: link([c.upstream], upstreamName),
914- repo: link([msg.key], repoName)
915- }) + '</section>')
916- })
917- })
918- } else {
919- return done(function (err, repoName) {
920- if (err) return cb(err)
921- var repoLink = link([msg.key], repoName)
913+G.renderFeedItem = function (req, msg, cb) {
914+ var self = this
915+ var c = msg.value.content
916+ var msgLink = link([msg.key],
917+ new Date(msg.value.timestamp).toLocaleString(req._locale))
918+ var author = msg.value.author
919+ var authorLink = link([msg.value.author], msg.authorName)
920+ switch (c.type) {
921+ case 'git-repo':
922+ var done = multicb({ pluck: 1, spread: true })
923+ self.getRepoName(author, msg.key, done())
924+ if (c.upstream) {
925+ return self.getMsg(c.upstream, function (err, upstreamMsg) {
926+ if (err) return cb(null, self.serveError(req, err))
927+ self.getRepoName(upstreamMsg.author, c.upstream, done())
928+ done(function (err, repoName, upstreamName) {
922929 cb(null, '<section class="collapse">' + msgLink + '<br>' +
923- req._t('CreatedRepo', {
930+ req._t('Forked', {
924931 name: authorLink,
925- repo: repoLink
932+ upstream: link([c.upstream], upstreamName),
933+ repo: link([msg.key], repoName)
926934 }) + '</section>')
927935 })
928- }
929- case 'git-update':
930- return getRepoName(about, author, c.repo, function (err, repoName) {
936+ })
937+ } else {
938+ return done(function (err, repoName) {
931939 if (err) return cb(err)
932- var repoLink = link([c.repo], repoName)
940+ var repoLink = link([msg.key], repoName)
933941 cb(null, '<section class="collapse">' + msgLink + '<br>' +
934- req._t('Pushed', {
942+ req._t('CreatedRepo', {
935943 name: authorLink,
936944 repo: repoLink
937945 }) + '</section>')
938946 })
939- case 'issue':
940- case 'pull-request':
941- var issueLink = link([msg.key], c.title)
942- return getMsg(c.project, function (err, projectMsg) {
943- if (err) return cb(null, serveRepoNotFound(req, c.repo, err))
944- getRepoName(about, projectMsg.author, c.project,
945- function (err, repoName) {
946- if (err) return cb(err)
947- var repoLink = link([c.project], repoName)
948- cb(null, '<section class="collapse">' + msgLink + '<br>' +
949- req._t('OpenedIssue', {
950- name: authorLink,
951- type: req._t(c.type == 'pull-request' ?
952- 'pull request' : 'issue.'),
953- title: issueLink,
954- project: repoLink
955- }) + '</section>')
956- })
957- })
958- case 'about':
947+ }
948+ case 'git-update':
949+ return self.getRepoName(author, c.repo, function (err, repoName) {
950+ if (err) return cb(err)
951+ var repoLink = link([c.repo], repoName)
952+ cb(null, '<section class="collapse">' + msgLink + '<br>' +
953+ req._t('Pushed', {
954+ name: authorLink,
955+ repo: repoLink
956+ }) + '</section>')
957+ })
958+ case 'issue':
959+ case 'pull-request':
960+ var issueLink = link([msg.key], c.title)
961+ return self.getMsg(c.project, function (err, projectMsg) {
962+ if (err) return cb(null, self.serveRepoNotFound(req, c.repo, err))
963+ self.getRepoName(projectMsg.author, c.project,
964+ function (err, repoName) {
965+ if (err) return cb(err)
966+ var repoLink = link([c.project], repoName)
967+ cb(null, '<section class="collapse">' + msgLink + '<br>' +
968+ req._t('OpenedIssue', {
969+ name: authorLink,
970+ type: req._t(c.type == 'pull-request' ?
971+ 'pull request' : 'issue.'),
972+ title: issueLink,
973+ project: repoLink
974+ }) + '</section>')
975+ })
976+ })
977+ case 'about':
978+ return cb(null, '<section class="collapse">' + msgLink + '<br>' +
979+ req._t('Named', {
980+ author: authorLink,
981+ target: '<tt>' + escapeHTML(c.about) + '</tt>',
982+ name: link([c.about], c.name)
983+ }) + '</section>')
984+ case 'post':
985+ return this.pullReqs.get(c.issue, function (err, pr) {
986+ if (err) return cb(err)
987+ var type = pr.msg.value.content.type == 'pull-request' ?
988+ 'pull request' : 'issue.'
959989 return cb(null, '<section class="collapse">' + msgLink + '<br>' +
960- req._t('Named', {
990+ req._t('CommentedOn', {
961991 author: authorLink,
962- target: '<tt>' + escapeHTML(c.about) + '</tt>',
963- name: link([c.about], c.name)
964- }) + '</section>')
965- case 'post':
966- return pullReqs.get(c.issue, function (err, pr) {
967- if (err) return cb(err)
968- var type = pr.msg.value.content.type == 'pull-request' ?
969- 'pull request' : 'issue.'
970- return cb(null, '<section class="collapse">' + msgLink + '<br>' +
971- req._t('CommentedOn', {
972- author: authorLink,
973- target: req._t(type) + ' ' + link([pr.id], pr.title, true)
974- }) +
975- '<blockquote>' + markdown(c.text) + '</blockquote>' +
976- '</section>')
977- })
978- default:
979- return cb(null, json(msg))
980- }
992+ target: req._t(type) + ' ' + link([pr.id], pr.title, true)
993+ }) +
994+ '<blockquote>' + markdown(c.text) + '</blockquote>' +
995+ '</section>')
996+ })
997+ default:
998+ return cb(null, json(msg))
981999 }
1000+}
9821001
983- /* Index */
1002+/* Index */
9841003
985- function serveIndex(req) {
986- return serveTemplate(req)(renderFeed(req))
987- }
1004+G.serveIndex = function (req) {
1005+ return this.serveTemplate(req)(this.renderFeed(req))
1006+}
9881007
989- function serveUserPage(req, feedId, dirs) {
990- switch (dirs[0]) {
991- case undefined:
992- case '':
993- case 'activity':
994- return serveUserActivity(req, feedId)
995- case 'repos':
996- return serveUserRepos(req, feedId)
997- }
1008+G.serveUserPage = function (req, feedId, dirs) {
1009+ switch (dirs[0]) {
1010+ case undefined:
1011+ case '':
1012+ case 'activity':
1013+ return this.serveUserActivity(req, feedId)
1014+ case 'repos':
1015+ return this.serveUserRepos(req, feedId)
9981016 }
1017+}
9991018
1000- function renderUserPage(req, feedId, page, titleTemplate, body) {
1001- return readNext(function (cb) {
1002- about.getName(feedId, function (err, name) {
1003- if (err) return cb(err)
1004- var title = titleTemplate ? titleTemplate
1005- .replace(/\%{name\}/g, escapeHTML(name))
1006- : escapeHTML(name)
1007- cb(null, serveTemplate(req, title)(cat([
1008- pull.once('<h2>' + link([feedId], name) +
1009- '<code class="user-id">' + feedId + '</code></h2>' +
1010- nav([
1011- [[feedId], req._t('Activity'), 'activity'],
1012- [[feedId, 'repos'], req._t('Repos'), 'repos']
1013- ], page)),
1014- body
1015- ])))
1016- })
1019+G.renderUserPage = function (req, feedId, page, titleTemplate, body) {
1020+ var self = this
1021+ return readNext(function (cb) {
1022+ self.about.getName(feedId, function (err, name) {
1023+ if (err) return cb(err)
1024+ var title = titleTemplate ? titleTemplate
1025+ .replace(/\%{name\}/g, escapeHTML(name))
1026+ : escapeHTML(name)
1027+ cb(null, self.serveTemplate(req, title)(cat([
1028+ pull.once('<h2>' + link([feedId], name) +
1029+ '<code class="user-id">' + feedId + '</code></h2>' +
1030+ nav([
1031+ [[feedId], req._t('Activity'), 'activity'],
1032+ [[feedId, 'repos'], req._t('Repos'), 'repos']
1033+ ], page)),
1034+ body
1035+ ])))
10171036 })
1018- }
1037+ })
1038+}
10191039
1020- function serveUserActivity(req, feedId) {
1021- return renderUserPage(req, feedId, 'activity', null,
1022- renderFeed(req, feedId))
1023- }
1040+G.serveUserActivity = function (req, feedId) {
1041+ return this.renderUserPage(req, feedId, 'activity', null,
1042+ this.renderFeed(req, feedId))
1043+}
10241044
1025- function serveUserRepos(req, feedId) {
1026- var title = req._t('UsersRepos', {name: '%{name}'})
1027- return renderUserPage(req, feedId, 'repos', title, pull(
1028- cat([
1029- ssb.messagesByType({
1030- type: 'git-update',
1031- reverse: true
1032- }),
1033- ssb.messagesByType({
1034- type: 'git-repo',
1035- reverse: true
1036- })
1037- ]),
1038- pull.filter(function (msg) {
1039- return msg.value.author == feedId
1045+G.serveUserRepos = function (req, feedId) {
1046+ var self = this
1047+ var title = req._t('UsersRepos', {name: '%{name}'})
1048+ return this.renderUserPage(req, feedId, 'repos', title, pull(
1049+ cat([
1050+ self.ssb.messagesByType({
1051+ type: 'git-update',
1052+ reverse: true
10401053 }),
1041- pull.unique(function (msg) {
1042- return msg.value.content.repo || msg.key
1043- }),
1044- pull.take(20),
1045- paramap(function (msg, cb) {
1046- var repoId = msg.value.content.repo || msg.key
1047- var done = multicb({ pluck: 1, spread: true })
1048- getRepoName(about, feedId, repoId, done())
1049- getVotes(repoId, done())
1050- done(function (err, repoName, votes) {
1051- if (err) return cb(err)
1052- cb(null, '<section class="collapse">' +
1053- '<span class="right-bar">' +
1054- '<i>✌</i> ' +
1055- link([repoId, 'digs'], votes.upvotes, true,
1056- ' title="' + req._t('Digs') + '"') +
1057- '</span>' +
1058- '<strong>' + link([repoId], repoName) + '</strong>' +
1059- '<div class="date-info">' +
1060- req._t(msg.value.content.type == 'git-update' ?
1061- 'UpdatedOnDate' : 'CreatedOnDate',
1062- {
1063- date: timestamp(msg.value.timestamp, req)
1064- }) + '</div>' +
1065- '</section>')
1066- })
1067- }, 8)
1068- ))
1069- }
1054+ self.ssb.messagesByType({
1055+ type: 'git-repo',
1056+ reverse: true
1057+ })
1058+ ]),
1059+ pull.filter(function (msg) {
1060+ return msg.value.author == feedId
1061+ }),
1062+ pull.unique(function (msg) {
1063+ return msg.value.content.repo || msg.key
1064+ }),
1065+ pull.take(20),
1066+ paramap(function (msg, cb) {
1067+ var repoId = msg.value.content.repo || msg.key
1068+ var done = multicb({ pluck: 1, spread: true })
1069+ self.getRepoName(feedId, repoId, done())
1070+ self.getVotes(repoId, done())
1071+ done(function (err, repoName, votes) {
1072+ if (err) return cb(err)
1073+ cb(null, '<section class="collapse">' +
1074+ '<span class="right-bar">' +
1075+ '<i>✌</i> ' +
1076+ link([repoId, 'digs'], votes.upvotes, true,
1077+ ' title="' + req._t('Digs') + '"') +
1078+ '</span>' +
1079+ '<strong>' + link([repoId], repoName) + '</strong>' +
1080+ '<div class="date-info">' +
1081+ req._t(msg.value.content.type == 'git-update' ?
1082+ 'UpdatedOnDate' : 'CreatedOnDate',
1083+ {
1084+ date: timestamp(msg.value.timestamp, req)
1085+ }) + '</div>' +
1086+ '</section>')
1087+ })
1088+ }, 8)
1089+ ))
1090+}
10701091
10711092 /* Message */
10721093
1073- function serveMessage(req, id, path) {
1074- return readNext(function (cb) {
1075- ssb.get(id, function (err, msg) {
1076- if (err) return cb(null, serveError(req, err))
1077- var c = msg.content || {}
1078- switch (c.type) {
1079- case 'git-repo':
1080- return getRepo(id, function (err, repo) {
1081- if (err) return cb(null, serveError(req, err))
1082- cb(null, serveRepoPage(req, Repo(repo), path))
1094+G.serveMessage = function (req, id, path) {
1095+ var self = this
1096+ return readNext(function (cb) {
1097+ self.ssb.get(id, function (err, msg) {
1098+ if (err) return cb(null, self.serveError(req, err))
1099+ var c = msg.content || {}
1100+ switch (c.type) {
1101+ case 'git-repo':
1102+ return self.getRepo(id, function (err, repo) {
1103+ if (err) return cb(null, self.serveError(req, err))
1104+ cb(null, self.serveRepoPage(req, Repo(repo), path))
1105+ })
1106+ case 'git-update':
1107+ return self.getRepo(c.repo, function (err, repo) {
1108+ if (err) return cb(null, self.serveRepoNotFound(req, c.repo, err))
1109+ cb(null, self.serveRepoUpdate(req, Repo(repo), id, msg, path))
1110+ })
1111+ case 'issue':
1112+ return self.getRepo(c.project, function (err, repo) {
1113+ if (err) return cb(null,
1114+ self.serveRepoNotFound(req, c.project, err))
1115+ self.issues.get(id, function (err, issue) {
1116+ if (err) return cb(null, self.serveError(req, err))
1117+ cb(null, self.serveRepoIssue(req, Repo(repo), issue, path))
10831118 })
1084- case 'git-update':
1085- return getRepo(c.repo, function (err, repo) {
1086- if (err) return cb(null, serveRepoNotFound(req, c.repo, err))
1087- cb(null, serveRepoUpdate(req, Repo(repo), id, msg, path))
1119+ })
1120+ case 'pull-request':
1121+ return self.getRepo(c.repo, function (err, repo) {
1122+ if (err) return cb(null,
1123+ self.serveRepoNotFound(req, c.project, err))
1124+ self.pullReqs.get(id, function (err, pr) {
1125+ if (err) return cb(null, self.serveError(req, err))
1126+ cb(null, self.serveRepoPullReq(req, Repo(repo), pr, path))
10881127 })
1089- case 'issue':
1090- return getRepo(c.project, function (err, repo) {
1091- if (err) return cb(null, serveRepoNotFound(req, c.project, err))
1092- issues.get(id, function (err, issue) {
1093- if (err) return cb(null, serveError(req, err))
1094- cb(null, serveRepoIssue(req, Repo(repo), issue, path))
1128+ })
1129+ case 'issue-edit':
1130+ if (ref.isMsgId(c.issue)) {
1131+ return self.pullReqs.get(c.issue, function (err, issue) {
1132+ if (err) return cb(err)
1133+ var serve = issue.msg.value.content.type == 'pull-request' ?
1134+ self.serveRepoPullReq : self.serveRepoIssue
1135+ self.getRepo(issue.project, function (err, repo) {
1136+ if (err) {
1137+ if (!repo) return cb(null,
1138+ self.serveRepoNotFound(req, c.repo, err))
1139+ return cb(null, self.serveError(req, err))
1140+ }
1141+ cb(null, serve.call(self, req, Repo(repo), issue, path, id))
10951142 })
10961143 })
1097- case 'pull-request':
1098- return getRepo(c.repo, function (err, repo) {
1099- if (err) return cb(null, serveRepoNotFound(req, c.project, err))
1100- pullReqs.get(id, function (err, pr) {
1101- if (err) return cb(null, serveError(req, err))
1102- cb(null, serveRepoPullReq(req, Repo(repo), pr, path))
1103- })
1144+ }
1145+ // fallthrough
1146+ case 'post':
1147+ if (ref.isMsgId(c.issue) && ref.isMsgId(c.repo)) {
1148+ // comment on an issue
1149+ var done = multicb({ pluck: 1, spread: true })
1150+ self.getRepo(c.repo, done())
1151+ self.pullReqs.get(c.issue, done())
1152+ return done(function (err, repo, issue) {
1153+ if (err) {
1154+ if (!repo) return cb(null,
1155+ self.serveRepoNotFound(req, c.repo, err))
1156+ return cb(null, self.serveError(req, err))
1157+ }
1158+ var serve = issue.msg.value.content.type == 'pull-request' ?
1159+ self.serveRepoPullReq : self.serveRepoIssue
1160+ cb(null, serve.call(self, req, Repo(repo), issue, path, id))
11041161 })
1105- case 'issue-edit':
1106- if (ref.isMsgId(c.issue)) {
1107- return pullReqs.get(c.issue, function (err, issue) {
1108- if (err) return cb(err)
1109- var serve = issue.msg.value.content.type == 'pull-request' ?
1110- serveRepoPullReq : serveRepoIssue
1111- getRepo(issue.project, function (err, repo) {
1112- if (err) {
1113- if (!repo) return cb(null,
1114- serveRepoNotFound(req, c.repo, err))
1115- return cb(null, serveError(req, err))
1116- }
1117- cb(null, serve(req, Repo(repo), issue, path, id))
1118- })
1119- })
1120- }
1121- // fallthrough
1122- case 'post':
1123- if (ref.isMsgId(c.issue) && ref.isMsgId(c.repo)) {
1124- // comment on an issue
1125- var done = multicb({ pluck: 1, spread: true })
1126- getRepo(c.repo, done())
1127- pullReqs.get(c.issue, done())
1128- return done(function (err, repo, issue) {
1129- if (err) {
1130- if (!repo) return cb(null,
1131- serveRepoNotFound(req, c.repo, err))
1132- return cb(null, serveError(req, err))
1162+ } else if (ref.isMsgId(c.root)) {
1163+ // comment on issue from patchwork?
1164+ return self.getMsg(c.root, function (err, root) {
1165+ if (err) return cb(null, self.serveError(req, err))
1166+ var repoId = root.content.repo || root.content.project
1167+ if (!ref.isMsgId(repoId))
1168+ return cb(null, self.serveGenericMessage(req, id, msg, path))
1169+ self.getRepo(repoId, function (err, repo) {
1170+ if (err) return cb(null, self.serveError(req, err))
1171+ switch (root.content && root.content.type) {
1172+ case 'issue':
1173+ return self.issues.get(c.root, function (err, issue) {
1174+ if (err) return cb(null, self.serveError(req, err))
1175+ return cb(null,
1176+ self.serveRepoIssue(req, Repo(repo), issue, path, id))
1177+ })
1178+ case 'pull-request':
1179+ return self.pullReqs.get(c.root, function (err, pr) {
1180+ if (err) return cb(null, self.serveError(req, err))
1181+ return cb(null,
1182+ self.serveRepoPullReq(req, Repo(repo), pr, path, id))
1183+ })
11331184 }
1134- var serve = issue.msg.value.content.type == 'pull-request' ?
1135- serveRepoPullReq : serveRepoIssue
1136- cb(null, serve(req, Repo(repo), issue, path, id))
11371185 })
1138- } else if (ref.isMsgId(c.root)) {
1139- // comment on issue from patchwork?
1140- return getMsg(c.root, function (err, root) {
1141- if (err) return cb(null, serveError(req, err))
1142- var repoId = root.content.repo || root.content.project
1143- if (!ref.isMsgId(repoId))
1144- return cb(null, serveGenericMessage(req, id, msg, path))
1145- getRepo(repoId, function (err, repo) {
1146- if (err) return cb(null, serveError(req, err))
1147- switch (root.content && root.content.type) {
1148- case 'issue':
1149- return issues.get(c.root, function (err, issue) {
1150- if (err) return cb(null, serveError(req, err))
1151- return cb(null,
1152- serveRepoIssue(req, Repo(repo), issue, path, id))
1153- })
1154- case 'pull-request':
1155- pullReqs.get(c.root, function (err, pr) {
1156- if (err) return cb(null, serveError(req, err))
1157- return cb(null,
1158- serveRepoPullReq(req, Repo(repo), pr, path, id))
1159- })
1160- }
1161- })
1162- })
1163- }
1164- // fallthrough
1165- default:
1166- if (ref.isMsgId(c.repo))
1167- return getRepo(c.repo, function (err, repo) {
1168- if (err) return cb(null, serveRepoNotFound(req, c.repo, err))
1169- cb(null, serveRepoSomething(req, Repo(repo), id, msg, path))
1170- })
1171- else
1172- return cb(null, serveGenericMessage(req, id, msg, path))
1173- }
1174- })
1186+ })
1187+ }
1188+ // fallthrough
1189+ default:
1190+ if (ref.isMsgId(c.repo))
1191+ return self.getRepo(c.repo, function (err, repo) {
1192+ if (err) return cb(null,
1193+ self.serveRepoNotFound(req, c.repo, err))
1194+ cb(null, self.serveRepoSomething(req, Repo(repo), id, msg, path))
1195+ })
1196+ else
1197+ return cb(null, self.serveGenericMessage(req, id, msg, path))
1198+ }
11751199 })
1176- }
1200+ })
1201+}
11771202
1178- function serveGenericMessage(req, id, msg, path) {
1179- return serveTemplate(req, id)(pull.once(
1180- '<section><h2>' + link([id]) + '</h2>' +
1181- json(msg) +
1182- '</section>'))
1183- }
1203+G.serveGenericMessage = function (req, id, msg, path) {
1204+ return this.serveTemplate(req, id)(pull.once(
1205+ '<section><h2>' + link([id]) + '</h2>' +
1206+ json(msg) +
1207+ '</section>'))
1208+}
11841209
11851210 /* Repo */
11861211
1187- function serveRepoPage(req, repo, path) {
1188- var defaultBranch = 'master'
1189- var query = req._u.query
1212+G.serveRepoPage = function (req, repo, path) {
1213+ var self = this
1214+ var defaultBranch = 'master'
1215+ var query = req._u.query
11901216
1191- if (query.rev != null) {
1192- // Allow navigating revs using GET query param.
1193- // Replace the branch in the path with the rev query value
1194- path[0] = path[0] || 'tree'
1195- path[1] = query.rev
1196- req._u.pathname = encodeLink([repo.id].concat(path))
1197- delete req._u.query.rev
1198- delete req._u.search
1199- return serveRedirect(req, url.format(req._u))
1200- }
1217+ if (query.rev != null) {
1218+ // Allow navigating revs using GET query param.
1219+ // Replace the branch in the path with the rev query value
1220+ path[0] = path[0] || 'tree'
1221+ path[1] = query.rev
1222+ req._u.pathname = encodeLink([repo.id].concat(path))
1223+ delete req._u.query.rev
1224+ delete req._u.search
1225+ return self.serveRedirect(req, url.format(req._u))
1226+ }
12011227
1202- // get branch
1203- return path[1] ?
1204- serveRepoPage2(req, repo, path) :
1205- readNext(function (cb) {
1206- // TODO: handle this in pull-git-repo or ssb-git-repo
1207- repo.getSymRef('HEAD', true, function (err, ref) {
1208- if (err) return cb(err)
1209- repo.resolveRef(ref, function (err, rev) {
1210- path[1] = rev ? ref : null
1211- cb(null, serveRepoPage2(req, repo, path))
1212- })
1228+ // get branch
1229+ return path[1] ?
1230+ G_serveRepoPage2.call(self, req, repo, path) :
1231+ readNext(function (cb) {
1232+ // TODO: handle this in pull-git-repo or ssb-git-repo
1233+ repo.getSymRef('HEAD', true, function (err, ref) {
1234+ if (err) return cb(err)
1235+ repo.resolveRef(ref, function (err, rev) {
1236+ path[1] = rev ? ref : null
1237+ cb(null, G_serveRepoPage2.call(self, req, repo, path))
12131238 })
12141239 })
1215- }
1240+ })
1241+}
12161242
1217- function serveRepoPage2(req, repo, path) {
1218- var branch = path[1]
1219- var filePath = path.slice(2)
1220- switch (path[0]) {
1221- case undefined:
1222- case '':
1223- return serveRepoTree(req, repo, branch, [])
1224- case 'activity':
1225- return serveRepoActivity(req, repo, branch)
1226- case 'commits':
1227- return serveRepoCommits(req, repo, branch)
1228- case 'commit':
1229- return serveRepoCommit(req, repo, path[1])
1230- case 'tag':
1231- return serveRepoTag(req, repo, branch)
1232- case 'tree':
1233- return serveRepoTree(req, repo, branch, filePath)
1234- case 'blob':
1235- return serveRepoBlob(req, repo, branch, filePath)
1236- case 'raw':
1237- return serveRepoRaw(req, repo, branch, filePath)
1238- case 'digs':
1239- return serveRepoDigs(req, repo)
1240- case 'fork':
1241- return serveRepoForkPrompt(req, repo)
1242- case 'forks':
1243- return serveRepoForks(req, repo)
1244- case 'issues':
1245- switch (path[1]) {
1246- case 'new':
1247- if (filePath.length == 0)
1248- return serveRepoNewIssue(req, repo)
1249- break
1250- default:
1251- return serveRepoIssues(req, repo, false)
1252- }
1253- case 'pulls':
1254- return serveRepoIssues(req, repo, true)
1255- case 'compare':
1256- return serveRepoCompare(req, repo)
1257- case 'comparing':
1258- return serveRepoComparing(req, repo)
1259- default:
1260- return serve404(req)
1261- }
1243+function G_serveRepoPage2(req, repo, path) {
1244+ var branch = path[1]
1245+ var filePath = path.slice(2)
1246+ switch (path[0]) {
1247+ case undefined:
1248+ case '':
1249+ return this.serveRepoTree(req, repo, branch, [])
1250+ case 'activity':
1251+ return this.serveRepoActivity(req, repo, branch)
1252+ case 'commits':
1253+ return this.serveRepoCommits(req, repo, branch)
1254+ case 'commit':
1255+ return this.serveRepoCommit(req, repo, path[1])
1256+ case 'tag':
1257+ return this.serveRepoTag(req, repo, branch)
1258+ case 'tree':
1259+ return this.serveRepoTree(req, repo, branch, filePath)
1260+ case 'blob':
1261+ return this.serveRepoBlob(req, repo, branch, filePath)
1262+ case 'raw':
1263+ return this.serveRepoRaw(req, repo, branch, filePath)
1264+ case 'digs':
1265+ return this.serveRepoDigs(req, repo)
1266+ case 'fork':
1267+ return this.serveRepoForkPrompt(req, repo)
1268+ case 'forks':
1269+ return this.serveRepoForks(req, repo)
1270+ case 'issues':
1271+ switch (path[1]) {
1272+ case 'new':
1273+ if (filePath.length == 0)
1274+ return this.serveRepoNewIssue(req, repo)
1275+ break
1276+ default:
1277+ return this.serveRepoIssues(req, repo, false)
1278+ }
1279+ case 'pulls':
1280+ return this.serveRepoIssues(req, repo, true)
1281+ case 'compare':
1282+ return this.serveRepoCompare(req, repo)
1283+ case 'comparing':
1284+ return this.serveRepoComparing(req, repo)
1285+ default:
1286+ return this.serve404(req)
12621287 }
1288+}
12631289
1264- function serveRepoNotFound(req, id, err) {
1265- return serveTemplate(req, req._t('error.RepoNotFound'), 404)(pull.values([
1266- '<h2>' + req._t('error.RepoNotFound') + '</h2>',
1267- '<p>' + req._t('error.RepoNameNotFound') + '</p>',
1268- '<pre>' + escapeHTML(err.stack) + '</pre>'
1269- ]))
1270- }
1290+G.serveRepoNotFound = function (req, id, err) {
1291+ return this.serveTemplate(req, req._t('error.RepoNotFound'), 404)
1292+ (pull.values([
1293+ '<h2>' + req._t('error.RepoNotFound') + '</h2>',
1294+ '<p>' + req._t('error.RepoNameNotFound') + '</p>',
1295+ '<pre>' + escapeHTML(err.stack) + '</pre>'
1296+ ]))
1297+}
12711298
1272- function renderRepoPage(req, repo, page, branch, titleTemplate, body) {
1273- var gitUrl = 'ssb://' + repo.id
1274- var gitLink = '<input class="clone-url" readonly="readonly" ' +
1275- 'value="' + gitUrl + '" size="45" ' +
1276- 'onclick="this.select()"/>'
1277- var digsPath = [repo.id, 'digs']
1299+G.renderRepoPage = function (req, repo, page, branch, titleTemplate, body) {
1300+ var self = this
1301+ var gitUrl = 'ssb://' + repo.id
1302+ var gitLink = '<input class="clone-url" readonly="readonly" ' +
1303+ 'value="' + gitUrl + '" size="45" ' +
1304+ 'onclick="this.select()"/>'
1305+ var digsPath = [repo.id, 'digs']
12781306
1279- var done = multicb({ pluck: 1, spread: true })
1280- getRepoName(about, repo.feed, repo.id, done())
1281- about.getName(repo.feed, done())
1282- getVotes(repo.id, done())
1307+ var done = multicb({ pluck: 1, spread: true })
1308+ self.getRepoName(repo.feed, repo.id, done())
1309+ self.about.getName(repo.feed, done())
1310+ self.getVotes(repo.id, done())
12831311
1284- if (repo.upstream) {
1285- getRepoName(about, repo.upstream.feed, repo.upstream.id, done())
1286- about.getName(repo.upstream.feed, done())
1287- }
1312+ if (repo.upstream) {
1313+ self.getRepoName(repo.upstream.feed, repo.upstream.id, done())
1314+ self.about.getName(repo.upstream.feed, done())
1315+ }
12881316
1289- return readNext(function (cb) {
1290- done(function (err, repoName, authorName, votes,
1291- upstreamName, upstreamAuthorName) {
1292- if (err) return cb(null, serveError(req, err))
1293- var upvoted = votes.upvoters[myId] > 0
1294- var upstreamLink = !repo.upstream ? '' :
1295- link([repo.upstream])
1296- var title = titleTemplate ? titleTemplate
1297- .replace(/%\{repo\}/g, repoName)
1298- .replace(/%\{author\}/g, authorName)
1299- : authorName + '/' + repoName
1300- cb(null, serveTemplate(req, title)(cat([
1301- pull.once(
1302- '<div class="repo-title">' +
1303- '<form class="right-bar" action="" method="post">' +
1304- '<button class="btn" name="action" value="vote" ' +
1305- (isPublic ? 'disabled="disabled"' : ' type="submit"') + '>' +
1306- '<i>✌</i> ' + req._t(!isPublic && upvoted ? 'Undig' : 'Dig') +
1307- '</button>' +
1308- (isPublic ? '' : '<input type="hidden" name="value" value="' +
1309- (upvoted ? '0' : '1') + '">' +
1310- '<input type="hidden" name="id" value="' +
1311- escapeHTML(repo.id) + '">') + ' ' +
1312- '<strong>' + link(digsPath, votes.upvotes) + '</strong> ' +
1313- (isPublic ? '' : '<button class="btn" type="submit" ' +
1314- ' name="action" value="fork-prompt">' +
1315- '<i>⑂</i> ' + req._t('Fork') +
1316- '</button>') + ' ' +
1317- link([repo.id, 'forks'], '+', false, ' title="' +
1318- req._t('Forks') + '"') +
1319- '</form>' +
1320- renderNameForm(req, !isPublic, repo.id, repoName, 'repo-name',
1321- null, req._t('repo.Rename'),
1322- '<h2 class="bgslash">' + link([repo.feed], authorName) + ' / ' +
1323- link([repo.id], repoName) + '</h2>') +
1324- '</div>' +
1325- (repo.upstream ? '<small class="bgslash">' + req._t('ForkedFrom', {
1326- repo: link([repo.upstream.feed], upstreamAuthorName) + '/' +
1327- link([repo.upstream.id], upstreamName)
1328- }) + '</small>' : '') +
1329- nav([
1330- [[repo.id], req._t('Code'), 'code'],
1331- [[repo.id, 'activity'], req._t('Activity'), 'activity'],
1332- [[repo.id, 'commits', branch||''], req._t('Commits'), 'commits'],
1333- [[repo.id, 'issues'], req._t('Issues'), 'issues'],
1334- [[repo.id, 'pulls'], req._t('PullRequests'), 'pulls']
1335- ], page, gitLink)),
1336- body
1337- ])))
1338- })
1317+ return readNext(function (cb) {
1318+ done(function (err, repoName, authorName, votes,
1319+ upstreamName, upstreamAuthorName) {
1320+ if (err) return cb(null, self.serveError(req, err))
1321+ var upvoted = votes.upvoters[self.myId] > 0
1322+ var upstreamLink = !repo.upstream ? '' :
1323+ link([repo.upstream])
1324+ var title = titleTemplate ? titleTemplate
1325+ .replace(/%\{repo\}/g, repoName)
1326+ .replace(/%\{author\}/g, authorName)
1327+ : authorName + '/' + repoName
1328+ var isPublic = self.isPublic
1329+ cb(null, self.serveTemplate(req, title)(cat([
1330+ pull.once(
1331+ '<div class="repo-title">' +
1332+ '<form class="right-bar" action="" method="post">' +
1333+ '<button class="btn" name="action" value="vote" ' +
1334+ (isPublic ? 'disabled="disabled"' : ' type="submit"') + '>' +
1335+ '<i>✌</i> ' + req._t(!isPublic && upvoted ? 'Undig' : 'Dig') +
1336+ '</button>' +
1337+ (isPublic ? '' : '<input type="hidden" name="value" value="' +
1338+ (upvoted ? '0' : '1') + '">' +
1339+ '<input type="hidden" name="id" value="' +
1340+ escapeHTML(repo.id) + '">') + ' ' +
1341+ '<strong>' + link(digsPath, votes.upvotes) + '</strong> ' +
1342+ (isPublic ? '' : '<button class="btn" type="submit" ' +
1343+ ' name="action" value="fork-prompt">' +
1344+ '<i>⑂</i> ' + req._t('Fork') +
1345+ '</button>') + ' ' +
1346+ link([repo.id, 'forks'], '+', false, ' title="' +
1347+ req._t('Forks') + '"') +
1348+ '</form>' +
1349+ renderNameForm(req, !isPublic, repo.id, repoName, 'repo-name',
1350+ null, req._t('repo.Rename'),
1351+ '<h2 class="bgslash">' + link([repo.feed], authorName) + ' / ' +
1352+ link([repo.id], repoName) + '</h2>') +
1353+ '</div>' +
1354+ (repo.upstream ? '<small class="bgslash">' + req._t('ForkedFrom', {
1355+ repo: link([repo.upstream.feed], upstreamAuthorName) + '/' +
1356+ link([repo.upstream.id], upstreamName)
1357+ }) + '</small>' : '') +
1358+ nav([
1359+ [[repo.id], req._t('Code'), 'code'],
1360+ [[repo.id, 'activity'], req._t('Activity'), 'activity'],
1361+ [[repo.id, 'commits', branch||''], req._t('Commits'), 'commits'],
1362+ [[repo.id, 'issues'], req._t('Issues'), 'issues'],
1363+ [[repo.id, 'pulls'], req._t('PullRequests'), 'pulls']
1364+ ], page, gitLink)),
1365+ body
1366+ ])))
13391367 })
1340- }
1368+ })
1369+}
13411370
1342- function serveEmptyRepo(req, repo) {
1343- if (repo.feed != myId)
1344- return renderRepoPage(req, repo, 'code', null, null, pull.once(
1345- '<section>' +
1346- '<h3>' + req._t('EmptyRepo') + '</h3>' +
1347- '</section>'))
1348-
1349- var gitUrl = 'ssb://' + repo.id
1350- return renderRepoPage(req, repo, 'code', null, null, pull.once(
1371+G.serveEmptyRepo = function (req, repo) {
1372+ if (repo.feed != this.myId)
1373+ return this.renderRepoPage(req, repo, 'code', null, null, pull.once(
13511374 '<section>' +
1352- '<h3>' + req._t('initRepo.GettingStarted') + '</h3>' +
1353- '<h4>' + req._t('initRepo.CreateNew') + '</h4><pre>' +
1354- 'touch ' + req._t('initRepo.README') + '.md\n' +
1355- 'git init\n' +
1356- 'git add ' + req._t('initRepo.README') + '.md\n' +
1357- 'git commit -m "' + req._t('initRepo.InitialCommit') + '"\n' +
1358- 'git remote add origin ' + gitUrl + '\n' +
1359- 'git push -u origin master</pre>\n' +
1360- '<h4>' + req._t('initRepo.PushExisting') + '</h4>\n' +
1361- '<pre>git remote add origin ' + gitUrl + '\n' +
1362- 'git push -u origin master</pre>' +
1375+ '<h3>' + req._t('EmptyRepo') + '</h3>' +
13631376 '</section>'))
1364- }
13651377
1366- function serveRepoTree(req, repo, rev, path) {
1367- if (!rev) return serveEmptyRepo(req, repo)
1368- var type = repo.isCommitHash(rev) ? 'Tree' : 'Branch'
1369- var title = (path.length ? path.join('/') + ' · ' : '') +
1370- '%{author}/%{repo}' +
1371- (repo.head == 'refs/heads/' + rev ? '' : '@' + rev)
1372- return renderRepoPage(req, repo, 'code', rev, title, cat([
1373- pull.once('<section><form action="" method="get">' +
1374- '<h3>' + req._t(type) + ': ' + rev + ' '),
1375- revMenu(req, repo, rev),
1376- pull.once('</h3></form>'),
1377- type == 'Branch' && renderRepoLatest(req, repo, rev),
1378- pull.once('</section><section>'),
1379- renderRepoTree(req, repo, rev, path),
1380- pull.once('</section>'),
1381- renderRepoReadme(req, repo, rev, path)
1382- ]))
1383- }
1378+ var gitUrl = 'ssb://' + repo.id
1379+ return this.renderRepoPage(req, repo, 'code', null, null, pull.once(
1380+ '<section>' +
1381+ '<h3>' + req._t('initRepo.GettingStarted') + '</h3>' +
1382+ '<h4>' + req._t('initRepo.CreateNew') + '</h4><pre>' +
1383+ 'touch ' + req._t('initRepo.README') + '.md\n' +
1384+ 'git init\n' +
1385+ 'git add ' + req._t('initRepo.README') + '.md\n' +
1386+ 'git commit -m "' + req._t('initRepo.InitialCommit') + '"\n' +
1387+ 'git remote add origin ' + gitUrl + '\n' +
1388+ 'git push -u origin master</pre>\n' +
1389+ '<h4>' + req._t('initRepo.PushExisting') + '</h4>\n' +
1390+ '<pre>git remote add origin ' + gitUrl + '\n' +
1391+ 'git push -u origin master</pre>' +
1392+ '</section>'))
1393+}
13841394
1385- /* Search */
1395+G.serveRepoTree = function (req, repo, rev, path) {
1396+ if (!rev) return this.serveEmptyRepo(req, repo)
1397+ var type = repo.isCommitHash(rev) ? 'Tree' : 'Branch'
1398+ var title = (path.length ? path.join('/') + ' · ' : '') +
1399+ '%{author}/%{repo}' +
1400+ (repo.head == 'refs/heads/' + rev ? '' : '@' + rev)
1401+ return this.renderRepoPage(req, repo, 'code', rev, title, cat([
1402+ pull.once('<section><form action="" method="get">' +
1403+ '<h3>' + req._t(type) + ': ' + rev + ' '),
1404+ revMenu(req, repo, rev),
1405+ pull.once('</h3></form>'),
1406+ type == 'Branch' && renderRepoLatest(req, repo, rev),
1407+ pull.once('</section><section>'),
1408+ renderRepoTree(req, repo, rev, path),
1409+ pull.once('</section>'),
1410+ renderRepoReadme(req, repo, rev, path)
1411+ ]))
1412+}
13861413
1387- function serveSearch(req) {
1388- var q = String(req._u.query.q || '')
1389- if (!q) return serveIndex(req)
1390- var qId = q.replace(/^ssb:\/*/, '')
1391- if (ref.type(qId))
1392- return serveRedirect(req, encodeURIComponent(qId))
1414+/* Search */
13931415
1394- var search = new RegExp(q, 'i')
1395- return serveTemplate(req, req._t('Search') + ' &middot; ' + q, 200)(
1396- renderFeed(req, null, function (opts) {
1397- opts.type == 'about'
1398- return function (read) {
1399- return pull(
1400- many([
1401- getRepoNames(opts),
1402- read
1403- ]),
1404- pull.filter(function (msg) {
1405- var c = msg.value.content
1406- return (
1407- search.test(msg.key) ||
1408- c.text && search.test(c.text) ||
1409- c.name && search.test(c.name) ||
1410- c.title && search.test(c.title))
1411- })
1412- )
1413- }
1414- })
1415- )
1416- }
1416+G.serveSearch = function (req) {
1417+ var self = this
1418+ var q = String(req._u.query.q || '')
1419+ if (!q) return this.serveIndex(req)
1420+ var qId = q.replace(/^ssb:\/*/, '')
1421+ if (ref.type(qId))
1422+ return this.serveRedirect(req, encodeURIComponent(qId))
14171423
1418- function getRepoNames(opts) {
1419- return pull(
1420- ssb.messagesByType({
1421- type: 'about',
1422- reverse: opts.reverse,
1423- lt: opts.lt,
1424- gt: opts.gt,
1424+ var search = new RegExp(q, 'i')
1425+ return this.serveTemplate(req, req._t('Search') + ' &middot; ' + q, 200)(
1426+ this.renderFeed(req, null, function (opts) {
1427+ opts.type == 'about'
1428+ return function (read) {
1429+ return pull(
1430+ many([
1431+ self.getRepoNames(opts),
1432+ read
1433+ ]),
1434+ pull.filter(function (msg) {
1435+ var c = msg.value.content
1436+ return (
1437+ search.test(msg.key) ||
1438+ c.text && search.test(c.text) ||
1439+ c.name && search.test(c.name) ||
1440+ c.title && search.test(c.title))
1441+ })
1442+ )
1443+ }
1444+ })
1445+ )
1446+}
1447+
1448+G.getRepoNames = function (opts) {
1449+ return pull(
1450+ this.ssb.messagesByType({
1451+ type: 'about',
1452+ reverse: opts.reverse,
1453+ lt: opts.lt,
1454+ gt: opts.gt,
1455+ }),
1456+ pull.filter(function (msg) {
1457+ return '%' == String(msg.value.content.about)[0]
1458+ && msg.value.content.name
1459+ })
1460+ )
1461+}
1462+
1463+/* Repo activity */
1464+
1465+G.serveRepoActivity = function (req, repo, branch) {
1466+ var self = this
1467+ var title = req._t('Activity') + ' · %{author}/%{repo}'
1468+ return self.renderRepoPage(req, repo, 'activity', branch, title, cat([
1469+ pull.once('<h3>' + req._t('Activity') + '</h3>'),
1470+ pull(
1471+ self.ssb.links({
1472+ dest: repo.id,
1473+ source: repo.feed,
1474+ rel: 'repo',
1475+ values: true,
1476+ reverse: true
14251477 }),
1426- pull.filter(function (msg) {
1427- return '%' == String(msg.value.content.about)[0]
1428- && msg.value.content.name
1478+ pull.map(renderRepoUpdate.bind(self, req, repo))
1479+ ),
1480+ readOnce(function (cb) {
1481+ var done = multicb({ pluck: 1, spread: true })
1482+ self.about.getName(repo.feed, done())
1483+ self.getMsg(repo.id, done())
1484+ done(function (err, authorName, msg) {
1485+ if (err) return cb(err)
1486+ self.renderFeedItem(req, {
1487+ key: repo.id,
1488+ value: msg,
1489+ authorName: authorName
1490+ }, cb)
14291491 })
1430- )
1431- }
1492+ })
1493+ ]))
1494+}
14321495
1433- /* Repo activity */
1496+function renderRepoUpdate(req, repo, msg, full) {
1497+ var c = msg.value.content
14341498
1435- function serveRepoActivity(req, repo, branch) {
1436- var title = req._t('Activity') + ' · %{author}/%{repo}'
1437- return renderRepoPage(req, repo, 'activity', branch, title, cat([
1438- pull.once('<h3>' + req._t('Activity') + '</h3>'),
1439- pull(
1440- ssb.links({
1441- dest: repo.id,
1442- source: repo.feed,
1443- rel: 'repo',
1444- values: true,
1445- reverse: true
1446- }),
1447- pull.map(renderRepoUpdate.bind(this, req, repo))
1448- ),
1449- readOnce(function (cb) {
1450- var done = multicb({ pluck: 1, spread: true })
1451- about.getName(repo.feed, done())
1452- getMsg(repo.id, done())
1453- done(function (err, authorName, msg) {
1454- if (err) return cb(err)
1455- renderFeedItem(req, {
1456- key: repo.id,
1457- value: msg,
1458- authorName: authorName
1459- }, cb)
1460- })
1461- })
1462- ]))
1499+ if (c.type != 'git-update') {
1500+ return ''
1501+ // return renderFeedItem(msg, cb)
1502+ // TODO: render post, issue, pull-request
14631503 }
14641504
1465- function renderRepoUpdate(req, repo, msg, full) {
1466- var c = msg.value.content
1505+ var branches = []
1506+ var tags = []
1507+ if (c.refs) for (var name in c.refs) {
1508+ var m = name.match(/^refs\/(heads|tags)\/(.*)$/) || [,, name]
1509+ ;(m[1] == 'tags' ? tags : branches)
1510+ .push({name: m[2], value: c.refs[name]})
1511+ }
1512+ var numObjects = c.objects ? Object.keys(c.objects).length : 0
14671513
1468- if (c.type != 'git-update') {
1469- return ''
1470- // return renderFeedItem(msg, cb)
1471- // TODO: render post, issue, pull-request
1472- }
1514+ var dateStr = new Date(msg.value.timestamp).toLocaleString(req._locale)
1515+ return '<section class="collapse">' +
1516+ link([msg.key], dateStr) + '<br>' +
1517+ branches.map(function (update) {
1518+ if (!update.value) {
1519+ return '<s>' + escapeHTML(update.name) + '</s><br/>'
1520+ } else {
1521+ var commitLink = link([repo.id, 'commit', update.value])
1522+ var branchLink = link([repo.id, 'tree', update.name])
1523+ return branchLink + ' &rarr; <tt>' + commitLink + '</tt><br/>'
1524+ }
1525+ }).join('') +
1526+ tags.map(function (update) {
1527+ return update.value
1528+ ? link([repo.id, 'tag', update.value], update.name)
1529+ : '<s>' + escapeHTML(update.name) + '</s>'
1530+ }).join(', ') +
1531+ '</section>'
1532+}
14731533
1474- var branches = []
1475- var tags = []
1476- if (c.refs) for (var name in c.refs) {
1477- var m = name.match(/^refs\/(heads|tags)\/(.*)$/) || [,, name]
1478- ;(m[1] == 'tags' ? tags : branches)
1479- .push({name: m[2], value: c.refs[name]})
1480- }
1481- var numObjects = c.objects ? Object.keys(c.objects).length : 0
1534+/* Repo commits */
14821535
1483- var dateStr = new Date(msg.value.timestamp).toLocaleString(req._locale)
1484- return '<section class="collapse">' +
1485- link([msg.key], dateStr) + '<br>' +
1486- branches.map(function (update) {
1487- if (!update.value) {
1488- return '<s>' + escapeHTML(update.name) + '</s><br/>'
1489- } else {
1490- var commitLink = link([repo.id, 'commit', update.value])
1491- var branchLink = link([repo.id, 'tree', update.name])
1492- return branchLink + ' &rarr; <tt>' + commitLink + '</tt><br/>'
1536+G.serveRepoCommits = function (req, repo, branch) {
1537+ var query = req._u.query
1538+ var title = req._t('Commits') + ' · %{author}/%{repo}'
1539+ return this.renderRepoPage(req, repo, 'commits', branch, title, cat([
1540+ pull.once('<h3>' + req._t('Commits') + '</h3>'),
1541+ pull(
1542+ repo.readLog(query.start || branch),
1543+ pull.take(20),
1544+ paramap(repo.getCommitParsed.bind(repo), 8),
1545+ paginate(
1546+ !query.start ? '' : function (first, cb) {
1547+ cb(null, '&hellip;')
1548+ },
1549+ pull.map(renderCommit.bind(this, req, repo)),
1550+ function (commit, cb) {
1551+ cb(null, commit.parents && commit.parents[0] ?
1552+ '<a href="?start=' + commit.id + '">' +
1553+ req._t('Older') + '</a>' : '')
14931554 }
1494- }).join('') +
1495- tags.map(function (update) {
1496- return update.value
1497- ? link([repo.id, 'tag', update.value], update.name)
1498- : '<s>' + escapeHTML(update.name) + '</s>'
1499- }).join(', ') +
1500- '</section>'
1501- }
1502-
1503- /* Repo commits */
1504-
1505- function serveRepoCommits(req, repo, branch) {
1506- var query = req._u.query
1507- var title = req._t('Commits') + ' · %{author}/%{repo}'
1508- return renderRepoPage(req, repo, 'commits', branch, title, cat([
1509- pull.once('<h3>' + req._t('Commits') + '</h3>'),
1510- pull(
1511- repo.readLog(query.start || branch),
1512- pull.take(20),
1513- paramap(repo.getCommitParsed.bind(repo), 8),
1514- paginate(
1515- !query.start ? '' : function (first, cb) {
1516- cb(null, '&hellip;')
1517- },
1518- pull.map(renderCommit.bind(this, req, repo)),
1519- function (commit, cb) {
1520- cb(null, commit.parents && commit.parents[0] ?
1521- '<a href="?start=' + commit.id + '">' +
1522- req._t('Older') + '</a>' : '')
1523- }
1524- )
15251555 )
1526- ]))
1527- }
1556+ )
1557+ ]))
1558+}
15281559
1529- function renderCommit(req, repo, commit) {
1530- var commitPath = [repo.id, 'commit', commit.id]
1531- var treePath = [repo.id, 'tree', commit.id]
1532- return '<section class="collapse">' +
1533- '<strong>' + link(commitPath, commit.title) + '</strong><br>' +
1534- '<tt>' + commit.id + '</tt> ' +
1535- link(treePath, req._t('Tree')) + '<br>' +
1536- escapeHTML(commit.author.name) + ' &middot; ' +
1537- commit.author.date.toLocaleString(req._locale) +
1538- (commit.separateAuthor ? '<br>' + req._t('CommittedOn', {
1539- name: escapeHTML(commit.committer.name),
1540- date: commit.committer.date.toLocaleString(req._locale)
1541- }) : '') +
1542- '</section>'
1543- }
1560+function renderCommit(req, repo, commit) {
1561+ var commitPath = [repo.id, 'commit', commit.id]
1562+ var treePath = [repo.id, 'tree', commit.id]
1563+ return '<section class="collapse">' +
1564+ '<strong>' + link(commitPath, commit.title) + '</strong><br>' +
1565+ '<tt>' + commit.id + '</tt> ' +
1566+ link(treePath, req._t('Tree')) + '<br>' +
1567+ escapeHTML(commit.author.name) + ' &middot; ' +
1568+ commit.author.date.toLocaleString(req._locale) +
1569+ (commit.separateAuthor ? '<br>' + req._t('CommittedOn', {
1570+ name: escapeHTML(commit.committer.name),
1571+ date: commit.committer.date.toLocaleString(req._locale)
1572+ }) : '') +
1573+ '</section>'
1574+}
15441575
1545- /* Branch menu */
1576+/* Branch menu */
15461577
1547- function formatRevOptions(currentName) {
1548- return function (name) {
1549- var htmlName = escapeHTML(name)
1550- return '<option value="' + htmlName + '"' +
1551- (name == currentName ? ' selected="selected"' : '') +
1552- '>' + htmlName + '</option>'
1553- }
1578+function formatRevOptions(currentName) {
1579+ return function (name) {
1580+ var htmlName = escapeHTML(name)
1581+ return '<option value="' + htmlName + '"' +
1582+ (name == currentName ? ' selected="selected"' : '') +
1583+ '>' + htmlName + '</option>'
15541584 }
1585+}
15551586
1556- function formatRevType(req, type) {
1557- return (
1558- type == 'heads' ? req._t('Branches') :
1559- type == 'tags' ? req._t('Tags') :
1560- type)
1561- }
1587+function formatRevType(req, type) {
1588+ return (
1589+ type == 'heads' ? req._t('Branches') :
1590+ type == 'tags' ? req._t('Tags') :
1591+ type)
1592+}
15621593
1563- function revMenu(req, repo, currentName) {
1564- return readOnce(function (cb) {
1565- repo.getRefNames(function (err, refs) {
1566- if (err) return cb(err)
1567- cb(null, '<select name="rev" onchange="this.form.submit()">' +
1568- Object.keys(refs).map(function (group) {
1569- return '<optgroup label="' + formatRevType(req, group) + '">' +
1570- refs[group].map(formatRevOptions(currentName)).join('') +
1571- '</optgroup>'
1572- }).join('') +
1573- '</select><noscript> ' +
1574- '<input type="submit" value="' + req._t('Go') + '"/></noscript>')
1575- })
1594+function revMenu(req, repo, currentName) {
1595+ return readOnce(function (cb) {
1596+ repo.getRefNames(function (err, refs) {
1597+ if (err) return cb(err)
1598+ cb(null, '<select name="rev" onchange="this.form.submit()">' +
1599+ Object.keys(refs).map(function (group) {
1600+ return '<optgroup label="' + formatRevType(req, group) + '">' +
1601+ refs[group].map(formatRevOptions(currentName)).join('') +
1602+ '</optgroup>'
1603+ }).join('') +
1604+ '</select><noscript> ' +
1605+ '<input type="submit" value="' + req._t('Go') + '"/></noscript>')
15761606 })
1577- }
1607+ })
1608+}
15781609
1579- function branchMenu(repo, name, currentName) {
1580- return cat([
1581- pull.once('<select name="' + name + '">'),
1582- pull(
1583- repo.refs(),
1584- pull.map(function (ref) {
1585- var m = ref.name.match(/^refs\/([^\/]*)\/(.*)$/) || [,, ref.name]
1586- return m[1] == 'heads' && m[2]
1587- }),
1588- pull.filter(Boolean),
1589- pullSort(),
1590- pull.map(formatRevOptions(currentName))
1591- ),
1592- pull.once('</select>')
1593- ])
1594- }
1610+function branchMenu(repo, name, currentName) {
1611+ return cat([
1612+ pull.once('<select name="' + name + '">'),
1613+ pull(
1614+ repo.refs(),
1615+ pull.map(function (ref) {
1616+ var m = ref.name.match(/^refs\/([^\/]*)\/(.*)$/) || [,, ref.name]
1617+ return m[1] == 'heads' && m[2]
1618+ }),
1619+ pull.filter(Boolean),
1620+ pullSort(),
1621+ pull.map(formatRevOptions(currentName))
1622+ ),
1623+ pull.once('</select>')
1624+ ])
1625+}
15951626
1596- /* Repo tree */
1627+/* Repo tree */
15971628
1598- function renderRepoLatest(req, repo, rev) {
1599- return readOnce(function (cb) {
1600- repo.getCommitParsed(rev, function (err, commit) {
1601- if (err) return cb(err)
1602- var commitPath = [repo.id, 'commit', commit.id]
1603- cb(null,
1604- req._t('Latest') + ': ' +
1605- '<strong>' + link(commitPath, commit.title) + '</strong><br/>' +
1606- '<tt>' + commit.id + '</tt><br/> ' +
1607- req._t('CommittedOn', {
1608- name: escapeHTML(commit.committer.name),
1609- date: commit.committer.date.toLocaleString(req._locale)
1610- }) +
1611- (commit.separateAuthor ? '<br/>' + req._t('AuthoredOn', {
1612- name: escapeHTML(commit.author.name),
1613- date: commit.author.date.toLocaleString(req._locale)
1614- }) : ''))
1615- })
1629+function renderRepoLatest(req, repo, rev) {
1630+ return readOnce(function (cb) {
1631+ repo.getCommitParsed(rev, function (err, commit) {
1632+ if (err) return cb(err)
1633+ var commitPath = [repo.id, 'commit', commit.id]
1634+ cb(null,
1635+ req._t('Latest') + ': ' +
1636+ '<strong>' + link(commitPath, commit.title) + '</strong><br/>' +
1637+ '<tt>' + commit.id + '</tt><br/> ' +
1638+ req._t('CommittedOn', {
1639+ name: escapeHTML(commit.committer.name),
1640+ date: commit.committer.date.toLocaleString(req._locale)
1641+ }) +
1642+ (commit.separateAuthor ? '<br/>' + req._t('AuthoredOn', {
1643+ name: escapeHTML(commit.author.name),
1644+ date: commit.author.date.toLocaleString(req._locale)
1645+ }) : ''))
16161646 })
1617- }
1647+ })
1648+}
16181649
1619- // breadcrumbs
1620- function linkPath(basePath, path) {
1621- path = path.slice()
1622- var last = path.pop()
1623- return path.map(function (dir, i) {
1624- return link(basePath.concat(path.slice(0, i+1)), dir)
1625- }).concat(last).join(' / ')
1626- }
1650+// breadcrumbs
1651+function linkPath(basePath, path) {
1652+ path = path.slice()
1653+ var last = path.pop()
1654+ return path.map(function (dir, i) {
1655+ return link(basePath.concat(path.slice(0, i+1)), dir)
1656+ }).concat(last).join(' / ')
1657+}
16271658
1628- function renderRepoTree(req, repo, rev, path) {
1629- var pathLinks = path.length === 0 ? '' :
1630- ': ' + linkPath([repo.id, 'tree'], [rev].concat(path))
1631- return cat([
1632- pull.once('<h3>' + req._t('Files') + pathLinks + '</h3>'),
1633- pull(
1634- repo.readDir(rev, path),
1635- pull.map(function (file) {
1636- var type = (file.mode === 040000) ? 'tree' :
1637- (file.mode === 0160000) ? 'commit' : 'blob'
1638- if (type == 'commit')
1639- return [
1640- '<span title="' + req._t('gitCommitLink') + '">🖈</span>',
1641- '<span title="' + escapeHTML(file.id) + '">' +
1642- escapeHTML(file.name) + '</span>']
1643- var filePath = [repo.id, type, rev].concat(path, file.name)
1644- return ['<i>' + (type == 'tree' ? '📁' : '📄') + '</i>',
1645- link(filePath, file.name)]
1646- }),
1647- table('class="files"')
1648- )
1649- ])
1650- }
1659+function renderRepoTree(req, repo, rev, path) {
1660+ var pathLinks = path.length === 0 ? '' :
1661+ ': ' + linkPath([repo.id, 'tree'], [rev].concat(path))
1662+ return cat([
1663+ pull.once('<h3>' + req._t('Files') + pathLinks + '</h3>'),
1664+ pull(
1665+ repo.readDir(rev, path),
1666+ pull.map(function (file) {
1667+ var type = (file.mode === 040000) ? 'tree' :
1668+ (file.mode === 0160000) ? 'commit' : 'blob'
1669+ if (type == 'commit')
1670+ return [
1671+ '<span title="' + req._t('gitCommitLink') + '">🖈</span>',
1672+ '<span title="' + escapeHTML(file.id) + '">' +
1673+ escapeHTML(file.name) + '</span>']
1674+ var filePath = [repo.id, type, rev].concat(path, file.name)
1675+ return ['<i>' + (type == 'tree' ? '📁' : '📄') + '</i>',
1676+ link(filePath, file.name)]
1677+ }),
1678+ table('class="files"')
1679+ )
1680+ ])
1681+}
16511682
1652- /* Repo readme */
1683+/* Repo readme */
16531684
1654- function renderRepoReadme(req, repo, branch, path) {
1655- return readNext(function (cb) {
1656- pull(
1657- repo.readDir(branch, path),
1658- pull.filter(function (file) {
1659- return /readme(\.|$)/i.test(file.name)
1660- }),
1661- pull.take(1),
1662- pull.collect(function (err, files) {
1663- if (err) return cb(null, pull.empty())
1664- var file = files[0]
1665- if (!file)
1666- return cb(null, pull.once(path.length ? '' :
1667- '<p>' + req._t('NoReadme') + '</p>'))
1668- repo.getObjectFromAny(file.id, function (err, obj) {
1669- if (err) return cb(err)
1670- cb(null, cat([
1671- pull.once('<section><h4><a name="readme">' +
1672- escapeHTML(file.name) + '</a></h4><hr/>'),
1673- renderObjectData(obj, file.name, repo, branch, path),
1674- pull.once('</section>')
1675- ]))
1676- })
1685+function renderRepoReadme(req, repo, branch, path) {
1686+ return readNext(function (cb) {
1687+ pull(
1688+ repo.readDir(branch, path),
1689+ pull.filter(function (file) {
1690+ return /readme(\.|$)/i.test(file.name)
1691+ }),
1692+ pull.take(1),
1693+ pull.collect(function (err, files) {
1694+ if (err) return cb(null, pull.empty())
1695+ var file = files[0]
1696+ if (!file)
1697+ return cb(null, pull.once(path.length ? '' :
1698+ '<p>' + req._t('NoReadme') + '</p>'))
1699+ repo.getObjectFromAny(file.id, function (err, obj) {
1700+ if (err) return cb(err)
1701+ cb(null, cat([
1702+ pull.once('<section><h4><a name="readme">' +
1703+ escapeHTML(file.name) + '</a></h4><hr/>'),
1704+ renderObjectData(obj, file.name, repo, branch, path),
1705+ pull.once('</section>')
1706+ ]))
16771707 })
1678- )
1679- })
1680- }
1708+ })
1709+ )
1710+ })
1711+}
16811712
1682- /* Repo commit */
1713+/* Repo commit */
16831714
1684- function serveRepoCommit(req, repo, rev) {
1685- return readNext(function (cb) {
1686- repo.getCommitParsed(rev, function (err, commit) {
1687- if (err) return cb(err)
1688- var commitPath = [repo.id, 'commit', commit.id]
1689- var treePath = [repo.id, 'tree', commit.id]
1690- var title = escapeHTML(commit.title) + ' · ' +
1691- '%{author}/%{repo}@' + commit.id.substr(0, 8)
1692- cb(null, renderRepoPage(req, repo, null, rev, title, cat([
1693- pull.once(
1694- '<h3>' + link(commitPath,
1695- req._t('CommitRev', {rev: rev})) + '</h3>' +
1696- '<section class="collapse">' +
1697- '<div class="right-bar">' +
1698- link(treePath, req._t('BrowseFiles')) +
1699- '</div>' +
1700- '<h4>' + linkify(escapeHTML(commit.title)) + '</h4>' +
1701- (commit.body ? linkify(pre(commit.body)) : '') +
1702- (commit.separateAuthor ? req._t('AuthoredOn', {
1703- name: escapeHTML(commit.author.name),
1704- date: commit.author.date.toLocaleString(req._locale)
1705- }) + '<br/>' : '') +
1706- req._t('CommittedOn', {
1707- name: escapeHTML(commit.committer.name),
1708- date: commit.committer.date.toLocaleString(req._locale)
1709- }) + '<br/>' +
1710- commit.parents.map(function (id) {
1711- return req._t('Parent') + ': ' +
1712- link([repo.id, 'commit', id], id)
1713- }).join('<br>') +
1714- '</section>' +
1715- '<section><h3>' + req._t('FilesChanged') + '</h3>'),
1716- // TODO: show diff from all parents (merge commits)
1717- renderDiffStat(req, [repo, repo], [commit.parents[0], commit.id]),
1718- pull.once('</section>')
1719- ])))
1720- })
1715+G.serveRepoCommit = function (req, repo, rev) {
1716+ var self = this
1717+ return readNext(function (cb) {
1718+ repo.getCommitParsed(rev, function (err, commit) {
1719+ if (err) return cb(err)
1720+ var commitPath = [repo.id, 'commit', commit.id]
1721+ var treePath = [repo.id, 'tree', commit.id]
1722+ var title = escapeHTML(commit.title) + ' · ' +
1723+ '%{author}/%{repo}@' + commit.id.substr(0, 8)
1724+ cb(null, self.renderRepoPage(req, repo, null, rev, title, cat([
1725+ pull.once(
1726+ '<h3>' + link(commitPath,
1727+ req._t('CommitRev', {rev: rev})) + '</h3>' +
1728+ '<section class="collapse">' +
1729+ '<div class="right-bar">' +
1730+ link(treePath, req._t('BrowseFiles')) +
1731+ '</div>' +
1732+ '<h4>' + linkify(escapeHTML(commit.title)) + '</h4>' +
1733+ (commit.body ? linkify(pre(commit.body)) : '') +
1734+ (commit.separateAuthor ? req._t('AuthoredOn', {
1735+ name: escapeHTML(commit.author.name),
1736+ date: commit.author.date.toLocaleString(req._locale)
1737+ }) + '<br/>' : '') +
1738+ req._t('CommittedOn', {
1739+ name: escapeHTML(commit.committer.name),
1740+ date: commit.committer.date.toLocaleString(req._locale)
1741+ }) + '<br/>' +
1742+ commit.parents.map(function (id) {
1743+ return req._t('Parent') + ': ' +
1744+ link([repo.id, 'commit', id], id)
1745+ }).join('<br>') +
1746+ '</section>' +
1747+ '<section><h3>' + req._t('FilesChanged') + '</h3>'),
1748+ // TODO: show diff from all parents (merge commits)
1749+ renderDiffStat(req, [repo, repo], [commit.parents[0], commit.id]),
1750+ pull.once('</section>')
1751+ ])))
17211752 })
1722- }
1753+ })
1754+}
17231755
1724- /* Repo tag */
1756+/* Repo tag */
17251757
1726- function serveRepoTag(req, repo, rev) {
1727- return readNext(function (cb) {
1728- repo.getTagParsed(rev, function (err, tag) {
1729- if (err) return cb(err)
1730- var title = req._t('TagName', {
1731- tag: escapeHTML(tag.tag)
1732- }) + ' · %{author}/%{repo}'
1733- var body = (tag.title + '\n\n' +
1734- tag.body.replace(/-----BEGIN PGP SIGNATURE-----\n[^.]*?\n-----END PGP SIGNATURE-----\s*$/, '')).trim()
1735- cb(null, renderRepoPage(req, repo, 'tags', tag.object, title, pull.once(
1758+G.serveRepoTag = function (req, repo, rev) {
1759+ var self = this
1760+ return readNext(function (cb) {
1761+ repo.getTagParsed(rev, function (err, tag) {
1762+ if (err) return cb(err)
1763+ var title = req._t('TagName', {
1764+ tag: escapeHTML(tag.tag)
1765+ }) + ' · %{author}/%{repo}'
1766+ var body = (tag.title + '\n\n' +
1767+ tag.body.replace(/-----BEGIN PGP SIGNATURE-----\n[^.]*?\n-----END PGP SIGNATURE-----\s*$/, '')).trim()
1768+ cb(null, self.renderRepoPage(req, repo, 'tags', tag.object, title,
1769+ pull.once(
17361770 '<section class="collapse">' +
17371771 '<h3>' + link([repo.id, 'tag', rev], tag.tag) + '</h3>' +
17381772 req._t('TaggedOn', {
17391773 name: escapeHTML(tag.tagger.name),
@@ -1741,594 +1775,603 @@
17411775 }) + '<br/>' +
17421776 link([repo.id, tag.type, tag.object]) +
17431777 linkify(pre(body)) +
17441778 '</section>')))
1745- })
17461779 })
1747- }
1780+ })
1781+}
17481782
17491783
1750- /* Diff stat */
1784+/* Diff stat */
17511785
1752- function renderDiffStat(req, repos, treeIds) {
1753- if (treeIds.length == 0) treeIds = [null]
1754- var id = treeIds[0]
1755- var lastI = treeIds.length - 1
1756- var oldTree = treeIds[0]
1757- var changedFiles = []
1758- return cat([
1759- pull(
1760- Repo.diffTrees(repos, treeIds, true),
1761- pull.map(function (item) {
1762- var filename = escapeHTML(item.filename = item.path.join('/'))
1763- var oldId = item.id && item.id[0]
1764- var newId = item.id && item.id[lastI]
1765- var oldMode = item.mode && item.mode[0]
1766- var newMode = item.mode && item.mode[lastI]
1767- var action =
1768- !oldId && newId ? req._t('action.added') :
1769- oldId && !newId ? req._t('action.deleted') :
1770- oldMode != newMode ? req._t('action.changedMode', {
1771- old: oldMode.toString(8),
1772- new: newMode.toString(8)
1773- }) : req._t('changed')
1774- if (item.id)
1775- changedFiles.push(item)
1776- var blobsPath = item.id[1]
1777- ? [repos[1].id, 'blob', treeIds[1]]
1778- : [repos[0].id, 'blob', treeIds[0]]
1779- var rawsPath = item.id[1]
1780- ? [repos[1].id, 'raw', treeIds[1]]
1781- : [repos[0].id, 'raw', treeIds[0]]
1782- item.blobPath = blobsPath.concat(item.path)
1783- item.rawPath = rawsPath.concat(item.path)
1784- var fileHref = item.id ?
1785- '#' + encodeURIComponent(item.path.join('/')) :
1786- encodeLink(item.blobPath)
1787- return ['<a href="' + fileHref + '">' + filename + '</a>', action]
1788- }),
1789- table()
1790- ),
1791- pull(
1792- pull.values(changedFiles),
1793- paramap(function (item, cb) {
1794- var extension = getExtension(item.filename)
1795- if (extension in imgMimes) {
1796- var filename = escapeHTML(item.filename)
1797- return cb(null,
1798- '<pre><table class="code">' +
1799- '<tr><th id="' + escapeHTML(item.filename) + '">' +
1800- filename + '</th></tr>' +
1801- '<tr><td><img src="' + encodeLink(item.rawPath) + '"' +
1802- ' alt="' + filename + '"/></td></tr>' +
1803- '</table></pre>')
1804- }
1805- var done = multicb({ pluck: 1, spread: true })
1806- getRepoObjectString(repos[0], item.id[0], done())
1807- getRepoObjectString(repos[1], item.id[lastI], done())
1808- done(function (err, strOld, strNew) {
1809- if (err) return cb(err)
1810- cb(null, htmlLineDiff(req, item.filename, item.filename,
1811- strOld, strNew,
1812- encodeLink(item.blobPath)))
1813- })
1814- }, 4)
1815- )
1816- ])
1817- }
1786+function renderDiffStat(req, repos, treeIds) {
1787+ if (treeIds.length == 0) treeIds = [null]
1788+ var id = treeIds[0]
1789+ var lastI = treeIds.length - 1
1790+ var oldTree = treeIds[0]
1791+ var changedFiles = []
1792+ return cat([
1793+ pull(
1794+ Repo.diffTrees(repos, treeIds, true),
1795+ pull.map(function (item) {
1796+ var filename = escapeHTML(item.filename = item.path.join('/'))
1797+ var oldId = item.id && item.id[0]
1798+ var newId = item.id && item.id[lastI]
1799+ var oldMode = item.mode && item.mode[0]
1800+ var newMode = item.mode && item.mode[lastI]
1801+ var action =
1802+ !oldId && newId ? req._t('action.added') :
1803+ oldId && !newId ? req._t('action.deleted') :
1804+ oldMode != newMode ? req._t('action.changedMode', {
1805+ old: oldMode.toString(8),
1806+ new: newMode.toString(8)
1807+ }) : req._t('changed')
1808+ if (item.id)
1809+ changedFiles.push(item)
1810+ var blobsPath = item.id[1]
1811+ ? [repos[1].id, 'blob', treeIds[1]]
1812+ : [repos[0].id, 'blob', treeIds[0]]
1813+ var rawsPath = item.id[1]
1814+ ? [repos[1].id, 'raw', treeIds[1]]
1815+ : [repos[0].id, 'raw', treeIds[0]]
1816+ item.blobPath = blobsPath.concat(item.path)
1817+ item.rawPath = rawsPath.concat(item.path)
1818+ var fileHref = item.id ?
1819+ '#' + encodeURIComponent(item.path.join('/')) :
1820+ encodeLink(item.blobPath)
1821+ return ['<a href="' + fileHref + '">' + filename + '</a>', action]
1822+ }),
1823+ table()
1824+ ),
1825+ pull(
1826+ pull.values(changedFiles),
1827+ paramap(function (item, cb) {
1828+ var extension = getExtension(item.filename)
1829+ if (extension in imgMimes) {
1830+ var filename = escapeHTML(item.filename)
1831+ return cb(null,
1832+ '<pre><table class="code">' +
1833+ '<tr><th id="' + escapeHTML(item.filename) + '">' +
1834+ filename + '</th></tr>' +
1835+ '<tr><td><img src="' + encodeLink(item.rawPath) + '"' +
1836+ ' alt="' + filename + '"/></td></tr>' +
1837+ '</table></pre>')
1838+ }
1839+ var done = multicb({ pluck: 1, spread: true })
1840+ getRepoObjectString(repos[0], item.id[0], done())
1841+ getRepoObjectString(repos[1], item.id[lastI], done())
1842+ done(function (err, strOld, strNew) {
1843+ if (err) return cb(err)
1844+ cb(null, htmlLineDiff(req, item.filename, item.filename,
1845+ strOld, strNew,
1846+ encodeLink(item.blobPath)))
1847+ })
1848+ }, 4)
1849+ )
1850+ ])
1851+}
18181852
1819- function htmlLineDiff(req, filename, anchor, oldStr, newStr, blobHref) {
1820- var diff = JsDiff.structuredPatch('', '', oldStr, newStr)
1821- var groups = diff.hunks.map(function (hunk) {
1822- var oldLine = hunk.oldStart
1823- var newLine = hunk.newStart
1824- var header = '<tr class="diff-hunk-header"><td colspan=2></td><td>' +
1825- '@@ -' + oldLine + ',' + hunk.oldLines + ' ' +
1826- '+' + newLine + ',' + hunk.newLines + ' @@' +
1827- '</td></tr>'
1828- return [header].concat(hunk.lines.map(function (line) {
1829- var s = line[0]
1830- if (s == '\\') return
1831- var html = highlight(line, getExtension(filename))
1832- var trClass = s == '+' ? 'diff-new' : s == '-' ? 'diff-old' : ''
1833- var lineNums = [s == '+' ? '' : oldLine++, s == '-' ? '' : newLine++]
1834- var id = [filename].concat(lineNums).join('-')
1835- return '<tr id="' + escapeHTML(id) + '" class="' + trClass + '">' +
1836- lineNums.map(function (num) {
1837- return '<td class="code-linenum">' +
1838- (num ? '<a href="#' + encodeURIComponent(id) + '">' +
1839- num + '</a>' : '') + '</td>'
1840- }).join('') +
1841- '<td class="code-text">' + html + '</td></tr>'
1842- }))
1843- })
1844- return '<pre><table class="code">' +
1845- '<tr><th colspan=3 id="' + escapeHTML(anchor) + '">' + filename +
1846- '<span class="right-bar">' +
1847- '<a href="' + blobHref + '">' + req._t('View') + '</a> ' +
1848- '</span></th></tr>' +
1849- [].concat.apply([], groups).join('') +
1850- '</table></pre>'
1851- }
1853+function htmlLineDiff(req, filename, anchor, oldStr, newStr, blobHref) {
1854+ var diff = JsDiff.structuredPatch('', '', oldStr, newStr)
1855+ var groups = diff.hunks.map(function (hunk) {
1856+ var oldLine = hunk.oldStart
1857+ var newLine = hunk.newStart
1858+ var header = '<tr class="diff-hunk-header"><td colspan=2></td><td>' +
1859+ '@@ -' + oldLine + ',' + hunk.oldLines + ' ' +
1860+ '+' + newLine + ',' + hunk.newLines + ' @@' +
1861+ '</td></tr>'
1862+ return [header].concat(hunk.lines.map(function (line) {
1863+ var s = line[0]
1864+ if (s == '\\') return
1865+ var html = highlight(line, getExtension(filename))
1866+ var trClass = s == '+' ? 'diff-new' : s == '-' ? 'diff-old' : ''
1867+ var lineNums = [s == '+' ? '' : oldLine++, s == '-' ? '' : newLine++]
1868+ var id = [filename].concat(lineNums).join('-')
1869+ return '<tr id="' + escapeHTML(id) + '" class="' + trClass + '">' +
1870+ lineNums.map(function (num) {
1871+ return '<td class="code-linenum">' +
1872+ (num ? '<a href="#' + encodeURIComponent(id) + '">' +
1873+ num + '</a>' : '') + '</td>'
1874+ }).join('') +
1875+ '<td class="code-text">' + html + '</td></tr>'
1876+ }))
1877+ })
1878+ return '<pre><table class="code">' +
1879+ '<tr><th colspan=3 id="' + escapeHTML(anchor) + '">' + filename +
1880+ '<span class="right-bar">' +
1881+ '<a href="' + blobHref + '">' + req._t('View') + '</a> ' +
1882+ '</span></th></tr>' +
1883+ [].concat.apply([], groups).join('') +
1884+ '</table></pre>'
1885+}
18521886
1853- /* An unknown message linking to a repo */
1887+/* An unknown message linking to a repo */
18541888
1855- function serveRepoSomething(req, repo, id, msg, path) {
1856- return renderRepoPage(req, repo, null, null, null,
1857- pull.once('<section><h3>' + link([id]) + '</h3>' +
1858- json(msg) + '</section>'))
1859- }
1889+G.serveRepoSomething = function (req, repo, id, msg, path) {
1890+ return this.renderRepoPage(req, repo, null, null, null,
1891+ pull.once('<section><h3>' + link([id]) + '</h3>' +
1892+ json(msg) + '</section>'))
1893+}
18601894
1861- /* Repo update */
1895+/* Repo update */
18621896
1863- function objsArr(objs) {
1864- return Array.isArray(objs) ? objs :
1865- Object.keys(objs).map(function (sha1) {
1866- var obj = Object.create(objs[sha1])
1867- obj.sha1 = sha1
1868- return obj
1869- })
1870- }
1897+function objsArr(objs) {
1898+ return Array.isArray(objs) ? objs :
1899+ Object.keys(objs).map(function (sha1) {
1900+ var obj = Object.create(objs[sha1])
1901+ obj.sha1 = sha1
1902+ return obj
1903+ })
1904+}
18711905
1872- function serveRepoUpdate(req, repo, id, msg, path) {
1873- var raw = req._u.query.raw != null
1874- var title = req._t('Update') + ' · %{author}/%{repo}'
1906+G.serveRepoUpdate = function (req, repo, id, msg, path) {
1907+ var self = this
1908+ var raw = req._u.query.raw != null
1909+ var title = req._t('Update') + ' · %{author}/%{repo}'
18751910
1876- if (raw)
1877- return renderRepoPage(req, repo, 'activity', null, title, pull.once(
1878- '<a href="?" class="raw-link header-align">' +
1879- req._t('Info') + '</a>' +
1880- '<h3>' + req._t('Update') + '</h3>' +
1881- '<section class="collapse">' +
1882- json({key: id, value: msg}) + '</section>'))
1911+ if (raw)
1912+ return self.renderRepoPage(req, repo, 'activity', null, title, pull.once(
1913+ '<a href="?" class="raw-link header-align">' +
1914+ req._t('Info') + '</a>' +
1915+ '<h3>' + req._t('Update') + '</h3>' +
1916+ '<section class="collapse">' +
1917+ json({key: id, value: msg}) + '</section>'))
18831918
1884- // convert packs to old single-object style
1885- if (msg.content.indexes) {
1886- for (var i = 0; i < msg.content.indexes.length; i++) {
1887- msg.content.packs[i] = {
1888- pack: {link: msg.content.packs[i].link},
1889- idx: msg.content.indexes[i]
1890- }
1919+ // convert packs to old single-object style
1920+ if (msg.content.indexes) {
1921+ for (var i = 0; i < msg.content.indexes.length; i++) {
1922+ msg.content.packs[i] = {
1923+ pack: {link: msg.content.packs[i].link},
1924+ idx: msg.content.indexes[i]
18911925 }
18921926 }
1927+ }
18931928
1894- var commits = cat([
1895- msg.content.objects && pull(
1896- pull.values(msg.content.objects),
1897- pull.filter(function (obj) { return obj.type == 'commit' }),
1898- paramap(function (obj, cb) {
1899- getBlob(req, obj.link || obj.key, function (err, readObject) {
1900- if (err) return cb(err)
1901- Repo.getCommitParsed({read: readObject}, cb)
1902- })
1903- }, 8)
1904- ),
1905- msg.content.packs && pull(
1906- pull.values(msg.content.packs),
1907- paramap(function (pack, cb) {
1908- var done = multicb({ pluck: 1, spread: true })
1909- getBlob(req, pack.pack.link, done())
1910- getBlob(req, pack.idx.link, done())
1911- done(function (err, readPack, readIdx) {
1912- if (err) return cb(renderError(err))
1913- cb(null, gitPack.decodeWithIndex(repo, readPack, readIdx))
1914- })
1915- }, 4),
1916- pull.flatten(),
1917- pull.asyncMap(function (obj, cb) {
1918- if (obj.type == 'commit')
1919- Repo.getCommitParsed(obj, cb)
1920- else
1921- pull(obj.read, pull.drain(null, cb))
1922- }),
1923- pull.filter()
1924- )
1925- ])
1929+ var commits = cat([
1930+ msg.content.objects && pull(
1931+ pull.values(msg.content.objects),
1932+ pull.filter(function (obj) { return obj.type == 'commit' }),
1933+ paramap(function (obj, cb) {
1934+ self.getBlob(req, obj.link || obj.key, function (err, readObject) {
1935+ if (err) return cb(err)
1936+ Repo.getCommitParsed({read: readObject}, cb)
1937+ })
1938+ }, 8)
1939+ ),
1940+ msg.content.packs && pull(
1941+ pull.values(msg.content.packs),
1942+ paramap(function (pack, cb) {
1943+ var done = multicb({ pluck: 1, spread: true })
1944+ self.getBlob(req, pack.pack.link, done())
1945+ self.getBlob(req, pack.idx.link, done())
1946+ done(function (err, readPack, readIdx) {
1947+ if (err) return cb(renderError(err))
1948+ cb(null, gitPack.decodeWithIndex(repo, readPack, readIdx))
1949+ })
1950+ }, 4),
1951+ pull.flatten(),
1952+ pull.asyncMap(function (obj, cb) {
1953+ if (obj.type == 'commit')
1954+ Repo.getCommitParsed(obj, cb)
1955+ else
1956+ pull(obj.read, pull.drain(null, cb))
1957+ }),
1958+ pull.filter()
1959+ )
1960+ ])
19261961
1927- return renderRepoPage(req, repo, 'activity', null, title, cat([
1928- pull.once('<a href="?raw" class="raw-link header-align">' +
1929- req._t('Data') + '</a>' +
1930- '<h3>' + req._t('Update') + '</h3>' +
1931- renderRepoUpdate(req, repo, {key: id, value: msg}, true)),
1932- (msg.content.objects || msg.content.packs) &&
1933- pull.once('<h3>' + req._t('Commits') + '</h3>'),
1934- pull(commits, pull.map(function (commit) {
1935- return renderCommit(req, repo, commit)
1936- }))
1937- ]))
1938- }
1962+ return self.renderRepoPage(req, repo, 'activity', null, title, cat([
1963+ pull.once('<a href="?raw" class="raw-link header-align">' +
1964+ req._t('Data') + '</a>' +
1965+ '<h3>' + req._t('Update') + '</h3>' +
1966+ renderRepoUpdate(req, repo, {key: id, value: msg}, true)),
1967+ (msg.content.objects || msg.content.packs) &&
1968+ pull.once('<h3>' + req._t('Commits') + '</h3>'),
1969+ pull(commits, pull.map(function (commit) {
1970+ return renderCommit(req, repo, commit)
1971+ }))
1972+ ]))
1973+}
19391974
1940- /* Blob */
1975+/* Blob */
19411976
1942- function serveRepoBlob(req, repo, rev, path) {
1943- return readNext(function (cb) {
1944- repo.getFile(rev, path, function (err, object) {
1945- if (err) return cb(null, serveBlobNotFound(req, repo.id, err))
1946- var type = repo.isCommitHash(rev) ? 'Tree' : 'Branch'
1947- var pathLinks = path.length === 0 ? '' :
1948- ': ' + linkPath([repo.id, 'tree'], [rev].concat(path))
1949- var rawFilePath = [repo.id, 'raw', rev].concat(path)
1950- var dirPath = path.slice(0, path.length-1)
1951- var filename = path[path.length-1]
1952- var extension = getExtension(filename)
1953- var title = (path.length ? path.join('/') + ' · ' : '') +
1954- '%{author}/%{repo}' +
1955- (repo.head == 'refs/heads/' + rev ? '' : '@' + rev)
1956- cb(null, renderRepoPage(req, repo, 'code', rev, title, cat([
1957- pull.once('<section><form action="" method="get">' +
1958- '<h3>' + req._t(type) + ': ' + rev + ' '),
1959- revMenu(req, repo, rev),
1960- pull.once('</h3></form>'),
1961- type == 'Branch' && renderRepoLatest(req, repo, rev),
1962- pull.once('</section><section class="collapse">' +
1963- '<h3>' + req._t('Files') + pathLinks + '</h3>' +
1964- '<div>' + object.length + ' bytes' +
1965- '<span class="raw-link">' +
1966- link(rawFilePath, req._t('Raw')) + '</span>' +
1967- '</div></section>' +
1968- '<section>'),
1969- extension in imgMimes
1970- ? pull.once('<img src="' + encodeLink(rawFilePath) +
1971- '" alt="' + escapeHTML(filename) + '" />')
1972- : renderObjectData(object, filename, repo, rev, dirPath),
1973- pull.once('</section>')
1974- ])))
1975- })
1977+G.serveRepoBlob = function (req, repo, rev, path) {
1978+ var self = this
1979+ return readNext(function (cb) {
1980+ repo.getFile(rev, path, function (err, object) {
1981+ if (err) return cb(null, self.serveBlobNotFound(req, repo.id, err))
1982+ var type = repo.isCommitHash(rev) ? 'Tree' : 'Branch'
1983+ var pathLinks = path.length === 0 ? '' :
1984+ ': ' + linkPath([repo.id, 'tree'], [rev].concat(path))
1985+ var rawFilePath = [repo.id, 'raw', rev].concat(path)
1986+ var dirPath = path.slice(0, path.length-1)
1987+ var filename = path[path.length-1]
1988+ var extension = getExtension(filename)
1989+ var title = (path.length ? path.join('/') + ' · ' : '') +
1990+ '%{author}/%{repo}' +
1991+ (repo.head == 'refs/heads/' + rev ? '' : '@' + rev)
1992+ cb(null, self.renderRepoPage(req, repo, 'code', rev, title, cat([
1993+ pull.once('<section><form action="" method="get">' +
1994+ '<h3>' + req._t(type) + ': ' + rev + ' '),
1995+ revMenu(req, repo, rev),
1996+ pull.once('</h3></form>'),
1997+ type == 'Branch' && renderRepoLatest(req, repo, rev),
1998+ pull.once('</section><section class="collapse">' +
1999+ '<h3>' + req._t('Files') + pathLinks + '</h3>' +
2000+ '<div>' + object.length + ' bytes' +
2001+ '<span class="raw-link">' +
2002+ link(rawFilePath, req._t('Raw')) + '</span>' +
2003+ '</div></section>' +
2004+ '<section>'),
2005+ extension in imgMimes
2006+ ? pull.once('<img src="' + encodeLink(rawFilePath) +
2007+ '" alt="' + escapeHTML(filename) + '" />')
2008+ : renderObjectData(object, filename, repo, rev, dirPath),
2009+ pull.once('</section>')
2010+ ])))
19762011 })
1977- }
2012+ })
2013+}
19782014
1979- function serveBlobNotFound(req, repoId, err) {
1980- return serveTemplate(req, req._t('error.BlobNotFound'), 404)(pull.once(
1981- '<h2>' + req._t('error.BlobNotFound') + '</h2>' +
1982- '<p>' + req._t('error.BlobNotFoundInRepo', {
1983- repo: link([repoId])
1984- }) + '</p>' +
1985- '<pre>' + escapeHTML(err.stack) + '</pre>'
1986- ))
1987- }
2015+G.serveBlobNotFound = function (req, repoId, err) {
2016+ return this.serveTemplate(req, req._t('error.BlobNotFound'), 404)(pull.once(
2017+ '<h2>' + req._t('error.BlobNotFound') + '</h2>' +
2018+ '<p>' + req._t('error.BlobNotFoundInRepo', {
2019+ repo: link([repoId])
2020+ }) + '</p>' +
2021+ '<pre>' + escapeHTML(err.stack) + '</pre>'
2022+ ))
2023+}
19882024
1989- /* Raw blob */
2025+/* Raw blob */
19902026
1991- function serveRepoRaw(req, repo, branch, path) {
1992- return readNext(function (cb) {
1993- repo.getFile(branch, path, function (err, object) {
1994- if (err) return cb(null,
1995- serveBuffer(404, req._t('error.BlobNotFound')))
1996- var extension = getExtension(path[path.length-1])
1997- var contentType = imgMimes[extension]
1998- cb(null, pull(object.read, serveRaw(object.length, contentType)))
1999- })
2027+G.serveRepoRaw = function (req, repo, branch, path) {
2028+ return readNext(function (cb) {
2029+ repo.getFile(branch, path, function (err, object) {
2030+ if (err) return cb(null,
2031+ this.serveBuffer(404, req._t('error.BlobNotFound')))
2032+ var extension = getExtension(path[path.length-1])
2033+ var contentType = imgMimes[extension]
2034+ cb(null, pull(object.read, this.serveRaw(object.length, contentType)))
20002035 })
2001- }
2036+ })
2037+}
20022038
2003- function serveRaw(length, contentType) {
2004- var headers = {
2005- 'Content-Type': contentType || 'text/plain; charset=utf-8',
2006- 'Cache-Control': 'max-age=31536000'
2007- }
2008- if (length != null)
2009- headers['Content-Length'] = length
2010- return function (read) {
2011- return cat([pull.once([200, headers]), read])
2012- }
2039+G.serveRaw = function (length, contentType) {
2040+ var headers = {
2041+ 'Content-Type': contentType || 'text/plain; charset=utf-8',
2042+ 'Cache-Control': 'max-age=31536000'
20132043 }
2014-
2015- function getBlob(req, key, cb) {
2016- ssb.blobs.want(key, function (err, got) {
2017- if (err) cb(err)
2018- else if (!got) cb(new Error(req._t('error.MissingBlob', {key: key})))
2019- else cb(null, ssb.blobs.get(key))
2020- })
2044+ if (length != null)
2045+ headers['Content-Length'] = length
2046+ return function (read) {
2047+ return cat([pull.once([200, headers]), read])
20212048 }
2049+}
20222050
2023- function serveBlob(req, key) {
2024- return readNext(function (cb) {
2025- getBlob(req, key, function (err, read) {
2026- if (err) cb(null, serveError(req, err))
2027- else if (!read) cb(null, serve404(req))
2028- else cb(null, serveRaw()(read))
2029- })
2051+G.getBlob = function (req, key, cb) {
2052+ var blobs = this.ssb.blobs
2053+ blobs.want(key, function (err, got) {
2054+ if (err) cb(err)
2055+ else if (!got) cb(new Error(req._t('error.MissingBlob', {key: key})))
2056+ else cb(null, blobs.get(key))
2057+ })
2058+}
2059+
2060+G.serveBlob = function (req, key) {
2061+ var self = this
2062+ return readNext(function (cb) {
2063+ self.getBlob(req, key, function (err, read) {
2064+ if (err) cb(null, self.serveError(req, err))
2065+ else if (!read) cb(null, self.serve404(req))
2066+ else cb(null, self.serveRaw()(read))
20302067 })
2031- }
2068+ })
2069+}
20322070
2033- /* Digs */
2071+/* Digs */
20342072
2035- function serveRepoDigs(req, repo) {
2036- return readNext(function (cb) {
2037- var title = req._t('Digs') + ' · %{author}/%{repo}'
2038- getVotes(repo.id, function (err, votes) {
2039- cb(null, renderRepoPage(req, repo, null, null, title, cat([
2040- pull.once('<section><h3>' + req._t('Digs') + '</h3>' +
2041- '<div>' + req._t('Total') + ': ' + votes.upvotes + '</div>'),
2042- pull(
2043- pull.values(Object.keys(votes.upvoters)),
2044- paramap(function (feedId, cb) {
2045- about.getName(feedId, function (err, name) {
2046- if (err) return cb(err)
2047- cb(null, link([feedId], name))
2048- })
2049- }, 8),
2050- ul()
2051- ),
2052- pull.once('</section>')
2053- ])))
2054- })
2073+G.serveRepoDigs = function (req, repo) {
2074+ var self = this
2075+ return readNext(function (cb) {
2076+ var title = req._t('Digs') + ' · %{author}/%{repo}'
2077+ self.getVotes(repo.id, function (err, votes) {
2078+ cb(null, self.renderRepoPage(req, repo, null, null, title, cat([
2079+ pull.once('<section><h3>' + req._t('Digs') + '</h3>' +
2080+ '<div>' + req._t('Total') + ': ' + votes.upvotes + '</div>'),
2081+ pull(
2082+ pull.values(Object.keys(votes.upvoters)),
2083+ paramap(function (feedId, cb) {
2084+ self.about.getName(feedId, function (err, name) {
2085+ if (err) return cb(err)
2086+ cb(null, link([feedId], name))
2087+ })
2088+ }, 8),
2089+ ul()
2090+ ),
2091+ pull.once('</section>')
2092+ ])))
20552093 })
2056- }
2094+ })
2095+}
20572096
2058- /* Forks */
2097+/* Forks */
20592098
2060- function getForks(repo, includeSelf) {
2061- return pull(
2062- cat([
2063- includeSelf && readOnce(function (cb) {
2064- getMsg(repo.id, function (err, value) {
2065- cb(err, value && {key: repo.id, value: value})
2066- })
2067- }),
2068- ssb.links({
2069- dest: repo.id,
2070- values: true,
2071- rel: 'upstream'
2099+G.getForks = function (repo, includeSelf) {
2100+ var self = this
2101+ return pull(
2102+ cat([
2103+ includeSelf && readOnce(function (cb) {
2104+ self.getMsg(repo.id, function (err, value) {
2105+ cb(err, value && {key: repo.id, value: value})
20722106 })
2073- ]),
2074- pull.filter(function (msg) {
2075- return msg.value.content && msg.value.content.type == 'git-repo'
20762107 }),
2077- paramap(function (msg, cb) {
2078- getRepoFullName(about, msg.value.author, msg.key,
2079- function (err, repoName, authorName) {
2080- if (err) return cb(err)
2081- cb(null, {
2082- key: msg.key,
2083- value: msg.value,
2084- repoName: repoName,
2085- authorName: authorName
2086- })
2108+ this.ssb.links({
2109+ dest: repo.id,
2110+ values: true,
2111+ rel: 'upstream'
2112+ })
2113+ ]),
2114+ pull.filter(function (msg) {
2115+ return msg.value.content && msg.value.content.type == 'git-repo'
2116+ }),
2117+ paramap(function (msg, cb) {
2118+ self.getRepoFullName(msg.value.author, msg.key,
2119+ function (err, repoName, authorName) {
2120+ if (err) return cb(err)
2121+ cb(null, {
2122+ key: msg.key,
2123+ value: msg.value,
2124+ repoName: repoName,
2125+ authorName: authorName
20872126 })
2088- }, 8)
2089- )
2090- }
2127+ })
2128+ }, 8)
2129+ )
2130+}
20912131
2092- function serveRepoForks(req, repo) {
2093- var hasForks
2094- var title = req._t('Forks') + ' · %{author}/%{repo}'
2095- return renderRepoPage(req, repo, null, null, title, cat([
2096- pull.once('<h3>' + req._t('Forks') + '</h3>'),
2097- pull(
2098- getForks(repo),
2099- pull.map(function (msg) {
2100- hasForks = true
2101- return '<section class="collapse">' +
2102- link([msg.value.author], msg.authorName) + ' / ' +
2103- link([msg.key], msg.repoName) +
2104- '<span class="right-bar">' +
2105- timestamp(msg.value.timestamp, req) +
2106- '</span></section>'
2107- })
2108- ),
2109- readOnce(function (cb) {
2110- cb(null, hasForks ? '' : req._t('NoForks'))
2132+G.serveRepoForks = function (req, repo) {
2133+ var hasForks
2134+ var title = req._t('Forks') + ' · %{author}/%{repo}'
2135+ return this.renderRepoPage(req, repo, null, null, title, cat([
2136+ pull.once('<h3>' + req._t('Forks') + '</h3>'),
2137+ pull(
2138+ this.getForks(repo),
2139+ pull.map(function (msg) {
2140+ hasForks = true
2141+ return '<section class="collapse">' +
2142+ link([msg.value.author], msg.authorName) + ' / ' +
2143+ link([msg.key], msg.repoName) +
2144+ '<span class="right-bar">' +
2145+ timestamp(msg.value.timestamp, req) +
2146+ '</span></section>'
21112147 })
2112- ]))
2113- }
2148+ ),
2149+ readOnce(function (cb) {
2150+ cb(null, hasForks ? '' : req._t('NoForks'))
2151+ })
2152+ ]))
2153+}
21142154
2115- function serveRepoForkPrompt(req, repo) {
2116- var title = req._t('Fork') + ' · %{author}/%{repo}'
2117- return renderRepoPage(req, repo, null, null, title, pull.once(
2118- '<form action="" method="post" onreset="history.back()">' +
2119- '<h3>' + req._t('ForkRepoPrompt') + '</h3>' +
2120- '<p>' + hiddenInputs({ id: repo.id }) +
2121- '<button class="btn open" type="submit" name="action" value="fork">' +
2122- req._t('Fork') +
2123- '</button>' +
2124- ' <button class="btn" type="reset">' +
2125- req._t('Cancel') + '</button>' +
2126- '</p></form>'
2127- ))
2128- }
2155+G.serveRepoForkPrompt = function (req, repo) {
2156+ var title = req._t('Fork') + ' · %{author}/%{repo}'
2157+ return this.renderRepoPage(req, repo, null, null, title, pull.once(
2158+ '<form action="" method="post" onreset="history.back()">' +
2159+ '<h3>' + req._t('ForkRepoPrompt') + '</h3>' +
2160+ '<p>' + hiddenInputs({ id: repo.id }) +
2161+ '<button class="btn open" type="submit" name="action" value="fork">' +
2162+ req._t('Fork') +
2163+ '</button>' +
2164+ ' <button class="btn" type="reset">' +
2165+ req._t('Cancel') + '</button>' +
2166+ '</p></form>'
2167+ ))
2168+}
21292169
21302170 /* Issues */
21312171
2132- function serveRepoIssues(req, repo, isPRs) {
2133- var count = 0
2134- var state = req._u.query.state || 'open'
2135- var newPath = isPRs ? [repo.id, 'compare'] : [repo.id, 'issues', 'new']
2136- var title = req._t('Issues') + ' · %{author}/%{repo}'
2137- return renderRepoPage(req, repo, isPRs ? 'pulls' : 'issues', null, title, cat([
2138- pull.once(
2139- (isPublic ? '' :
2140- '<form class="right-bar" method="get"' +
2141- ' action="' + encodeLink(newPath) + '">' +
2142- '<button class="btn">&plus; ' +
2143- req._t(isPRs ? 'pullRequest.New' : 'issue.New') +
2144- '</button>' +
2145- '</form>') +
2146- '<h3>' + req._t(isPRs ? 'PullRequests' : 'Issues') + '</h3>' +
2147- nav([
2148- ['?', req._t('issues.Open'), 'open'],
2149- ['?state=closed', req._t('issues.Closed'), 'closed'],
2150- ['?state=all', req._t('issues.All'), 'all']
2151- ], state)),
2152- pull(
2153- (isPRs ? pullReqs : issues).list({
2154- repo: repo.id,
2155- project: repo.id,
2156- open: {open: true, closed: false}[state]
2157- }),
2158- pull.map(function (issue) {
2159- count++
2160- var state = (issue.open ? 'open' : 'closed')
2161- var stateStr = req._t(issue.open ?
2162- 'issue.state.Open' : 'issue.state.Closed')
2163- return '<section class="collapse">' +
2164- '<i class="issue-state issue-state-' + state + '"' +
2165- ' title="' + stateStr + '">◼</i> ' +
2166- '<a href="' + encodeLink(issue.id) + '">' +
2167- escapeHTML(issue.title) +
2168- '<span class="right-bar">' +
2169- new Date(issue.created_at).toLocaleString(req._locale) +
2170- '</span>' +
2171- '</a>' +
2172- '</section>'
2173- })
2174- ),
2175- readOnce(function (cb) {
2176- cb(null, count > 0 ? '' :
2177- '<p>' + req._t(isPRs ? 'NoPullRequests' : 'NoIssues') + '</p>')
2172+G.serveRepoIssues = function (req, repo, isPRs) {
2173+ var self = this
2174+ var count = 0
2175+ var state = req._u.query.state || 'open'
2176+ var newPath = isPRs ? [repo.id, 'compare'] : [repo.id, 'issues', 'new']
2177+ var title = req._t('Issues') + ' · %{author}/%{repo}'
2178+ var page = isPRs ? 'pulls' : 'issues'
2179+ return self.renderRepoPage(req, repo, page, null, title, cat([
2180+ pull.once(
2181+ (self.isPublic ? '' :
2182+ '<form class="right-bar" method="get"' +
2183+ ' action="' + encodeLink(newPath) + '">' +
2184+ '<button class="btn">&plus; ' +
2185+ req._t(isPRs ? 'pullRequest.New' : 'issue.New') +
2186+ '</button>' +
2187+ '</form>') +
2188+ '<h3>' + req._t(isPRs ? 'PullRequests' : 'Issues') + '</h3>' +
2189+ nav([
2190+ ['?', req._t('issues.Open'), 'open'],
2191+ ['?state=closed', req._t('issues.Closed'), 'closed'],
2192+ ['?state=all', req._t('issues.All'), 'all']
2193+ ], state)),
2194+ pull(
2195+ (isPRs ? self.pullReqs : self.issues).list({
2196+ repo: repo.id,
2197+ project: repo.id,
2198+ open: {open: true, closed: false}[state]
2199+ }),
2200+ pull.map(function (issue) {
2201+ count++
2202+ var state = (issue.open ? 'open' : 'closed')
2203+ var stateStr = req._t(issue.open ?
2204+ 'issue.state.Open' : 'issue.state.Closed')
2205+ return '<section class="collapse">' +
2206+ '<i class="issue-state issue-state-' + state + '"' +
2207+ ' title="' + stateStr + '">◼</i> ' +
2208+ '<a href="' + encodeLink(issue.id) + '">' +
2209+ escapeHTML(issue.title) +
2210+ '<span class="right-bar">' +
2211+ new Date(issue.created_at).toLocaleString(req._locale) +
2212+ '</span>' +
2213+ '</a>' +
2214+ '</section>'
21782215 })
2179- ]))
2180- }
2216+ ),
2217+ readOnce(function (cb) {
2218+ cb(null, count > 0 ? '' :
2219+ '<p>' + req._t(isPRs ? 'NoPullRequests' : 'NoIssues') + '</p>')
2220+ })
2221+ ]))
2222+}
21812223
2182- /* New Issue */
2224+/* New Issue */
21832225
2184- function serveRepoNewIssue(req, repo, issueId, path) {
2185- var title = req._t('issue.New') + ' · %{author}/%{repo}'
2186- return renderRepoPage(req, repo, 'issues', null, title, pull.once(
2187- '<h3>' + req._t('issue.New') + '</h3>' +
2188- '<section><form action="" method="post">' +
2189- '<input type="hidden" name="action" value="new-issue">' +
2190- '<p><input class="wide-input" name="title" placeholder="' +
2191- req._t('issue.Title') + '" size="77" /></p>' +
2192- renderPostForm(req, repo, req._t('Description'), 8) +
2193- '<button type="submit" class="btn">' + req._t('Create') + '</button>' +
2194- '</form></section>'))
2195- }
2226+G.serveRepoNewIssue = function (req, repo, issueId, path) {
2227+ var title = req._t('issue.New') + ' · %{author}/%{repo}'
2228+ return this.renderRepoPage(req, repo, 'issues', null, title, pull.once(
2229+ '<h3>' + req._t('issue.New') + '</h3>' +
2230+ '<section><form action="" method="post">' +
2231+ '<input type="hidden" name="action" value="new-issue">' +
2232+ '<p><input class="wide-input" name="title" placeholder="' +
2233+ req._t('issue.Title') + '" size="77" /></p>' +
2234+ renderPostForm(req, repo, req._t('Description'), 8) +
2235+ '<button type="submit" class="btn">' + req._t('Create') + '</button>' +
2236+ '</form></section>'))
2237+}
21962238
2197- /* Issue */
2239+/* Issue */
21982240
2199- function serveRepoIssue(req, repo, issue, path, postId) {
2200- var isAuthor = (myId == issue.author) || (myId == repo.feed)
2201- var newestMsg = {key: issue.id, value: {timestamp: issue.created_at}}
2202- var title = escapeHTML(issue.title) + ' · %{author}/%{repo}'
2203- return renderRepoPage(req, repo, 'issues', null, title, cat([
2204- pull.once(
2205- renderNameForm(req, !isPublic, issue.id, issue.title, 'issue-title',
2206- null, req._t('issue.Rename'),
2207- '<h3>' + link([issue.id], issue.title) + '</h3>') +
2208- '<code>' + issue.id + '</code>' +
2209- '<section class="collapse">' +
2210- (issue.open
2211- ? '<strong class="issue-status open">' +
2212- req._t('issue.state.Open') + '</strong>'
2213- : '<strong class="issue-status closed">' +
2214- req._t('issue.state.Closed') + '</strong>')),
2215- readOnce(function (cb) {
2216- about.getName(issue.author, function (err, authorName) {
2217- if (err) return cb(err)
2218- var authorLink = link([issue.author], authorName)
2219- cb(null, req._t('issue.Opened',
2220- {name: authorLink, datetime: timestamp(issue.created_at, req)}))
2221- })
2241+G.serveRepoIssue = function (req, repo, issue, path, postId) {
2242+ var self = this
2243+ var isAuthor = (self.myId == issue.author) || (self.myId == repo.feed)
2244+ var newestMsg = {key: issue.id, value: {timestamp: issue.created_at}}
2245+ var title = escapeHTML(issue.title) + ' · %{author}/%{repo}'
2246+ return self.renderRepoPage(req, repo, 'issues', null, title, cat([
2247+ pull.once(
2248+ renderNameForm(req, !self.isPublic, issue.id, issue.title, 'issue-title',
2249+ null, req._t('issue.Rename'),
2250+ '<h3>' + link([issue.id], issue.title) + '</h3>') +
2251+ '<code>' + issue.id + '</code>' +
2252+ '<section class="collapse">' +
2253+ (issue.open
2254+ ? '<strong class="issue-status open">' +
2255+ req._t('issue.state.Open') + '</strong>'
2256+ : '<strong class="issue-status closed">' +
2257+ req._t('issue.state.Closed') + '</strong>')),
2258+ readOnce(function (cb) {
2259+ self.about.getName(issue.author, function (err, authorName) {
2260+ if (err) return cb(err)
2261+ var authorLink = link([issue.author], authorName)
2262+ cb(null, req._t('issue.Opened',
2263+ {name: authorLink, datetime: timestamp(issue.created_at, req)}))
2264+ })
2265+ }),
2266+ pull.once('<hr/>' + markdown(issue.text, repo) + '</section>'),
2267+ // render posts and edits
2268+ pull(
2269+ self.ssb.links({
2270+ dest: issue.id,
2271+ values: true
22222272 }),
2223- pull.once('<hr/>' + markdown(issue.text, repo) + '</section>'),
2224- // render posts and edits
2225- pull(
2226- ssb.links({
2227- dest: issue.id,
2228- values: true
2229- }),
2230- pull.unique('key'),
2231- addAuthorName(about),
2232- sortMsgs(),
2233- pull.through(function (msg) {
2234- if (msg.value.timestamp > newestMsg.value.timestamp)
2235- newestMsg = msg
2236- }),
2237- pull.map(renderIssueActivityMsg.bind(null, req, repo, issue,
2238- req._t('issue.'), postId))
2239- ),
2240- isPublic ? pull.empty() : readOnce(function (cb) {
2241- cb(null, renderIssueCommentForm(req, issue, repo, newestMsg.key,
2242- isAuthor, req._t('issue.')))
2243- })
2244- ]))
2245- }
2273+ pull.unique('key'),
2274+ self.addAuthorName(),
2275+ sortMsgs(),
2276+ pull.through(function (msg) {
2277+ if (msg.value.timestamp > newestMsg.value.timestamp)
2278+ newestMsg = msg
2279+ }),
2280+ pull.map(self.renderIssueActivityMsg.bind(self, req, repo, issue,
2281+ req._t('issue.'), postId))
2282+ ),
2283+ self.isPublic ? pull.empty() : readOnce(function (cb) {
2284+ cb(null, renderIssueCommentForm(req, issue, repo, newestMsg.key,
2285+ isAuthor, req._t('issue.')))
2286+ })
2287+ ]))
2288+}
22462289
2247- function renderIssueActivityMsg(req, repo, issue, type, postId, msg) {
2248- var authorLink = link([msg.value.author], msg.authorName)
2249- var msgHref = encodeLink(msg.key) + '#' + encodeURIComponent(msg.key)
2250- var msgTimeLink = '<a href="' + msgHref + '"' +
2251- ' name="' + escapeHTML(msg.key) + '">' +
2252- new Date(msg.value.timestamp).toLocaleString(req._locale) + '</a>'
2253- var c = msg.value.content
2254- switch (c.type) {
2255- case 'post':
2256- if (c.root == issue.id) {
2257- var changed = issues.isStatusChanged(msg, issue)
2258- return '<section class="collapse">' +
2259- (msg.key == postId ? '<div class="highlight">' : '') +
2260- '<tt class="right-bar item-id">' + msg.key + '</tt> ' +
2261- (changed == null ? authorLink : req._t(
2262- changed ? 'issue.Reopened' : 'issue.Closed',
2263- {name: authorLink, type: type})) +
2264- ' &middot; ' + msgTimeLink +
2265- (msg.key == postId ? '</div>' : '') +
2266- markdown(c.text, repo) +
2267- '</section>'
2268- } else {
2269- var text = c.text || (c.type + ' ' + msg.key)
2270- return '<section class="collapse mention-preview">' +
2271- req._t('issue.MentionedIn', {
2272- name: authorLink,
2273- type: type,
2274- post: '<a href="/' + msg.key + '#' + msg.key + '">' +
2275- String(text).substr(0, 140) + '</a>'
2276- }) + '</section>'
2277- }
2278- case 'issue':
2279- case 'pull-request':
2290+G.renderIssueActivityMsg = function (req, repo, issue, type, postId, msg) {
2291+ var authorLink = link([msg.value.author], msg.authorName)
2292+ var msgHref = encodeLink(msg.key) + '#' + encodeURIComponent(msg.key)
2293+ var msgTimeLink = '<a href="' + msgHref + '"' +
2294+ ' name="' + escapeHTML(msg.key) + '">' +
2295+ new Date(msg.value.timestamp).toLocaleString(req._locale) + '</a>'
2296+ var c = msg.value.content
2297+ switch (c.type) {
2298+ case 'post':
2299+ if (c.root == issue.id) {
2300+ var changed = this.issues.isStatusChanged(msg, issue)
2301+ return '<section class="collapse">' +
2302+ (msg.key == postId ? '<div class="highlight">' : '') +
2303+ '<tt class="right-bar item-id">' + msg.key + '</tt> ' +
2304+ (changed == null ? authorLink : req._t(
2305+ changed ? 'issue.Reopened' : 'issue.Closed',
2306+ {name: authorLink, type: type})) +
2307+ ' &middot; ' + msgTimeLink +
2308+ (msg.key == postId ? '</div>' : '') +
2309+ markdown(c.text, repo) +
2310+ '</section>'
2311+ } else {
2312+ var text = c.text || (c.type + ' ' + msg.key)
22802313 return '<section class="collapse mention-preview">' +
22812314 req._t('issue.MentionedIn', {
22822315 name: authorLink,
22832316 type: type,
2284- post: link([msg.key], String(c.title || msg.key).substr(0, 140))
2317+ post: '<a href="/' + msg.key + '#' + msg.key + '">' +
2318+ String(text).substr(0, 140) + '</a>'
22852319 }) + '</section>'
2286- case 'issue-edit':
2320+ }
2321+ case 'issue':
2322+ case 'pull-request':
2323+ return '<section class="collapse mention-preview">' +
2324+ req._t('issue.MentionedIn', {
2325+ name: authorLink,
2326+ type: type,
2327+ post: link([msg.key], String(c.title || msg.key).substr(0, 140))
2328+ }) + '</section>'
2329+ case 'issue-edit':
2330+ return '<section class="collapse">' +
2331+ (msg.key == postId ? '<div class="highlight">' : '') +
2332+ (c.title == null ? '' : req._t('issue.Renamed', {
2333+ name: authorLink,
2334+ type: type,
2335+ name: '<q>' + escapeHTML(c.title) + '</q>'
2336+ })) + ' &middot; ' + msgTimeLink +
2337+ (msg.key == postId ? '</div>' : '') +
2338+ '</section>'
2339+ case 'git-update':
2340+ var mention = this.issues.getMention(msg, issue)
2341+ if (mention) {
2342+ var commitLink = link([repo.id, 'commit', mention.object],
2343+ mention.label || mention.object)
22872344 return '<section class="collapse">' +
2288- (msg.key == postId ? '<div class="highlight">' : '') +
2289- (c.title == null ? '' : req._t('issue.Renamed', {
2345+ req._t(mention.open ? 'issue.Reopened' : 'issue.Closed', {
22902346 name: authorLink,
2291- type: type,
2292- name: '<q>' + escapeHTML(c.title) + '</q>'
2293- })) + ' &middot; ' + msgTimeLink +
2294- (msg.key == postId ? '</div>' : '') +
2347+ type: type
2348+ }) + ' &middot; ' + msgTimeLink + '<br/>' +
2349+ commitLink +
22952350 '</section>'
2296- case 'git-update':
2297- var mention = issues.getMention(msg, issue)
2298- if (mention) {
2299- var commitLink = link([repo.id, 'commit', mention.object],
2300- mention.label || mention.object)
2301- return '<section class="collapse">' +
2302- req._t(mention.open ? 'issue.Reopened' : 'issue.Closed', {
2303- name: authorLink,
2304- type: type
2305- }) + ' &middot; ' + msgTimeLink + '<br/>' +
2306- commitLink +
2307- '</section>'
2308- } else if ((mention = getMention(msg, issue.id))) {
2309- var commitLink = link(mention.object ?
2310- [repo.id, 'commit', mention.object] : [msg.key],
2311- mention.label || mention.object || msg.key)
2312- return '<section class="collapse">' +
2313- req._t('issue.Mentioned', {
2314- name: authorLink,
2315- type: type
2316- }) + ' &middot; ' + msgTimeLink + '<br/>' +
2317- commitLink +
2318- '</section>'
2319- } else {
2320- // fallthrough
2321- }
2322-
2323- default:
2351+ } else if ((mention = getMention(msg, issue.id))) {
2352+ var commitLink = link(mention.object ?
2353+ [repo.id, 'commit', mention.object] : [msg.key],
2354+ mention.label || mention.object || msg.key)
23242355 return '<section class="collapse">' +
2325- authorLink +
2326- ' &middot; ' + msgTimeLink +
2327- json(c) +
2356+ req._t('issue.Mentioned', {
2357+ name: authorLink,
2358+ type: type
2359+ }) + ' &middot; ' + msgTimeLink + '<br/>' +
2360+ commitLink +
23282361 '</section>'
2329- }
2362+ } else {
2363+ // fallthrough
2364+ }
2365+
2366+ default:
2367+ return '<section class="collapse">' +
2368+ authorLink +
2369+ ' &middot; ' + msgTimeLink +
2370+ json(c) +
2371+ '</section>'
23302372 }
2373+}
23312374
23322375 function renderIssueCommentForm(req, issue, repo, branch, isAuthor, type) {
23332376 return '<section><form action="" method="post">' +
23342377 '<input type="hidden" name="action" value="comment">' +
@@ -2349,383 +2392,387 @@
23492392 }
23502393
23512394 /* Pull Request */
23522395
2353- function serveRepoPullReq(req, repo, pr, path, postId) {
2354- var headRepo, authorLink
2355- var page = path[0] || 'activity'
2356- var title = escapeHTML(pr.title) + ' · %{author}/%{repo}'
2357- return renderRepoPage(req, repo, 'pulls', null, title, cat([
2358- pull.once('<div class="pull-request">' +
2359- renderNameForm(req, !isPublic, pr.id, pr.title, 'issue-title', null,
2360- req._t('pullRequest.Rename'),
2361- '<h3>' + link([pr.id], pr.title) + '</h3>') +
2362- '<code>' + pr.id + '</code>'),
2363- readOnce(function (cb) {
2364- var done = multicb({ pluck: 1, spread: true })
2365- var gotHeadRepo = done()
2366- about.getName(pr.author, done())
2367- var sameRepo = (pr.headRepo == pr.baseRepo)
2368- getRepo(pr.headRepo, function (err, headRepo) {
2369- if (err) return cb(err)
2370- getRepoName(about, headRepo.feed, headRepo.id, done())
2371- about.getName(headRepo.feed, done())
2372- gotHeadRepo(null, Repo(headRepo))
2373- })
2396+G.serveRepoPullReq = function (req, repo, pr, path, postId) {
2397+ var self = this
2398+ var headRepo, authorLink
2399+ var page = path[0] || 'activity'
2400+ var title = escapeHTML(pr.title) + ' · %{author}/%{repo}'
2401+ return self.renderRepoPage(req, repo, 'pulls', null, title, cat([
2402+ pull.once('<div class="pull-request">' +
2403+ renderNameForm(req, !self.isPublic, pr.id, pr.title, 'issue-title', null,
2404+ req._t('pullRequest.Rename'),
2405+ '<h3>' + link([pr.id], pr.title) + '</h3>') +
2406+ '<code>' + pr.id + '</code>'),
2407+ readOnce(function (cb) {
2408+ var done = multicb({ pluck: 1, spread: true })
2409+ var gotHeadRepo = done()
2410+ self.about.getName(pr.author, done())
2411+ var sameRepo = (pr.headRepo == pr.baseRepo)
2412+ self.getRepo(pr.headRepo, function (err, headRepo) {
2413+ if (err) return cb(err)
2414+ self.getRepoName(headRepo.feed, headRepo.id, done())
2415+ self.about.getName(headRepo.feed, done())
2416+ gotHeadRepo(null, Repo(headRepo))
2417+ })
23742418
2375- done(function (err, _headRepo, issueAuthorName,
2376- headRepoName, headRepoAuthorName) {
2377- if (err) return cb(err)
2378- headRepo = _headRepo
2379- authorLink = link([pr.author], issueAuthorName)
2380- var repoLink = link([pr.headRepo], headRepoName)
2381- var headRepoAuthorLink = link([headRepo.feed], headRepoAuthorName)
2382- var headRepoLink = link([headRepo.id], headRepoName)
2383- var headBranchLink = link([headRepo.id, 'tree', pr.headBranch])
2384- var baseBranchLink = link([repo.id, 'tree', pr.baseBranch])
2385- cb(null, '<section class="collapse">' +
2386- '<strong class="issue-status ' +
2387- (pr.open ? 'open' : 'closed') + '">' +
2388- req._t(pr.open ? 'issue.state.Open' : 'issue.state.Closed') +
2389- '</strong> ' +
2390- req._t('pullRequest.WantToMerge', {
2391- name: authorLink,
2392- base: '<code>' + baseBranchLink + '</code>',
2393- head: (sameRepo ?
2394- '<code>' + headBranchLink + '</code>' :
2395- '<code class="bgslash">' +
2396- headRepoAuthorLink + ' / ' +
2397- headRepoLink + ' / ' +
2398- headBranchLink + '</code>')
2399- }) + '</section>')
2400- })
2401- }),
2402- pull.once(
2403- nav([
2404- [[pr.id], req._t('Discussion'), 'activity'],
2405- [[pr.id, 'commits'], req._t('Commits'), 'commits'],
2406- [[pr.id, 'files'], req._t('Files'), 'files']
2407- ], page)),
2408- readNext(function (cb) {
2409- if (page == 'commits')
2410- renderPullReqCommits(req, pr, repo, headRepo, cb)
2411- else if (page == 'files')
2412- renderPullReqFiles(req, pr, repo, headRepo, cb)
2413- else cb(null,
2414- renderPullReqActivity(req, pr, repo, headRepo, authorLink, postId))
2419+ done(function (err, _headRepo, issueAuthorName,
2420+ headRepoName, headRepoAuthorName) {
2421+ if (err) return cb(err)
2422+ headRepo = _headRepo
2423+ authorLink = link([pr.author], issueAuthorName)
2424+ var repoLink = link([pr.headRepo], headRepoName)
2425+ var headRepoAuthorLink = link([headRepo.feed], headRepoAuthorName)
2426+ var headRepoLink = link([headRepo.id], headRepoName)
2427+ var headBranchLink = link([headRepo.id, 'tree', pr.headBranch])
2428+ var baseBranchLink = link([repo.id, 'tree', pr.baseBranch])
2429+ cb(null, '<section class="collapse">' +
2430+ '<strong class="issue-status ' +
2431+ (pr.open ? 'open' : 'closed') + '">' +
2432+ req._t(pr.open ? 'issue.state.Open' : 'issue.state.Closed') +
2433+ '</strong> ' +
2434+ req._t('pullRequest.WantToMerge', {
2435+ name: authorLink,
2436+ base: '<code>' + baseBranchLink + '</code>',
2437+ head: (sameRepo ?
2438+ '<code>' + headBranchLink + '</code>' :
2439+ '<code class="bgslash">' +
2440+ headRepoAuthorLink + ' / ' +
2441+ headRepoLink + ' / ' +
2442+ headBranchLink + '</code>')
2443+ }) + '</section>')
24152444 })
2445+ }),
2446+ pull.once(
2447+ nav([
2448+ [[pr.id], req._t('Discussion'), 'activity'],
2449+ [[pr.id, 'commits'], req._t('Commits'), 'commits'],
2450+ [[pr.id, 'files'], req._t('Files'), 'files']
2451+ ], page)),
2452+ readNext(function (cb) {
2453+ if (page == 'commits')
2454+ self.renderPullReqCommits(req, pr, repo, headRepo, cb)
2455+ else if (page == 'files')
2456+ self.renderPullReqFiles(req, pr, repo, headRepo, cb)
2457+ else cb(null,
2458+ self.renderPullReqActivity(req, pr, repo, headRepo, authorLink, postId))
2459+ })
2460+ ]))
2461+}
2462+
2463+G.renderPullReqCommits = function (req, pr, baseRepo, headRepo, cb) {
2464+ var self = this
2465+ self.pullReqs.getRevs(pr.id, function (err, revs) {
2466+ if (err) return cb(null, renderError(err))
2467+ cb(null, cat([
2468+ pull.once('<section>'),
2469+ self.renderCommitLog(req, baseRepo, revs.base, headRepo, revs.head),
2470+ pull.once('</section>')
24162471 ]))
2417- }
2472+ })
2473+}
24182474
2419- function renderPullReqCommits(req, pr, baseRepo, headRepo, cb) {
2420- pullReqs.getRevs(pr.id, function (err, revs) {
2421- if (err) return cb(null, renderError(err))
2422- cb(null, cat([
2423- pull.once('<section>'),
2424- renderCommitLog(req, baseRepo, revs.base, headRepo, revs.head),
2425- pull.once('</section>')
2426- ]))
2427- })
2428- }
2475+G.renderPullReqFiles = function (req, pr, baseRepo, headRepo, cb) {
2476+ this.pullReqs.getRevs(pr.id, function (err, revs) {
2477+ if (err) return cb(null, renderError(err))
2478+ cb(null, cat([
2479+ pull.once('<section>'),
2480+ renderDiffStat(req,
2481+ [baseRepo, headRepo], [revs.base, revs.head]),
2482+ pull.once('</section>')
2483+ ]))
2484+ })
2485+}
24292486
2430- function renderPullReqFiles(req, pr, baseRepo, headRepo, cb) {
2431- pullReqs.getRevs(pr.id, function (err, revs) {
2432- if (err) return cb(null, renderError(err))
2433- cb(null, cat([
2434- pull.once('<section>'),
2435- renderDiffStat(req,
2436- [baseRepo, headRepo], [revs.base, revs.head]),
2437- pull.once('</section>')
2438- ]))
2439- })
2440- }
2441-
2442- function renderPullReqActivity(req, pr, repo, headRepo, authorLink, postId) {
2443- var msgTimeLink = link([pr.id],
2444- new Date(pr.created_at).toLocaleString(req._locale))
2445- var newestMsg = {key: pr.id, value: {timestamp: pr.created_at}}
2446- var isAuthor = (myId == pr.author) || (myId == repo.feed)
2447- return cat([
2448- readOnce(function (cb) {
2449- cb(null,
2450- '<section class="collapse">' +
2451- authorLink + ' &middot; ' + msgTimeLink +
2452- markdown(pr.text, repo) + '</section>')
2453- }),
2454- // render posts, edits, and updates
2455- pull(
2456- many([
2457- ssb.links({
2458- dest: pr.id,
2459- values: true
2460- }),
2461- readNext(function (cb) {
2462- cb(null, pull(
2463- ssb.links({
2464- dest: headRepo.id,
2465- source: headRepo.feed,
2466- rel: 'repo',
2467- values: true,
2468- reverse: true
2469- }),
2470- pull.take(function (link) {
2471- return link.value.timestamp > pr.created_at
2472- }),
2473- pull.filter(function (link) {
2474- return link.value.content.type == 'git-update'
2475- && ('refs/heads/' + pr.headBranch) in link.value.content.refs
2476- })
2477- ))
2478- })
2479- ]),
2480- addAuthorName(about),
2481- pull.unique('key'),
2482- pull.through(function (msg) {
2483- if (msg.value.timestamp > newestMsg.value.timestamp)
2484- newestMsg = msg
2487+G.renderPullReqActivity = function (req, pr, repo, headRepo, authorLink, postId) {
2488+ var self = this
2489+ var msgTimeLink = link([pr.id],
2490+ new Date(pr.created_at).toLocaleString(req._locale))
2491+ var newestMsg = {key: pr.id, value: {timestamp: pr.created_at}}
2492+ var isAuthor = (self.myId == pr.author) || (self.myId == repo.feed)
2493+ return cat([
2494+ readOnce(function (cb) {
2495+ cb(null,
2496+ '<section class="collapse">' +
2497+ authorLink + ' &middot; ' + msgTimeLink +
2498+ markdown(pr.text, repo) + '</section>')
2499+ }),
2500+ // render posts, edits, and updates
2501+ pull(
2502+ many([
2503+ self.ssb.links({
2504+ dest: pr.id,
2505+ values: true
24852506 }),
2486- sortMsgs(),
2487- pull.map(function (item) {
2488- if (item.value.content.type == 'git-update')
2489- return renderBranchUpdate(req, pr, item)
2490- return renderIssueActivityMsg(req, repo, pr,
2491- req._t('pull request'), postId, item)
2507+ readNext(function (cb) {
2508+ cb(null, pull(
2509+ self.ssb.links({
2510+ dest: headRepo.id,
2511+ source: headRepo.feed,
2512+ rel: 'repo',
2513+ values: true,
2514+ reverse: true
2515+ }),
2516+ pull.take(function (link) {
2517+ return link.value.timestamp > pr.created_at
2518+ }),
2519+ pull.filter(function (link) {
2520+ return link.value.content.type == 'git-update'
2521+ && ('refs/heads/' + pr.headBranch) in link.value.content.refs
2522+ })
2523+ ))
24922524 })
2493- ),
2494- !isPublic && isAuthor && pr.open && pull.once(
2495- '<section class="merge-instructions">' +
2496- '<input type="checkbox" class="toggle" id="merge-instructions"/>' +
2497- '<h4><label for="merge-instructions" class="toggle-link"><a>' +
2498- req._t('mergeInstructions.MergeViaCmdLine') +
2499- '</a></label></h4>' +
2500- '<div class="contents">' +
2501- '<p>' + req._t('mergeInstructions.CheckOut') + '</p>' +
2502- '<pre>' +
2503- 'git fetch ssb://' + escapeHTML(pr.headRepo) + ' ' +
2504- escapeHTML(pr.headBranch) + '\n' +
2505- 'git checkout -b ' + escapeHTML(pr.headBranch) + ' FETCH_HEAD' +
2506- '</pre>' +
2507- '<p>' + req._t('mergeInstructions.MergeAndPush') + '</p>' +
2508- '<pre>' +
2509- 'git checkout ' + escapeHTML(pr.baseBranch) + '\n' +
2510- 'git merge ' + escapeHTML(pr.headBranch) + '\n' +
2511- 'git push ssb ' + escapeHTML(pr.baseBranch) +
2512- '</pre>' +
2513- '</div></section>'),
2514- !isPublic && readOnce(function (cb) {
2515- cb(null, renderIssueCommentForm(req, pr, repo, newestMsg.key, isAuthor,
2516- req._t('pull request')))
2525+ ]),
2526+ self.addAuthorName(),
2527+ pull.unique('key'),
2528+ pull.through(function (msg) {
2529+ if (msg.value.timestamp > newestMsg.value.timestamp)
2530+ newestMsg = msg
2531+ }),
2532+ sortMsgs(),
2533+ pull.map(function (item) {
2534+ if (item.value.content.type == 'git-update')
2535+ return self.renderBranchUpdate(req, pr, item)
2536+ return self.renderIssueActivityMsg(req, repo, pr,
2537+ req._t('pull request'), postId, item)
25172538 })
2518- ])
2519- }
2539+ ),
2540+ !self.isPublic && isAuthor && pr.open && pull.once(
2541+ '<section class="merge-instructions">' +
2542+ '<input type="checkbox" class="toggle" id="merge-instructions"/>' +
2543+ '<h4><label for="merge-instructions" class="toggle-link"><a>' +
2544+ req._t('mergeInstructions.MergeViaCmdLine') +
2545+ '</a></label></h4>' +
2546+ '<div class="contents">' +
2547+ '<p>' + req._t('mergeInstructions.CheckOut') + '</p>' +
2548+ '<pre>' +
2549+ 'git fetch ssb://' + escapeHTML(pr.headRepo) + ' ' +
2550+ escapeHTML(pr.headBranch) + '\n' +
2551+ 'git checkout -b ' + escapeHTML(pr.headBranch) + ' FETCH_HEAD' +
2552+ '</pre>' +
2553+ '<p>' + req._t('mergeInstructions.MergeAndPush') + '</p>' +
2554+ '<pre>' +
2555+ 'git checkout ' + escapeHTML(pr.baseBranch) + '\n' +
2556+ 'git merge ' + escapeHTML(pr.headBranch) + '\n' +
2557+ 'git push ssb ' + escapeHTML(pr.baseBranch) +
2558+ '</pre>' +
2559+ '</div></section>'),
2560+ !self.isPublic && readOnce(function (cb) {
2561+ cb(null, renderIssueCommentForm(req, pr, repo, newestMsg.key, isAuthor,
2562+ req._t('pull request')))
2563+ })
2564+ ])
2565+}
25202566
2521- function renderBranchUpdate(req, pr, msg) {
2522- var authorLink = link([msg.value.author], msg.authorName)
2523- var msgLink = link([msg.key],
2524- new Date(msg.value.timestamp).toLocaleString(req._locale))
2525- var rev = msg.value.content.refs['refs/heads/' + pr.headBranch]
2526- if (!rev)
2527- return '<section class="collapse">' +
2528- req._t('NameDeletedBranch', {
2529- name: authorLink,
2530- branch: '<code>' + pr.headBranch + '</code>'
2531- }) + ' &middot; ' + msgLink +
2532- '</section>'
2533-
2534- var revLink = link([pr.headRepo, 'commit', rev], rev.substr(0, 8))
2567+G.renderBranchUpdate = function (req, pr, msg) {
2568+ var authorLink = link([msg.value.author], msg.authorName)
2569+ var msgLink = link([msg.key],
2570+ new Date(msg.value.timestamp).toLocaleString(req._locale))
2571+ var rev = msg.value.content.refs['refs/heads/' + pr.headBranch]
2572+ if (!rev)
25352573 return '<section class="collapse">' +
2536- req._t('NameUpdatedBranch', {
2574+ req._t('NameDeletedBranch', {
25372575 name: authorLink,
2538- rev: '<code>' + revLink + '</code>'
2576+ branch: '<code>' + pr.headBranch + '</code>'
25392577 }) + ' &middot; ' + msgLink +
25402578 '</section>'
2541- }
25422579
2543- /* Compare changes */
2580+ var revLink = link([pr.headRepo, 'commit', rev], rev.substr(0, 8))
2581+ return '<section class="collapse">' +
2582+ req._t('NameUpdatedBranch', {
2583+ name: authorLink,
2584+ rev: '<code>' + revLink + '</code>'
2585+ }) + ' &middot; ' + msgLink +
2586+ '</section>'
2587+}
25442588
2545- function serveRepoCompare(req, repo) {
2546- var query = req._u.query
2547- var base
2548- var count = 0
2549- var title = req._t('CompareChanges') + ' · %{author}/%{repo}'
2589+/* Compare changes */
25502590
2551- return renderRepoPage(req, repo, 'pulls', null, title, cat([
2552- pull.once('<h3>' + req._t('CompareChanges') + '</h3>' +
2553- '<form action="' + encodeLink(repo.id) + '/comparing" method="get">' +
2554- '<section>'),
2555- pull.once(req._t('BaseBranch') + ': '),
2556- readNext(function (cb) {
2557- if (query.base) gotBase(null, query.base)
2558- else repo.getSymRef('HEAD', true, gotBase)
2559- function gotBase(err, ref) {
2591+G.serveRepoCompare = function (req, repo) {
2592+ var self = this
2593+ var query = req._u.query
2594+ var base
2595+ var count = 0
2596+ var title = req._t('CompareChanges') + ' · %{author}/%{repo}'
2597+
2598+ return self.renderRepoPage(req, repo, 'pulls', null, title, cat([
2599+ pull.once('<h3>' + req._t('CompareChanges') + '</h3>' +
2600+ '<form action="' + encodeLink(repo.id) + '/comparing" method="get">' +
2601+ '<section>'),
2602+ pull.once(req._t('BaseBranch') + ': '),
2603+ readNext(function (cb) {
2604+ if (query.base) gotBase(null, query.base)
2605+ else repo.getSymRef('HEAD', true, gotBase)
2606+ function gotBase(err, ref) {
2607+ if (err) return cb(err)
2608+ cb(null, branchMenu(repo, 'base', base = ref || 'HEAD'))
2609+ }
2610+ }),
2611+ pull.once('<br/>' + req._t('ComparisonRepoBranch') + ':'),
2612+ pull(
2613+ self.getForks(repo, true),
2614+ pull.asyncMap(function (msg, cb) {
2615+ self.getRepo(msg.key, function (err, repo) {
25602616 if (err) return cb(err)
2561- cb(null, branchMenu(repo, 'base', base = ref || 'HEAD'))
2617+ cb(null, {
2618+ msg: msg,
2619+ repo: repo
2620+ })
2621+ })
2622+ }),
2623+ pull.map(renderFork),
2624+ pull.flatten()
2625+ ),
2626+ pull.once('</section>'),
2627+ readOnce(function (cb) {
2628+ cb(null, count == 0 ? req._t('NoBranches') :
2629+ '<button type="submit" class="btn">' +
2630+ req._t('Compare') + '</button>')
2631+ }),
2632+ pull.once('</form>')
2633+ ]))
2634+
2635+ function renderFork(fork) {
2636+ return pull(
2637+ fork.repo.refs(),
2638+ pull.map(function (ref) {
2639+ var m = /^refs\/([^\/]*)\/(.*)$/.exec(ref.name) || [,ref.name]
2640+ return {
2641+ type: m[1],
2642+ name: m[2],
2643+ value: ref.value
25622644 }
25632645 }),
2564- pull.once('<br/>' + req._t('ComparisonRepoBranch') + ':'),
2565- pull(
2566- getForks(repo, true),
2567- pull.asyncMap(function (msg, cb) {
2568- getRepo(msg.key, function (err, repo) {
2569- if (err) return cb(err)
2570- cb(null, {
2571- msg: msg,
2572- repo: repo
2573- })
2574- })
2575- }),
2576- pull.map(renderFork),
2577- pull.flatten()
2578- ),
2579- pull.once('</section>'),
2580- readOnce(function (cb) {
2581- cb(null, count == 0 ? req._t('NoBranches') :
2582- '<button type="submit" class="btn">' +
2583- req._t('Compare') + '</button>')
2646+ pull.filter(function (ref) {
2647+ return ref.type == 'heads'
2648+ && !(ref.name == base && fork.msg.key == repo.id)
25842649 }),
2585- pull.once('</form>')
2586- ]))
2587-
2588- function renderFork(fork) {
2589- return pull(
2590- fork.repo.refs(),
2591- pull.map(function (ref) {
2592- var m = /^refs\/([^\/]*)\/(.*)$/.exec(ref.name) || [,ref.name]
2593- return {
2594- type: m[1],
2595- name: m[2],
2596- value: ref.value
2597- }
2598- }),
2599- pull.filter(function (ref) {
2600- return ref.type == 'heads'
2601- && !(ref.name == base && fork.msg.key == repo.id)
2602- }),
2603- pull.map(function (ref) {
2604- var branchLink = link([fork.msg.key, 'tree', ref.name], ref.name)
2605- var authorLink = link([fork.msg.value.author], fork.msg.authorName)
2606- var repoLink = link([fork.msg.key], fork.msg.repoName)
2607- var value = fork.msg.key + ':' + ref.name
2608- count++
2609- return '<div class="bgslash">' +
2610- '<input type="radio" name="head"' +
2611- ' value="' + escapeHTML(value) + '"' +
2612- (query.head == value ? ' checked="checked"' : '') + '> ' +
2613- authorLink + ' / ' + repoLink + ' / ' + branchLink + '</div>'
2614- })
2615- )
2616- }
2650+ pull.map(function (ref) {
2651+ var branchLink = link([fork.msg.key, 'tree', ref.name], ref.name)
2652+ var authorLink = link([fork.msg.value.author], fork.msg.authorName)
2653+ var repoLink = link([fork.msg.key], fork.msg.repoName)
2654+ var value = fork.msg.key + ':' + ref.name
2655+ count++
2656+ return '<div class="bgslash">' +
2657+ '<input type="radio" name="head"' +
2658+ ' value="' + escapeHTML(value) + '"' +
2659+ (query.head == value ? ' checked="checked"' : '') + '> ' +
2660+ authorLink + ' / ' + repoLink + ' / ' + branchLink + '</div>'
2661+ })
2662+ )
26172663 }
2664+}
26182665
2619- function serveRepoComparing(req, repo) {
2620- var query = req._u.query
2621- var baseBranch = query.base
2622- var s = (query.head || '').split(':')
2666+G.serveRepoComparing = function (req, repo) {
2667+ var self = this
2668+ var query = req._u.query
2669+ var baseBranch = query.base
2670+ var s = (query.head || '').split(':')
26232671
2624- if (!s || !baseBranch)
2625- return serveRedirect(req, encodeLink([repo.id, 'compare']))
2672+ if (!s || !baseBranch)
2673+ return self.serveRedirect(req, encodeLink([repo.id, 'compare']))
26262674
2627- var headRepoId = s[0]
2628- var headBranch = s[1]
2629- var baseLink = link([repo.id, 'tree', baseBranch])
2630- var headBranchLink = link([headRepoId, 'tree', headBranch])
2631- var backHref = encodeLink([repo.id, 'compare']) + req._u.search
2632- var title = req._t(query.expand ? 'OpenPullRequest': 'ComparingChanges')
2633- var pageTitle = title + ' · %{author}/%{repo}'
2675+ var headRepoId = s[0]
2676+ var headBranch = s[1]
2677+ var baseLink = link([repo.id, 'tree', baseBranch])
2678+ var headBranchLink = link([headRepoId, 'tree', headBranch])
2679+ var backHref = encodeLink([repo.id, 'compare']) + req._u.search
2680+ var title = req._t(query.expand ? 'OpenPullRequest': 'ComparingChanges')
2681+ var pageTitle = title + ' · %{author}/%{repo}'
26342682
2635- return renderRepoPage(req, repo, 'pulls', null, pageTitle, cat([
2636- pull.once('<h3>' + title + '</h3>'),
2637- readNext(function (cb) {
2638- getRepo(headRepoId, function (err, headRepo) {
2639- if (err) return cb(err)
2640- getRepoFullName(about, headRepo.feed, headRepo.id,
2641- function (err, repoName, authorName) {
2642- if (err) return cb(err)
2643- cb(null, renderRepoInfo(Repo(headRepo), repoName, authorName))
2644- }
2645- )
2646- })
2683+ return self.renderRepoPage(req, repo, 'pulls', null, pageTitle, cat([
2684+ pull.once('<h3>' + title + '</h3>'),
2685+ readNext(function (cb) {
2686+ self.getRepo(headRepoId, function (err, headRepo) {
2687+ if (err) return cb(err)
2688+ self.getRepoFullName(headRepo.feed, headRepo.id,
2689+ function (err, repoName, authorName) {
2690+ if (err) return cb(err)
2691+ cb(null, renderRepoInfo(Repo(headRepo), repoName, authorName))
2692+ }
2693+ )
26472694 })
2648- ]))
2695+ })
2696+ ]))
26492697
2650- function renderRepoInfo(headRepo, headRepoName, headRepoAuthorName) {
2651- var authorLink = link([headRepo.feed], headRepoAuthorName)
2652- var repoLink = link([headRepoId], headRepoName)
2653- return cat([
2654- pull.once('<section>' +
2655- req._t('Base') + ': ' + baseLink + '<br/>' +
2656- req._t('Head') + ': ' +
2657- '<span class="bgslash">' + authorLink + ' / ' + repoLink +
2658- ' / ' + headBranchLink + '</span>' +
2659- '</section>' +
2660- (query.expand ? '<section><form method="post" action="">' +
2661- hiddenInputs({
2662- action: 'new-pull',
2663- branch: baseBranch,
2664- head_repo: headRepoId,
2665- head_branch: headBranch
2666- }) +
2667- '<input class="wide-input" name="title"' +
2668- ' placeholder="' + req._t('Title') + '" size="77"/>' +
2669- renderPostForm(req, repo, req._t('Description'), 8) +
2670- '<button type="submit" class="btn open">' +
2671- req._t('Create') + '</button>' +
2672- '</form></section>'
2673- : '<section><form method="get" action="">' +
2674- hiddenInputs({
2675- base: baseBranch,
2676- head: query.head
2677- }) +
2678- '<button class="btn open" type="submit" name="expand" value="1">' +
2679- '<i>⎇</i> ' + req._t('CreatePullRequest') + '</button> ' +
2680- '<a href="' + backHref + '">' + req._t('Back') + '</a>' +
2681- '</form></section>') +
2682- '<div id="commits"></div>' +
2683- '<div class="tab-links">' +
2684- '<a href="#" id="files-link">' + req._t('FilesChanged') + '</a> ' +
2685- '<a href="#commits" id="commits-link">' +
2686- req._t('Commits') + '</a>' +
2687- '</div>' +
2688- '<section id="files-tab">'),
2689- renderDiffStat(req, [repo, headRepo], [baseBranch, headBranch]),
2690- pull.once('</section>' +
2691- '<section id="commits-tab">'),
2692- renderCommitLog(req, repo, baseBranch, headRepo, headBranch),
2693- pull.once('</section>')
2694- ])
2695- }
2696- }
2697-
2698- function renderCommitLog(req, baseRepo, baseBranch, headRepo, headBranch) {
2698+ function renderRepoInfo(headRepo, headRepoName, headRepoAuthorName) {
2699+ var authorLink = link([headRepo.feed], headRepoAuthorName)
2700+ var repoLink = link([headRepoId], headRepoName)
26992701 return cat([
2700- pull.once('<table class="compare-commits">'),
2701- readNext(function (cb) {
2702- baseRepo.resolveRef(baseBranch, function (err, baseBranchRev) {
2703- if (err) return cb(err)
2704- var currentDay
2705- return cb(null, pull(
2706- headRepo.readLog(headBranch),
2707- pull.take(function (rev) { return rev != baseBranchRev }),
2708- pullReverse(),
2709- paramap(headRepo.getCommitParsed.bind(headRepo), 8),
2710- pull.map(function (commit) {
2711- var commitPath = [headRepo.id, 'commit', commit.id]
2712- var commitIdShort = '<tt>' + commit.id.substr(0, 8) + '</tt>'
2713- var day = Math.floor(commit.author.date / 86400000)
2714- var dateRow = day == currentDay ? '' :
2715- '<tr><th colspan=3 class="date-info">' +
2716- commit.author.date.toLocaleDateString(req._locale) +
2717- '</th><tr>'
2718- currentDay = day
2719- return dateRow + '<tr>' +
2720- '<td>' + escapeHTML(commit.author.name) + '</td>' +
2721- '<td>' + link(commitPath, commit.title) + '</td>' +
2722- '<td>' + link(commitPath, commitIdShort, true) + '</td>' +
2723- '</tr>'
2724- })
2725- ))
2726- })
2727- }),
2728- pull.once('</table>')
2702+ pull.once('<section>' +
2703+ req._t('Base') + ': ' + baseLink + '<br/>' +
2704+ req._t('Head') + ': ' +
2705+ '<span class="bgslash">' + authorLink + ' / ' + repoLink +
2706+ ' / ' + headBranchLink + '</span>' +
2707+ '</section>' +
2708+ (query.expand ? '<section><form method="post" action="">' +
2709+ hiddenInputs({
2710+ action: 'new-pull',
2711+ branch: baseBranch,
2712+ head_repo: headRepoId,
2713+ head_branch: headBranch
2714+ }) +
2715+ '<input class="wide-input" name="title"' +
2716+ ' placeholder="' + req._t('Title') + '" size="77"/>' +
2717+ renderPostForm(req, repo, req._t('Description'), 8) +
2718+ '<button type="submit" class="btn open">' +
2719+ req._t('Create') + '</button>' +
2720+ '</form></section>'
2721+ : '<section><form method="get" action="">' +
2722+ hiddenInputs({
2723+ base: baseBranch,
2724+ head: query.head
2725+ }) +
2726+ '<button class="btn open" type="submit" name="expand" value="1">' +
2727+ '<i>⎇</i> ' + req._t('CreatePullRequest') + '</button> ' +
2728+ '<a href="' + backHref + '">' + req._t('Back') + '</a>' +
2729+ '</form></section>') +
2730+ '<div id="commits"></div>' +
2731+ '<div class="tab-links">' +
2732+ '<a href="#" id="files-link">' + req._t('FilesChanged') + '</a> ' +
2733+ '<a href="#commits" id="commits-link">' +
2734+ req._t('Commits') + '</a>' +
2735+ '</div>' +
2736+ '<section id="files-tab">'),
2737+ renderDiffStat(req, [repo, headRepo], [baseBranch, headBranch]),
2738+ pull.once('</section>' +
2739+ '<section id="commits-tab">'),
2740+ self.renderCommitLog(req, repo, baseBranch, headRepo, headBranch),
2741+ pull.once('</section>')
27292742 ])
27302743 }
27312744 }
2745+
2746+G.renderCommitLog = function (req, baseRepo, baseBranch, headRepo, headBranch) {
2747+ return cat([
2748+ pull.once('<table class="compare-commits">'),
2749+ readNext(function (cb) {
2750+ baseRepo.resolveRef(baseBranch, function (err, baseBranchRev) {
2751+ if (err) return cb(err)
2752+ var currentDay
2753+ return cb(null, pull(
2754+ headRepo.readLog(headBranch),
2755+ pull.take(function (rev) { return rev != baseBranchRev }),
2756+ pullReverse(),
2757+ paramap(headRepo.getCommitParsed.bind(headRepo), 8),
2758+ pull.map(function (commit) {
2759+ var commitPath = [headRepo.id, 'commit', commit.id]
2760+ var commitIdShort = '<tt>' + commit.id.substr(0, 8) + '</tt>'
2761+ var day = Math.floor(commit.author.date / 86400000)
2762+ var dateRow = day == currentDay ? '' :
2763+ '<tr><th colspan=3 class="date-info">' +
2764+ commit.author.date.toLocaleDateString(req._locale) +
2765+ '</th><tr>'
2766+ currentDay = day
2767+ return dateRow + '<tr>' +
2768+ '<td>' + escapeHTML(commit.author.name) + '</td>' +
2769+ '<td>' + link(commitPath, commit.title) + '</td>' +
2770+ '<td>' + link(commitPath, commitIdShort, true) + '</td>' +
2771+ '</tr>'
2772+ })
2773+ ))
2774+ })
2775+ }),
2776+ pull.once('</table>')
2777+ ])
2778+}
server.jsView
@@ -9,17 +9,15 @@
99 var config = require('ssb-config/inject')(appName)
1010 var ssbClient = require('ssb-client')
1111 var keys = require('ssb-keys')
1212 .loadOrCreateSync(require('path').join(config.path, 'secret'))
13+var Web = require('.')
1314
14-var opts = config
15-opts.listenAddr = opts._[1]
16-opts.appname = appName
15+config.listenAddr = config._[1]
16+config.appname = appName
1717
18-require('.')(opts, function (err, server) {
19- require('ssb-reconnect')(function (cb) {
20- ssbClient(keys, config, cb)
21- }, function (err, ssb, reconnect) {
22- if (err) throw err
23- server.setSSB(ssb, reconnect)
24- })
18+require('ssb-reconnect')(function (cb) {
19+ ssbClient(keys, config, cb)
20+}, function (err, ssb, reconnect) {
21+ if (err) throw err
22+ Web.init(ssb, config, reconnect)
2523 })

Built with git-ssb-web