git ssb

30+

cel / git-ssb-web



Tree: 51e9718cb367cc06af5a19f5ebafb0887420e957

Files: 51e9718cb367cc06af5a19f5ebafb0887420e957 / index.js

91765 bytesRaw
1var fs = require('fs')
2var http = require('http')
3var path = require('path')
4var url = require('url')
5var qs = require('querystring')
6var util = require('util')
7var ref = require('ssb-ref')
8var pull = require('pull-stream')
9var ssbGit = require('ssb-git-repo')
10var toPull = require('stream-to-pull-stream')
11var cat = require('pull-cat')
12var Repo = require('pull-git-repo')
13var ssbAbout = require('./about')
14var ssbVotes = require('./votes')
15var i18n = require('./i18n')
16var marked = require('ssb-marked')
17var asyncMemo = require('asyncmemo')
18var multicb = require('multicb')
19var schemas = require('ssb-msg-schemas')
20var Issues = require('ssb-issues')
21var PullRequests = require('ssb-pull-requests')
22var paramap = require('pull-paramap')
23var gitPack = require('pull-git-pack')
24var Mentions = require('ssb-mentions')
25var Highlight = require('highlight.js')
26var JsDiff = require('diff')
27var many = require('pull-many')
28
29var hlCssPath = path.resolve(require.resolve('highlight.js'), '../../styles')
30
31// render links to git objects and ssb objects
32var blockRenderer = new marked.Renderer()
33blockRenderer.urltransform = function (url) {
34 if (ref.isLink(url))
35 return encodeLink(url)
36 if (/^[0-9a-f]{40}$/.test(url) && this.options.repo)
37 return encodeLink([this.options.repo.id, 'commit', url])
38 return url
39}
40
41blockRenderer.image = function (href, title, text) {
42 href = href.replace(/^&/, '&')
43 var url
44 if (ref.isBlobId(href))
45 url = encodeLink(href)
46 else if (/^https?:\/\//.test(href))
47 url = href
48 else if (this.options.repo && this.options.rev && this.options.path)
49 url = path.join('/', encodeURIComponent(this.options.repo.id),
50 'raw', this.options.rev, this.options.path.join('/'), href)
51 else
52 return text
53 return '<img src="' + escapeHTML(url) + '" alt="' + text + '"' +
54 (title ? ' title="' + title + '"' : '') + '/>'
55}
56
57blockRenderer.mention = function (preceding, id) {
58 // prevent broken name mention
59 if (id[0] == '@' && !ref.isFeed(id))
60 return (preceding||'') + escapeHTML(id)
61
62 return marked.Renderer.prototype.mention.call(this, preceding, id)
63}
64
65function getExtension(filename) {
66 return (/\.([^.]+)$/.exec(filename) || [,filename])[1]
67}
68
69function highlight(code, lang) {
70 try {
71 return lang
72 ? Highlight.highlight(lang, code).value
73 : Highlight.highlightAuto(code).value
74 } catch(e) {
75 if (/^Unknown language/.test(e.message))
76 return escapeHTML(code)
77 throw e
78 }
79}
80
81marked.setOptions({
82 gfm: true,
83 mentions: true,
84 tables: true,
85 breaks: true,
86 pedantic: false,
87 sanitize: true,
88 smartLists: true,
89 smartypants: false,
90 highlight: highlight,
91 renderer: blockRenderer
92})
93
94// hack to make git link mentions work
95var mdRules = new marked.InlineLexer(1, marked.defaults).rules
96mdRules.mention =
97 /^(\s)?([@%&][A-Za-z0-9\._\-+=\/]*[A-Za-z0-9_\-+=\/]|[0-9a-f]{40})/
98mdRules.text = /^[\s\S]+?(?=[\\<!\[_*`]| {2,}\n| [@%&]|[0-9a-f]{40}|$)/
99
100function markdown(text, options, cb) {
101 if (!text) return ''
102 if (typeof text != 'string') text = String(text)
103 if (!options) options = {}
104 else if (options.id) options = {repo: options}
105 if (!options.rev) options.rev = 'HEAD'
106 if (!options.path) options.path = []
107
108 return marked(text, options, cb)
109}
110
111function ParamError(msg) {
112 var err = Error.call(this, msg)
113 err.name = ParamError.name
114 return err
115}
116util.inherits(ParamError, Error)
117
118function parseAddr(str, def) {
119 if (!str) return def
120 var i = str.lastIndexOf(':')
121 if (~i) return {host: str.substr(0, i), port: str.substr(i+1)}
122 if (isNaN(str)) return {host: str, port: def.port}
123 return {host: def.host, port: str}
124}
125
126function isArray(arr) {
127 return Object.prototype.toString.call(arr) == '[object Array]'
128}
129
130function encodeLink(url) {
131 if (!isArray(url)) url = [url]
132 return '/' + url.map(encodeURIComponent).join('/')
133}
134
135function link(parts, text, raw, props) {
136 if (text == null) text = parts[parts.length-1]
137 if (!raw) text = escapeHTML(text)
138 return '<a href="' + encodeLink(parts) + '"' +
139 (props ? ' ' + props : '') +
140 '>' + text + '</a>'
141}
142
143function linkify(text) {
144 // regex is from ssb-ref
145 return text.replace(/(@|%|&|&amp;)[A-Za-z0-9\/+]{43}=\.[\w\d]+/g, function (str) {
146 return '<a href="/' + encodeURIComponent(str) + '">' + str + '</a>'
147 })
148}
149
150function timestamp(time, req) {
151 time = Number(time)
152 var d = new Date(time)
153 return '<span title="' + time + '">' +
154 d.toLocaleString(req._locale) + '</span>'
155}
156
157function pre(text) {
158 return '<pre>' + escapeHTML(text) + '</pre>'
159}
160
161function json(obj) {
162 return linkify(pre(JSON.stringify(obj, null, 2)))
163}
164
165function escapeHTML(str) {
166 return String(str)
167 .replace(/&/g, '&amp;')
168 .replace(/</g, '&lt;')
169 .replace(/>/g, '&gt;')
170 .replace(/"/g, '&quot;')
171}
172
173function table(props) {
174 return function (read) {
175 return cat([
176 pull.once('<table' + (props ? ' ' + props : '') + '>'),
177 pull(
178 read,
179 pull.map(function (row) {
180 return row ? '<tr>' + row.map(function (cell) {
181 return '<td>' + cell + '</td>'
182 }).join('') + '</tr>' : ''
183 })
184 ),
185 pull.once('</table>')
186 ])
187 }
188}
189
190function ul(props) {
191 return function (read) {
192 return cat([
193 pull.once('<ul' + (props ? ' ' + props : '') + '>'),
194 pull(
195 read,
196 pull.map(function (li) {
197 return '<li>' + li + '</li>'
198 })
199 ),
200 pull.once('</ul>')
201 ])
202 }
203}
204
205function nav(links, page, after) {
206 return ['<nav>'].concat(
207 links.map(function (link) {
208 var href = typeof link[0] == 'string' ? link[0] : encodeLink(link[0])
209 var props = link[2] == page ? ' class="active"' : ''
210 return '<a href="' + href + '"' + props + '>' + link[1] + '</a>'
211 }), after || '', '</nav>').join('')
212}
213
214function renderNameForm(req, enabled, id, name, action, inputId, title, header) {
215 if (!inputId) inputId = action
216 return '<form class="petname" action="" method="post">' +
217 (enabled ?
218 '<input type="checkbox" class="name-checkbox" id="' + inputId + '" ' +
219 'onfocus="this.form.name.focus()" />' +
220 '<input name="name" class="name" value="' + escapeHTML(name) + '" ' +
221 'onkeyup="if (event.keyCode == 27) this.form.reset()" />' +
222 '<input type="hidden" name="action" value="' + action + '">' +
223 '<input type="hidden" name="id" value="' +
224 escapeHTML(id) + '">' +
225 '<label class="name-toggle" for="' + inputId + '" ' +
226 'title="' + title + '"><i>✍</i></label> ' +
227 '<input class="btn name-btn" type="submit" value="' +
228 req._t('Rename') + '">' +
229 header :
230 header + '<br clear="all"/>'
231 ) +
232 '</form>'
233}
234
235function renderPostForm(req, repo, placeholder, rows) {
236 return '<input type="radio" class="tab-radio" id="tab1" name="tab" checked="checked"/>' +
237 '<input type="radio" class="tab-radio" id="tab2" name="tab"/>' +
238 '<div id="tab-links" class="tab-links" style="display:none">' +
239 '<label for="tab1" id="write-tab-link" class="tab1-link">' +
240 req._t('post.Write') + '</label>' +
241 '<label for="tab2" id="preview-tab-link" class="tab2-link">' +
242 req._t('post.Preview') + '</label>' +
243 '</div>' +
244 '<input type="hidden" id="repo-id" value="' + repo.id + '"/>' +
245 '<div id="write-tab" class="tab1">' +
246 '<textarea id="post-text" name="text" class="wide-input"' +
247 ' rows="' + (rows||4) + '" cols="77"' +
248 (placeholder ? ' placeholder="' + placeholder + '"' : '') +
249 '></textarea>' +
250 '</div>' +
251 '<div class="preview-text tab2" id="preview-tab"></div>' +
252 '<script>' + issueCommentScript + '</script>'
253}
254
255function hiddenInputs(values) {
256 return Object.keys(values).map(function (key) {
257 return '<input type="hidden"' +
258 ' name="' + escapeHTML(key) + '"' +
259 ' value="' + escapeHTML(values[key]) + '"/>'
260 }).join('')
261}
262
263function readNext(fn) {
264 var next
265 return function (end, cb) {
266 if (next) return next(end, cb)
267 fn(function (err, _next) {
268 if (err) return cb(err)
269 next = _next
270 next(null, cb)
271 })
272 }
273}
274
275function readOnce(fn) {
276 var ended
277 return function (end, cb) {
278 fn(function (err, data) {
279 if (err || ended) return cb(err || ended)
280 ended = true
281 cb(null, data)
282 })
283 }
284}
285
286function paginate(onFirst, through, onLast, onEmpty) {
287 var ended, last, first = true, queue = []
288 return function (read) {
289 var mappedRead = through(function (end, cb) {
290 if (ended = end) return read(ended, cb)
291 if (queue.length)
292 return cb(null, queue.shift())
293 read(null, function (end, data) {
294 if (end) return cb(end)
295 last = data
296 cb(null, data)
297 })
298 })
299 return function (end, cb) {
300 var tmp
301 if (ended) return cb(ended)
302 if (ended = end) return read(ended, cb)
303 if (first)
304 return read(null, function (end, data) {
305 if (ended = end) {
306 if (end === true && onEmpty)
307 return onEmpty(cb)
308 return cb(ended)
309 }
310 first = false
311 last = data
312 queue.push(data)
313 if (onFirst)
314 onFirst(data, cb)
315 else
316 mappedRead(null, cb)
317 })
318 mappedRead(null, function (end, data) {
319 if (ended = end) {
320 if (end === true && last)
321 return onLast(last, cb)
322 }
323 cb(end, data)
324 })
325 }
326 }
327}
328
329function readObjectString(obj, cb) {
330 pull(obj.read, pull.collect(function (err, bufs) {
331 if (err) return cb(err)
332 cb(null, Buffer.concat(bufs, obj.length).toString('utf8'))
333 }))
334}
335
336function getRepoObjectString(repo, id, cb) {
337 if (!id) return cb(null, '')
338 repo.getObjectFromAny(id, function (err, obj) {
339 if (err) return cb(err)
340 readObjectString(obj, cb)
341 })
342}
343
344function compareMsgs(a, b) {
345 return (a.value.timestamp - b.value.timestamp) || (a.key - b.key)
346}
347
348function pullSort(comparator) {
349 return function (read) {
350 return readNext(function (cb) {
351 pull(read, pull.collect(function (err, items) {
352 if (err) return cb(err)
353 items.sort(comparator)
354 cb(null, pull.values(items))
355 }))
356 })
357 }
358}
359
360function sortMsgs() {
361 return pullSort(compareMsgs)
362}
363
364function pullReverse() {
365 return function (read) {
366 return readNext(function (cb) {
367 pull(read, pull.collect(function (err, items) {
368 cb(err, items && pull.values(items.reverse()))
369 }))
370 })
371 }
372}
373
374function tryDecodeURIComponent(str) {
375 if (!str || (str[0] == '%' && ref.isBlobId(str)))
376 return str
377 try {
378 str = decodeURIComponent(str)
379 } finally {
380 return str
381 }
382}
383
384function getMention(msg, id) {
385 if (msg.key == id) return msg
386 var mentions = msg.value.content.mentions
387 if (mentions) for (var i = 0; i < mentions.length; i++) {
388 var mention = mentions[i]
389 if (mention.link == id)
390 return mention
391 }
392 return null
393}
394
395var hasOwnProp = Object.prototype.hasOwnProperty
396
397function getContentType(filename) {
398 var ext = getExtension(filename)
399 return contentTypes[ext] || imgMimes[ext] || 'text/plain; charset=utf-8'
400}
401
402var contentTypes = {
403 css: 'text/css'
404}
405
406function readReqForm(req, cb) {
407 pull(
408 toPull(req),
409 pull.collect(function (err, bufs) {
410 if (err) return cb(err)
411 var data
412 try {
413 data = qs.parse(Buffer.concat(bufs).toString('ascii'))
414 } catch(e) {
415 return cb(e)
416 }
417 cb(null, data)
418 })
419 )
420}
421
422var issueCommentScript = '(' + function () {
423 var $ = document.getElementById.bind(document)
424 $('tab-links').style.display = 'block'
425 $('preview-tab-link').onclick = function (e) {
426 with (new XMLHttpRequest()) {
427 open('POST', '', true)
428 onload = function() {
429 $('preview-tab').innerHTML = responseText
430 }
431 send('action=markdown' +
432 '&repo=' + encodeURIComponent($('repo-id').value) +
433 '&text=' + encodeURIComponent($('post-text').value))
434 }
435 }
436}.toString() + ')()'
437
438var msgTypes = {
439 'git-repo': true,
440 'git-update': true,
441 'issue': true,
442 'pull-request': true
443}
444
445var imgMimes = {
446 png: 'image/png',
447 jpeg: 'image/jpeg',
448 jpg: 'image/jpeg',
449 gif: 'image/gif',
450 tif: 'image/tiff',
451 svg: 'image/svg+xml',
452 bmp: 'image/bmp'
453}
454
455var _httpServer
456
457module.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()
466
467 var web = new GitSSBWeb(ssb, config, reconnect)
468 _httpSserver = web.httpServer
469
470 return {}
471 }
472}
473
474function GitSSBWeb(ssb, config, reconnect) {
475 this.ssb = ssb
476 this.config = config
477 this.reconnect = reconnect
478
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)
489 })
490 })
491
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))
497
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}
505
506var G = GitSSBWeb.prototype
507
508G.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}
515
516G.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}
526
527G.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}
533
534G.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}
545
546/* Serving a request */
547
548function 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}
560
561function 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}
574
575G.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
602G.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)))
625 })
626
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 })
637
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))
649 })
650
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 })
663
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 })
682
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 }
710 })
711 })
712}
713
714G.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"))
719
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}
741
742G.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}
751
752G.serve404 = function (req) {
753 return this.serveBuffer(404, req._t("error.404NotFound"))
754}
755
756G.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}
764
765G.serveMarkdown = function (text, repo) {
766 return this.serveBuffer(200, markdown(text, repo),
767 'text/html; charset=utf-8')
768}
769
770function renderError(err, tag) {
771 tag = tag || 'h3'
772 return '<' + tag + '>' + err.name + '</' + tag + '>' +
773 '<pre>' + escapeHTML(err.stack) + '</pre>'
774}
775
776function 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 })
789 }
790}
791
792G.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}
823
824G.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
833function 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))
842 })
843 })
844}
845
846function 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}
856
857/* Feed */
858
859G.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) {
898 delete query.gt
899 delete query.forwards
900 query.lt = opts.gt + 1
901 } else {
902 delete query.lt
903 query.gt = opts.lt - 1
904 query.forwards = 1
905 }
906 cb(null, '<a href="?' + qs.stringify(query) + '">' +
907 req._t(query.forwards ? 'Older' : 'Newer') + '</a>')
908 }
909 )
910 )
911}
912
913G.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) {
929 cb(null, '<section class="collapse">' + msgLink + '<br>' +
930 req._t('Forked', {
931 name: authorLink,
932 upstream: link([c.upstream], upstreamName),
933 repo: link([msg.key], repoName)
934 }) + '</section>')
935 })
936 })
937 } else {
938 return done(function (err, repoName) {
939 if (err) return cb(err)
940 var repoLink = link([msg.key], repoName)
941 cb(null, '<section class="collapse">' + msgLink + '<br>' +
942 req._t('CreatedRepo', {
943 name: authorLink,
944 repo: repoLink
945 }) + '</section>')
946 })
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.'
989 return cb(null, '<section class="collapse">' + msgLink + '<br>' +
990 req._t('CommentedOn', {
991 author: authorLink,
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))
999 }
1000}
1001
1002/* Index */
1003
1004G.serveIndex = function (req) {
1005 return this.serveTemplate(req)(this.renderFeed(req))
1006}
1007
1008G.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)
1016 }
1017}
1018
1019G.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 ])))
1036 })
1037 })
1038}
1039
1040G.serveUserActivity = function (req, feedId) {
1041 return this.renderUserPage(req, feedId, 'activity', null,
1042 this.renderFeed(req, feedId))
1043}
1044
1045G.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
1053 }),
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}
1091
1092 /* Message */
1093
1094G.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))
1118 })
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))
1127 })
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))
1142 })
1143 })
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))
1161 })
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 })
1184 }
1185 })
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 }
1199 })
1200 })
1201}
1202
1203G.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}
1209
1210 /* Repo */
1211
1212G.serveRepoPage = function (req, repo, path) {
1213 var self = this
1214 var defaultBranch = 'master'
1215 var query = req._u.query
1216
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 }
1227
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))
1238 })
1239 })
1240 })
1241}
1242
1243function 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)
1287 }
1288}
1289
1290G.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}
1298
1299G.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']
1306
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())
1311
1312 if (repo.upstream) {
1313 self.getRepoName(repo.upstream.feed, repo.upstream.id, done())
1314 self.about.getName(repo.upstream.feed, done())
1315 }
1316
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 ])))
1367 })
1368 })
1369}
1370
1371G.serveEmptyRepo = function (req, repo) {
1372 if (repo.feed != this.myId)
1373 return this.renderRepoPage(req, repo, 'code', null, null, pull.once(
1374 '<section>' +
1375 '<h3>' + req._t('EmptyRepo') + '</h3>' +
1376 '</section>'))
1377
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}
1394
1395G.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}
1413
1414/* Search */
1415
1416G.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))
1423
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
1448G.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
1465G.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
1477 }),
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)
1491 })
1492 })
1493 ]))
1494}
1495
1496function renderRepoUpdate(req, repo, msg, full) {
1497 var c = msg.value.content
1498
1499 if (c.type != 'git-update') {
1500 return ''
1501 // return renderFeedItem(msg, cb)
1502 // TODO: render post, issue, pull-request
1503 }
1504
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
1513
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}
1533
1534/* Repo commits */
1535
1536G.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>' : '')
1554 }
1555 )
1556 )
1557 ]))
1558}
1559
1560function 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}
1575
1576/* Branch menu */
1577
1578function formatRevOptions(currentName) {
1579 return function (name) {
1580 var htmlName = escapeHTML(name)
1581 return '<option value="' + htmlName + '"' +
1582 (name == currentName ? ' selected="selected"' : '') +
1583 '>' + htmlName + '</option>'
1584 }
1585}
1586
1587function formatRevType(req, type) {
1588 return (
1589 type == 'heads' ? req._t('Branches') :
1590 type == 'tags' ? req._t('Tags') :
1591 type)
1592}
1593
1594function 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>')
1606 })
1607 })
1608}
1609
1610function 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}
1626
1627/* Repo tree */
1628
1629function 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 }) : ''))
1646 })
1647 })
1648}
1649
1650// breadcrumbs
1651function 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}
1658
1659function 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}
1682
1683/* Repo readme */
1684
1685function 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 ]))
1707 })
1708 })
1709 )
1710 })
1711}
1712
1713/* Repo commit */
1714
1715G.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 ])))
1752 })
1753 })
1754}
1755
1756/* Repo tag */
1757
1758G.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(
1770 '<section class="collapse">' +
1771 '<h3>' + link([repo.id, 'tag', rev], tag.tag) + '</h3>' +
1772 req._t('TaggedOn', {
1773 name: escapeHTML(tag.tagger.name),
1774 date: tag.tagger.date.toLocaleString(req._locale)
1775 }) + '<br/>' +
1776 link([repo.id, tag.type, tag.object]) +
1777 linkify(pre(body)) +
1778 '</section>')))
1779 })
1780 })
1781}
1782
1783
1784/* Diff stat */
1785
1786function 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}
1852
1853function 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}
1886
1887/* An unknown message linking to a repo */
1888
1889G.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}
1894
1895/* Repo update */
1896
1897function 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}
1905
1906G.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}'
1910
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>'))
1918
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]
1925 }
1926 }
1927 }
1928
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 ])
1961
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}
1974
1975/* Blob */
1976
1977G.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 ])))
2011 })
2012 })
2013}
2014
2015G.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}
2024
2025/* Raw blob */
2026
2027G.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)))
2035 })
2036 })
2037}
2038
2039G.serveRaw = function (length, contentType) {
2040 var headers = {
2041 'Content-Type': contentType || 'text/plain; charset=utf-8',
2042 'Cache-Control': 'max-age=31536000'
2043 }
2044 if (length != null)
2045 headers['Content-Length'] = length
2046 return function (read) {
2047 return cat([pull.once([200, headers]), read])
2048 }
2049}
2050
2051G.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
2060G.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))
2067 })
2068 })
2069}
2070
2071/* Digs */
2072
2073G.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 ])))
2093 })
2094 })
2095}
2096
2097/* Forks */
2098
2099G.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})
2106 })
2107 }),
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
2126 })
2127 })
2128 }, 8)
2129 )
2130}
2131
2132G.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>'
2147 })
2148 ),
2149 readOnce(function (cb) {
2150 cb(null, hasForks ? '' : req._t('NoForks'))
2151 })
2152 ]))
2153}
2154
2155G.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}
2169
2170 /* Issues */
2171
2172G.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>'
2215 })
2216 ),
2217 readOnce(function (cb) {
2218 cb(null, count > 0 ? '' :
2219 '<p>' + req._t(isPRs ? 'NoPullRequests' : 'NoIssues') + '</p>')
2220 })
2221 ]))
2222}
2223
2224/* New Issue */
2225
2226G.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}
2238
2239/* Issue */
2240
2241G.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
2272 }),
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}
2289
2290G.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)
2313 return '<section class="collapse mention-preview">' +
2314 req._t('issue.MentionedIn', {
2315 name: authorLink,
2316 type: type,
2317 post: '<a href="/' + msg.key + '#' + msg.key + '">' +
2318 String(text).substr(0, 140) + '</a>'
2319 }) + '</section>'
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)
2344 return '<section class="collapse">' +
2345 req._t(mention.open ? 'issue.Reopened' : 'issue.Closed', {
2346 name: authorLink,
2347 type: type
2348 }) + ' &middot; ' + msgTimeLink + '<br/>' +
2349 commitLink +
2350 '</section>'
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)
2355 return '<section class="collapse">' +
2356 req._t('issue.Mentioned', {
2357 name: authorLink,
2358 type: type
2359 }) + ' &middot; ' + msgTimeLink + '<br/>' +
2360 commitLink +
2361 '</section>'
2362 } else {
2363 // fallthrough
2364 }
2365
2366 default:
2367 return '<section class="collapse">' +
2368 authorLink +
2369 ' &middot; ' + msgTimeLink +
2370 json(c) +
2371 '</section>'
2372 }
2373}
2374
2375 function renderIssueCommentForm(req, issue, repo, branch, isAuthor, type) {
2376 return '<section><form action="" method="post">' +
2377 '<input type="hidden" name="action" value="comment">' +
2378 '<input type="hidden" name="id" value="' + issue.id + '">' +
2379 '<input type="hidden" name="issue" value="' + issue.id + '">' +
2380 '<input type="hidden" name="repo" value="' + repo.id + '">' +
2381 '<input type="hidden" name="branch" value="' + branch + '">' +
2382 renderPostForm(req, repo) +
2383 '<input type="submit" class="btn open" value="' +
2384 req._t('issue.Comment') + '" />' +
2385 (isAuthor ?
2386 '<input type="submit" class="btn"' +
2387 ' name="' + (issue.open ? 'close' : 'open') + '"' +
2388 ' value="' + req._t(
2389 issue.open ? 'issue.Close' : 'issue.Reopen', {type: type}
2390 ) + '"/>' : '') +
2391 '</form></section>'
2392 }
2393
2394 /* Pull Request */
2395
2396G.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 })
2418
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>')
2444 })
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
2463G.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>')
2471 ]))
2472 })
2473}
2474
2475G.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}
2486
2487G.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
2506 }),
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 ))
2524 })
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)
2538 })
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}
2566
2567G.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)
2573 return '<section class="collapse">' +
2574 req._t('NameDeletedBranch', {
2575 name: authorLink,
2576 branch: '<code>' + pr.headBranch + '</code>'
2577 }) + ' &middot; ' + msgLink +
2578 '</section>'
2579
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}
2588
2589/* Compare changes */
2590
2591G.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) {
2616 if (err) return cb(err)
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
2644 }
2645 }),
2646 pull.filter(function (ref) {
2647 return ref.type == 'heads'
2648 && !(ref.name == base && fork.msg.key == repo.id)
2649 }),
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 )
2663 }
2664}
2665
2666G.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(':')
2671
2672 if (!s || !baseBranch)
2673 return self.serveRedirect(req, encodeLink([repo.id, 'compare']))
2674
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}'
2682
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 )
2694 })
2695 })
2696 ]))
2697
2698 function renderRepoInfo(headRepo, headRepoName, headRepoAuthorName) {
2699 var authorLink = link([headRepo.feed], headRepoAuthorName)
2700 var repoLink = link([headRepoId], headRepoName)
2701 return cat([
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>')
2742 ])
2743 }
2744}
2745
2746G.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}
2779

Built with git-ssb-web