git ssb

30+

cel / git-ssb-web



Tree: bcc4c3555e43a5f9cb02b98de559a806dce9b933

Files: bcc4c3555e43a5f9cb02b98de559a806dce9b933 / index.js

91184 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 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
395function 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
402function 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
413function getMention(msg, id) {
414 if (msg.key == id) return msg
415 var mentions = msg.value.content.mentions
416 if (mentions) for (var i = 0; i < mentions.length; i++) {
417 var mention = mentions[i]
418 if (mention.link == id)
419 return mention
420 }
421 return null
422}
423
424var hasOwnProp = Object.prototype.hasOwnProperty
425
426function getContentType(filename) {
427 var ext = getExtension(filename)
428 return contentTypes[ext] || imgMimes[ext] || 'text/plain; charset=utf-8'
429}
430
431var contentTypes = {
432 css: 'text/css'
433}
434
435function readReqForm(req, cb) {
436 pull(
437 toPull(req),
438 pull.collect(function (err, bufs) {
439 if (err) return cb(err)
440 var data
441 try {
442 data = qs.parse(Buffer.concat(bufs).toString('ascii'))
443 } catch(e) {
444 return cb(e)
445 }
446 cb(null, data)
447 })
448 )
449}
450
451var issueCommentScript = '(' + function () {
452 var $ = document.getElementById.bind(document)
453 $('tab-links').style.display = 'block'
454 $('preview-tab-link').onclick = function (e) {
455 with (new XMLHttpRequest()) {
456 open('POST', '', true)
457 onload = function() {
458 $('preview-tab').innerHTML = responseText
459 }
460 send('action=markdown' +
461 '&repo=' + encodeURIComponent($('repo-id').value) +
462 '&text=' + encodeURIComponent($('post-text').value))
463 }
464 }
465}.toString() + ')()'
466
467var msgTypes = {
468 'git-repo': true,
469 'git-update': true,
470 'issue': true,
471 'pull-request': true
472}
473
474var imgMimes = {
475 png: 'image/png',
476 jpeg: 'image/jpeg',
477 jpg: 'image/jpeg',
478 gif: 'image/gif',
479 tif: 'image/tiff',
480 svg: 'image/svg+xml',
481 bmp: 'image/bmp'
482}
483
484module.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'
490
491 var addr = parseAddr(opts.listenAddr, {host: 'localhost', port: 7718})
492 http.createServer(onRequest).listen(addr.port, addr.host, onListening)
493
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 }
517
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)
522 }
523
524 /* Serving a request */
525
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))
538 })
539 }
540
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 }
553
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]
559
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))
568
569 switch (data.action) {
570 case 'fork-prompt':
571 return cb(null, serveRedirect(req,
572 encodeLink([data.id, 'fork'])))
573
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 })
583
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 })
594
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 })
607
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 })
620
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 })
639
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 })
649
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)))
659 })
660
661 case 'markdown':
662 return cb(null, serveMarkdown(data.text, {id: data.repo}))
663
664 default:
665 cb(null, serveBuffer(400, req._t('error.UnknownAction', data)))
666 }
667 })
668 })
669 }
670
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 }
688
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"))
694
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 })
714 })
715 }
716
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 }
726
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 }
736
737 function serve404(req) {
738 return serveBuffer(404, req._t("error.404NotFound"))
739 }
740
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 }
749
750 function serveMarkdown(text, repo) {
751 return serveBuffer(200, markdown(text, repo), 'text/html; charset=utf-8')
752 }
753
754 function renderError(err, tag) {
755 tag = tag || 'h3'
756 return '<' + tag + '>' + err.name + '</' + tag + '>' +
757 '<pre>' + escapeHTML(err.stack) + '</pre>'
758 }
759
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 }
775
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 ])
804 }
805
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 }
814
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 })
825 })
826 }
827
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 }
838
839 /* Feed */
840
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
873 delete query.gt
874 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>')
890 }
891 )
892 )
893 }
894
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)
922 cb(null, '<section class="collapse">' + msgLink + '<br>' +
923 req._t('CreatedRepo', {
924 name: authorLink,
925 repo: repoLink
926 }) + '</section>')
927 })
928 }
929 case 'git-update':
930 return getRepoName(about, author, c.repo, function (err, repoName) {
931 if (err) return cb(err)
932 var repoLink = link([c.repo], repoName)
933 cb(null, '<section class="collapse">' + msgLink + '<br>' +
934 req._t('Pushed', {
935 name: authorLink,
936 repo: repoLink
937 }) + '</section>')
938 })
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':
959 return cb(null, '<section class="collapse">' + msgLink + '<br>' +
960 req._t('Named', {
961 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 }
981 }
982
983 /* Index */
984
985 function serveIndex(req) {
986 return serveTemplate(req)(renderFeed(req))
987 }
988
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 }
998 }
999
1000 function renderUserPage(req, feedId, page, body) {
1001 return serveTemplate(req, feedId)(cat([
1002 readOnce(function (cb) {
1003 about.getName(feedId, function (err, name) {
1004 cb(null, '<h2>' + link([feedId], name) +
1005 '<code class="user-id">' + feedId + '</code></h2>' +
1006 nav([
1007 [[feedId], req._t('Activity'), 'activity'],
1008 [[feedId, 'repos'], req._t('Repos'), 'repos']
1009 ], page))
1010 })
1011 }),
1012 body,
1013 ]))
1014 }
1015
1016 function serveUserActivity(req, feedId) {
1017 return renderUserPage(req, feedId, 'activity', renderFeed(req, feedId))
1018 }
1019
1020 function serveUserRepos(req, feedId) {
1021 return renderUserPage(req, feedId, 'repos', pull(
1022 cat([
1023 ssb.messagesByType({
1024 type: 'git-update',
1025 reverse: true
1026 }),
1027 ssb.messagesByType({
1028 type: 'git-repo',
1029 reverse: true
1030 })
1031 ]),
1032 pull.filter(function (msg) {
1033 return msg.value.author == feedId
1034 }),
1035 pull.unique(function (msg) {
1036 return msg.value.content.repo || msg.key
1037 }),
1038 pull.take(20),
1039 paramap(function (msg, cb) {
1040 var repoId = msg.value.content.repo || msg.key
1041 var done = multicb({ pluck: 1, spread: true })
1042 getRepoName(about, feedId, repoId, done())
1043 getVotes(repoId, done())
1044 done(function (err, repoName, votes) {
1045 if (err) return cb(err)
1046 cb(null, '<section class="collapse">' +
1047 '<span class="right-bar">' +
1048 '<i>โœŒ</i> ' +
1049 link([repoId, 'digs'], votes.upvotes, true,
1050 ' title="' + req._t('Digs') + '"') +
1051 '</span>' +
1052 '<strong>' + link([repoId], repoName) + '</strong>' +
1053 '<div class="date-info">' +
1054 req._t(msg.value.content.type == 'git-update' ?
1055 'UpdatedOnDate' : 'CreatedOnDate',
1056 {
1057 date: timestamp(msg.value.timestamp, req)
1058 }) + '</div>' +
1059 '</section>')
1060 })
1061 }, 8)
1062 ))
1063 }
1064
1065 /* Message */
1066
1067 function serveMessage(req, id, path) {
1068 return readNext(function (cb) {
1069 ssb.get(id, function (err, msg) {
1070 if (err) return cb(null, serveError(req, err))
1071 var c = msg.content || {}
1072 switch (c.type) {
1073 case 'git-repo':
1074 return getRepo(id, function (err, repo) {
1075 if (err) return cb(null, serveError(req, err))
1076 cb(null, serveRepoPage(req, Repo(repo), path))
1077 })
1078 case 'git-update':
1079 return getRepo(c.repo, function (err, repo) {
1080 if (err) return cb(null, serveRepoNotFound(req, c.repo, err))
1081 cb(null, serveRepoUpdate(req, Repo(repo), id, msg, path))
1082 })
1083 case 'issue':
1084 return getRepo(c.project, function (err, repo) {
1085 if (err) return cb(null, serveRepoNotFound(req, c.project, err))
1086 issues.get(id, function (err, issue) {
1087 if (err) return cb(null, serveError(req, err))
1088 cb(null, serveRepoIssue(req, Repo(repo), issue, path))
1089 })
1090 })
1091 case 'pull-request':
1092 return getRepo(c.repo, function (err, repo) {
1093 if (err) return cb(null, serveRepoNotFound(req, c.project, err))
1094 pullReqs.get(id, function (err, pr) {
1095 if (err) return cb(null, serveError(req, err))
1096 cb(null, serveRepoPullReq(req, Repo(repo), pr, path))
1097 })
1098 })
1099 case 'issue-edit':
1100 if (ref.isMsgId(c.issue)) {
1101 return pullReqs.get(c.issue, function (err, issue) {
1102 if (err) return cb(err)
1103 var serve = issue.msg.value.content.type == 'pull-request' ?
1104 serveRepoPullReq : serveRepoIssue
1105 getRepo(issue.project, function (err, repo) {
1106 if (err) {
1107 if (!repo) return cb(null,
1108 serveRepoNotFound(req, c.repo, err))
1109 return cb(null, serveError(req, err))
1110 }
1111 cb(null, serve(req, Repo(repo), issue, path, id))
1112 })
1113 })
1114 }
1115 // fallthrough
1116 case 'post':
1117 if (ref.isMsgId(c.issue) && ref.isMsgId(c.repo)) {
1118 // comment on an issue
1119 var done = multicb({ pluck: 1, spread: true })
1120 getRepo(c.repo, done())
1121 pullReqs.get(c.issue, done())
1122 return done(function (err, repo, issue) {
1123 if (err) {
1124 if (!repo) return cb(null,
1125 serveRepoNotFound(req, c.repo, err))
1126 return cb(null, serveError(req, err))
1127 }
1128 var serve = issue.msg.value.content.type == 'pull-request' ?
1129 serveRepoPullReq : serveRepoIssue
1130 cb(null, serve(req, Repo(repo), issue, path, id))
1131 })
1132 } else if (ref.isMsgId(c.root)) {
1133 // comment on issue from patchwork?
1134 return getMsg(c.root, function (err, root) {
1135 if (err) return cb(null, serveError(req, err))
1136 var repoId = root.content.repo || root.content.project
1137 if (!ref.isMsgId(repoId))
1138 return cb(null, serveGenericMessage(req, id, msg, path))
1139 getRepo(repoId, function (err, repo) {
1140 if (err) return cb(null, serveError(req, err))
1141 switch (root.content && root.content.type) {
1142 case 'issue':
1143 return issues.get(c.root, function (err, issue) {
1144 if (err) return cb(null, serveError(req, err))
1145 return cb(null,
1146 serveRepoIssue(req, Repo(repo), issue, path, id))
1147 })
1148 case 'pull-request':
1149 pullReqs.get(c.root, function (err, pr) {
1150 if (err) return cb(null, serveError(req, err))
1151 return cb(null,
1152 serveRepoPullReq(req, Repo(repo), pr, path, id))
1153 })
1154 }
1155 })
1156 })
1157 }
1158 // fallthrough
1159 default:
1160 if (ref.isMsgId(c.repo))
1161 return getRepo(c.repo, function (err, repo) {
1162 if (err) return cb(null, serveRepoNotFound(req, c.repo, err))
1163 cb(null, serveRepoSomething(req, Repo(repo), id, msg, path))
1164 })
1165 else
1166 return cb(null, serveGenericMessage(req, id, msg, path))
1167 }
1168 })
1169 })
1170 }
1171
1172 function serveGenericMessage(req, id, msg, path) {
1173 return serveTemplate(req, id)(pull.once(
1174 '<section><h2>' + link([id]) + '</h2>' +
1175 json(msg) +
1176 '</section>'))
1177 }
1178
1179 /* Repo */
1180
1181 function serveRepoPage(req, repo, path) {
1182 var defaultBranch = 'master'
1183 var query = req._u.query
1184
1185 if (query.rev != null) {
1186 // Allow navigating revs using GET query param.
1187 // Replace the branch in the path with the rev query value
1188 path[0] = path[0] || 'tree'
1189 path[1] = query.rev
1190 req._u.pathname = encodeLink([repo.id].concat(path))
1191 delete req._u.query.rev
1192 delete req._u.search
1193 return serveRedirect(req, url.format(req._u))
1194 }
1195
1196 // get branch
1197 return path[1] ?
1198 serveRepoPage2(req, repo, path) :
1199 readNext(function (cb) {
1200 // TODO: handle this in pull-git-repo or ssb-git-repo
1201 repo.getSymRef('HEAD', true, function (err, ref) {
1202 if (err) return cb(err)
1203 repo.resolveRef(ref, function (err, rev) {
1204 path[1] = rev ? ref : null
1205 cb(null, serveRepoPage2(req, repo, path))
1206 })
1207 })
1208 })
1209 }
1210
1211 function serveRepoPage2(req, repo, path) {
1212 var branch = path[1]
1213 var filePath = path.slice(2)
1214 switch (path[0]) {
1215 case undefined:
1216 case '':
1217 return serveRepoTree(req, repo, branch, [])
1218 case 'activity':
1219 return serveRepoActivity(req, repo, branch)
1220 case 'commits':
1221 return serveRepoCommits(req, repo, branch)
1222 case 'commit':
1223 return serveRepoCommit(req, repo, path[1])
1224 case 'tree':
1225 return serveRepoTree(req, repo, branch, filePath)
1226 case 'blob':
1227 return serveRepoBlob(req, repo, branch, filePath)
1228 case 'raw':
1229 return serveRepoRaw(req, repo, branch, filePath)
1230 case 'digs':
1231 return serveRepoDigs(req, repo)
1232 case 'fork':
1233 return serveRepoForkPrompt(req, repo)
1234 case 'forks':
1235 return serveRepoForks(req, repo)
1236 case 'issues':
1237 switch (path[1]) {
1238 case 'new':
1239 if (filePath.length == 0)
1240 return serveRepoNewIssue(req, repo)
1241 break
1242 default:
1243 return serveRepoIssues(req, repo, false)
1244 }
1245 case 'pulls':
1246 return serveRepoIssues(req, repo, true)
1247 case 'compare':
1248 return serveRepoCompare(req, repo)
1249 case 'comparing':
1250 return serveRepoComparing(req, repo)
1251 default:
1252 return serve404(req)
1253 }
1254 }
1255
1256 function serveRepoNotFound(req, id, err) {
1257 return serveTemplate(req, req._t('error.RepoNotFound'), 404)(pull.values([
1258 '<h2>' + req._t('error.RepoNotFound') + '</h2>',
1259 '<p>' + req._t('error.RepoNameNotFound') + '</p>',
1260 '<pre>' + escapeHTML(err.stack) + '</pre>'
1261 ]))
1262 }
1263
1264 function renderRepoPage(req, repo, page, branch, body) {
1265 var gitUrl = 'ssb://' + repo.id
1266 var gitLink = '<input class="clone-url" readonly="readonly" ' +
1267 'value="' + gitUrl + '" size="45" ' +
1268 'onclick="this.select()"/>'
1269 var digsPath = [repo.id, 'digs']
1270
1271 var done = multicb({ pluck: 1, spread: true })
1272 getRepoName(about, repo.feed, repo.id, done())
1273 about.getName(repo.feed, done())
1274 getVotes(repo.id, done())
1275
1276 if (repo.upstream) {
1277 getRepoName(about, repo.upstream.feed, repo.upstream.id, done())
1278 about.getName(repo.upstream.feed, done())
1279 }
1280
1281 return readNext(function (cb) {
1282 done(function (err, repoName, authorName, votes,
1283 upstreamName, upstreamAuthorName) {
1284 if (err) return cb(null, serveError(req, err))
1285 var upvoted = votes.upvoters[myId] > 0
1286 var upstreamLink = !repo.upstream ? '' :
1287 link([repo.upstream])
1288 cb(null, serveTemplate(req, repo.id)(cat([
1289 pull.once(
1290 '<div class="repo-title">' +
1291 '<form class="right-bar" action="" method="post">' +
1292 '<button class="btn" name="action" value="vote" ' +
1293 (isPublic ? 'disabled="disabled"' : ' type="submit"') + '>' +
1294 '<i>โœŒ</i> ' + req._t(!isPublic && upvoted ? 'Undig' : 'Dig') +
1295 '</button>' +
1296 (isPublic ? '' : '<input type="hidden" name="value" value="' +
1297 (upvoted ? '0' : '1') + '">' +
1298 '<input type="hidden" name="id" value="' +
1299 escapeHTML(repo.id) + '">') + ' ' +
1300 '<strong>' + link(digsPath, votes.upvotes) + '</strong> ' +
1301 (isPublic ? '' : '<button class="btn" type="submit" ' +
1302 ' name="action" value="fork-prompt">' +
1303 '<i>โ‘‚</i> ' + req._t('Fork') +
1304 '</button>') + ' ' +
1305 link([repo.id, 'forks'], '+', false, ' title="' +
1306 req._t('Forks') + '"') +
1307 '</form>' +
1308 renderNameForm(req, !isPublic, repo.id, repoName, 'repo-name',
1309 null, req._t('repo.Rename'),
1310 '<h2 class="bgslash">' + link([repo.feed], authorName) + ' / ' +
1311 link([repo.id], repoName) + '</h2>') +
1312 '</div>' +
1313 (repo.upstream ? '<small class="bgslash">' + req._t('ForkedFrom', {
1314 repo: link([repo.upstream.feed], upstreamAuthorName) + '/' +
1315 link([repo.upstream.id], upstreamName)
1316 }) + '</small>' : '') +
1317 nav([
1318 [[repo.id], req._t('Code'), 'code'],
1319 [[repo.id, 'activity'], req._t('Activity'), 'activity'],
1320 [[repo.id, 'commits', branch||''], req._t('Commits'), 'commits'],
1321 [[repo.id, 'issues'], req._t('Issues'), 'issues'],
1322 [[repo.id, 'pulls'], req._t('PullRequests'), 'pulls']
1323 ], page, gitLink)),
1324 body
1325 ])))
1326 })
1327 })
1328 }
1329
1330 function serveEmptyRepo(req, repo) {
1331 if (repo.feed != myId)
1332 return renderRepoPage(req, repo, 'code', null, pull.once(
1333 '<section>' +
1334 '<h3>' + req._t('EmptyRepo') + '</h3>' +
1335 '</section>'))
1336
1337 var gitUrl = 'ssb://' + repo.id
1338 return renderRepoPage(req, repo, 'code', null, pull.once(
1339 '<section>' +
1340 '<h3>' + req._t('initRepo.GettingStarted') + '</h3>' +
1341 '<h4>' + req._t('initRepo.CreateNew') + '</h4><pre>' +
1342 'touch ' + req._t('initRepo.README') + '.md\n' +
1343 'git init\n' +
1344 'git add ' + req._t('initRepo.README') + '.md\n' +
1345 'git commit -m "' + req._t('initRepo.InitialCommit') + '"\n' +
1346 'git remote add origin ' + gitUrl + '\n' +
1347 'git push -u origin master</pre>\n' +
1348 '<h4>' + req._t('initRepo.PushExisting') + '</h4>\n' +
1349 '<pre>git remote add origin ' + gitUrl + '\n' +
1350 'git push -u origin master</pre>' +
1351 '</section>'))
1352 }
1353
1354 function serveRepoTree(req, repo, rev, path) {
1355 if (!rev) return serveEmptyRepo(req, repo)
1356 var type = repo.isCommitHash(rev) ? 'Tree' : 'Branch'
1357 return renderRepoPage(req, repo, 'code', rev, cat([
1358 pull.once('<section><form action="" method="get">' +
1359 '<h3>' + req._t(type) + ': ' + rev + ' '),
1360 revMenu(req, repo, rev),
1361 pull.once('</h3></form>'),
1362 type == 'Branch' && renderRepoLatest(req, repo, rev),
1363 pull.once('</section><section>'),
1364 renderRepoTree(req, repo, rev, path),
1365 pull.once('</section>'),
1366 renderRepoReadme(req, repo, rev, path)
1367 ]))
1368 }
1369
1370 /* Search */
1371
1372 function serveSearch(req) {
1373 var q = String(req._u.query.q || '')
1374 if (!q) return serveIndex(req)
1375 var qId = q.replace(/^ssb:\/*/, '')
1376 if (ref.type(qId))
1377 return serveRedirect(req, encodeURIComponent(qId))
1378
1379 var search = new RegExp(q, 'i')
1380 return serveTemplate(req, req._t('Search') + ' &middot; ' + q, 200)(
1381 renderFeed(req, null, function (opts) {
1382 opts.type == 'about'
1383 return function (read) {
1384 return pull(
1385 many([
1386 getRepoNames(opts),
1387 read
1388 ]),
1389 pull.filter(function (msg) {
1390 var c = msg.value.content
1391 return (
1392 search.test(msg.key) ||
1393 c.text && search.test(c.text) ||
1394 c.name && search.test(c.name) ||
1395 c.title && search.test(c.title))
1396 })
1397 )
1398 }
1399 })
1400 )
1401 }
1402
1403 function getRepoNames(opts) {
1404 return pull(
1405 ssb.messagesByType({
1406 type: 'about',
1407 reverse: opts.reverse,
1408 lt: opts.lt,
1409 gt: opts.gt,
1410 }),
1411 pull.filter(function (msg) {
1412 return '%' == String(msg.value.content.about)[0]
1413 && msg.value.content.name
1414 })
1415 )
1416 }
1417
1418 /* Repo activity */
1419
1420 function serveRepoActivity(req, repo, branch) {
1421 return renderRepoPage(req, repo, 'activity', branch, cat([
1422 pull.once('<h3>' + req._t('Activity') + '</h3>'),
1423 pull(
1424 ssb.links({
1425 dest: repo.id,
1426 source: repo.feed,
1427 rel: 'repo',
1428 values: true,
1429 reverse: true
1430 }),
1431 pull.map(renderRepoUpdate.bind(this, req, repo))
1432 ),
1433 readOnce(function (cb) {
1434 var done = multicb({ pluck: 1, spread: true })
1435 about.getName(repo.feed, done())
1436 getMsg(repo.id, done())
1437 done(function (err, authorName, msg) {
1438 if (err) return cb(err)
1439 renderFeedItem(req, {
1440 key: repo.id,
1441 value: msg,
1442 authorName: authorName
1443 }, cb)
1444 })
1445 })
1446 ]))
1447 }
1448
1449 function renderRepoUpdate(req, repo, msg, full) {
1450 var c = msg.value.content
1451
1452 if (c.type != 'git-update') {
1453 return ''
1454 // return renderFeedItem(msg, cb)
1455 // TODO: render post, issue, pull-request
1456 }
1457
1458 var refs = c.refs ? Object.keys(c.refs).map(function (ref) {
1459 return {name: ref, value: c.refs[ref]}
1460 }) : []
1461 var numObjects = c.objects ? Object.keys(c.objects).length : 0
1462
1463 var dateStr = new Date(msg.value.timestamp).toLocaleString(req._locale)
1464 return '<section class="collapse">' +
1465 link([msg.key], dateStr) + '<br>' +
1466 (numObjects ? req._t('PushedObjects', numObjects) + '<br/>': '') +
1467 refs.map(function (update) {
1468 var name = escapeHTML(update.name)
1469 if (!update.value) {
1470 return req._t('DeletedBranch', {branch: name})
1471 } else {
1472 var commitLink = link([repo.id, 'commit', update.value])
1473 return name + ' &rarr; <tt>' + commitLink + '</tt>'
1474 }
1475 }).join('<br>') +
1476 '</section>'
1477 }
1478
1479 /* Repo commits */
1480
1481 function serveRepoCommits(req, repo, branch) {
1482 var query = req._u.query
1483 return renderRepoPage(req, repo, 'commits', branch, cat([
1484 pull.once('<h3>' + req._t('Commits') + '</h3>'),
1485 pull(
1486 repo.readLog(query.start || branch),
1487 pull.take(20),
1488 paramap(repo.getCommitParsed.bind(repo), 8),
1489 paginate(
1490 !query.start ? '' : function (first, cb) {
1491 cb(null, '&hellip;')
1492 },
1493 pull.map(renderCommit.bind(this, req, repo)),
1494 function (commit, cb) {
1495 cb(null, commit.parents && commit.parents[0] ?
1496 '<a href="?start=' + commit.id + '">' +
1497 req._t('Older') + '</a>' : '')
1498 }
1499 )
1500 )
1501 ]))
1502 }
1503
1504 function renderCommit(req, repo, commit) {
1505 var commitPath = [repo.id, 'commit', commit.id]
1506 var treePath = [repo.id, 'tree', commit.id]
1507 return '<section class="collapse">' +
1508 '<strong>' + link(commitPath, commit.title) + '</strong><br>' +
1509 '<tt>' + commit.id + '</tt> ' +
1510 link(treePath, req._t('Tree')) + '<br>' +
1511 escapeHTML(commit.author.name) + ' &middot; ' +
1512 commit.author.date.toLocaleString(req._locale) +
1513 (commit.separateAuthor ? '<br>' + req._t('CommittedOn', {
1514 name: escapeHTML(commit.committer.name),
1515 date: commit.committer.date.toLocaleString(req._locale)
1516 }) : '') +
1517 '</section>'
1518 }
1519
1520 /* Branch menu */
1521
1522 function formatRevOptions(currentName) {
1523 return function (name) {
1524 var htmlName = escapeHTML(name)
1525 return '<option value="' + htmlName + '"' +
1526 (name == currentName ? ' selected="selected"' : '') +
1527 '>' + htmlName + '</option>'
1528 }
1529 }
1530
1531 function formatRevType(req, type) {
1532 return (
1533 type == 'heads' ? req._t('Branches') :
1534 type == 'tags' ? req._t('Tags') :
1535 type)
1536 }
1537
1538 function revMenu(req, repo, currentName) {
1539 return readOnce(function (cb) {
1540 repo.getRefNames(function (err, refs) {
1541 if (err) return cb(err)
1542 cb(null, '<select name="rev" onchange="this.form.submit()">' +
1543 Object.keys(refs).map(function (group) {
1544 return '<optgroup label="' + formatRevType(req, group) + '">' +
1545 refs[group].map(formatRevOptions(currentName)).join('') +
1546 '</optgroup>'
1547 }).join('') +
1548 '</select><noscript> ' +
1549 '<input type="submit" value="' + req._t('Go') + '"/></noscript>')
1550 })
1551 })
1552 }
1553
1554 function branchMenu(repo, name, currentName) {
1555 return cat([
1556 pull.once('<select name="' + name + '">'),
1557 pull(
1558 repo.refs(),
1559 pull.map(function (ref) {
1560 var m = ref.name.match(/^refs\/([^\/]*)\/(.*)$/) || [,, ref.name]
1561 return m[1] == 'heads' && m[2]
1562 }),
1563 pull.filter(Boolean),
1564 pullSort(),
1565 pull.map(formatRevOptions(currentName))
1566 ),
1567 pull.once('</select>')
1568 ])
1569 }
1570
1571 /* Repo tree */
1572
1573 function renderRepoLatest(req, repo, rev) {
1574 return readOnce(function (cb) {
1575 repo.getCommitParsed(rev, function (err, commit) {
1576 if (err) return cb(err)
1577 var commitPath = [repo.id, 'commit', commit.id]
1578 cb(null,
1579 req._t('Latest') + ': ' +
1580 '<strong>' + link(commitPath, commit.title) + '</strong><br/>' +
1581 '<tt>' + commit.id + '</tt><br/> ' +
1582 req._t('CommittedOn', {
1583 name: escapeHTML(commit.committer.name),
1584 date: commit.committer.date.toLocaleString(req._locale)
1585 }) +
1586 (commit.separateAuthor ? '<br/>' + req._t('AuthoredOn', {
1587 name: escapeHTML(commit.author.name),
1588 date: commit.author.date.toLocaleString(req._locale)
1589 }) : ''))
1590 })
1591 })
1592 }
1593
1594 // breadcrumbs
1595 function linkPath(basePath, path) {
1596 path = path.slice()
1597 var last = path.pop()
1598 return path.map(function (dir, i) {
1599 return link(basePath.concat(path.slice(0, i+1)), dir)
1600 }).concat(last).join(' / ')
1601 }
1602
1603 function renderRepoTree(req, repo, rev, path) {
1604 var pathLinks = path.length === 0 ? '' :
1605 ': ' + linkPath([repo.id, 'tree'], [rev].concat(path))
1606 return cat([
1607 pull.once('<h3>' + req._t('Files') + pathLinks + '</h3>'),
1608 pull(
1609 repo.readDir(rev, path),
1610 pull.map(function (file) {
1611 var type = (file.mode === 040000) ? 'tree' :
1612 (file.mode === 0160000) ? 'commit' : 'blob'
1613 if (type == 'commit')
1614 return [
1615 '<span title="' + req._t('gitCommitLink') + '">๐Ÿ–ˆ</span>',
1616 '<span title="' + escapeHTML(file.id) + '">' +
1617 escapeHTML(file.name) + '</span>']
1618 var filePath = [repo.id, type, rev].concat(path, file.name)
1619 return ['<i>' + (type == 'tree' ? '๐Ÿ“' : '๐Ÿ“„') + '</i>',
1620 link(filePath, file.name)]
1621 }),
1622 table('class="files"')
1623 )
1624 ])
1625 }
1626
1627 /* Repo readme */
1628
1629 function renderRepoReadme(req, repo, branch, path) {
1630 return readNext(function (cb) {
1631 pull(
1632 repo.readDir(branch, path),
1633 pull.filter(function (file) {
1634 return /readme(\.|$)/i.test(file.name)
1635 }),
1636 pull.take(1),
1637 pull.collect(function (err, files) {
1638 if (err) return cb(null, pull.empty())
1639 var file = files[0]
1640 if (!file)
1641 return cb(null, pull.once(path.length ? '' :
1642 '<p>' + req._t('NoReadme') + '</p>'))
1643 repo.getObjectFromAny(file.id, function (err, obj) {
1644 if (err) return cb(err)
1645 cb(null, cat([
1646 pull.once('<section><h4><a name="readme">' +
1647 escapeHTML(file.name) + '</a></h4><hr/>'),
1648 renderObjectData(obj, file.name, repo, branch, path),
1649 pull.once('</section>')
1650 ]))
1651 })
1652 })
1653 )
1654 })
1655 }
1656
1657 /* Repo commit */
1658
1659 function serveRepoCommit(req, repo, rev) {
1660 return renderRepoPage(req, repo, null, rev, cat([
1661 readNext(function (cb) {
1662 repo.getCommitParsed(rev, function (err, commit) {
1663 if (err) return cb(err)
1664 var commitPath = [repo.id, 'commit', commit.id]
1665 var treePath = [repo.id, 'tree', commit.id]
1666 cb(null, cat([pull.once(
1667 '<h3>' + link(commitPath,
1668 req._t('CommitRev', {rev: rev})) + '</h3>' +
1669 '<section class="collapse">' +
1670 '<div class="right-bar">' +
1671 link(treePath, req._t('BrowseFiles')) +
1672 '</div>' +
1673 '<h4>' + linkify(escapeHTML(commit.title)) + '</h4>' +
1674 (commit.body ? linkify(pre(commit.body)) : '') +
1675 (commit.separateAuthor ? req._t('AuthoredOn', {
1676 name: escapeHTML(commit.author.name),
1677 date: commit.author.date.toLocaleString(req._locale)
1678 }) + '<br/>' : '') +
1679 req._t('CommittedOn', {
1680 name: escapeHTML(commit.committer.name),
1681 date: commit.committer.date.toLocaleString(req._locale)
1682 }) + '<br/>' +
1683 commit.parents.map(function (id) {
1684 return req._t('Parent') + ': ' +
1685 link([repo.id, 'commit', id], id)
1686 }).join('<br>') +
1687 '</section>' +
1688 '<section><h3>' + req._t('FilesChanged') + '</h3>'),
1689 // TODO: show diff from all parents (merge commits)
1690 renderDiffStat(req, [repo, repo], [commit.parents[0], commit.id]),
1691 pull.once('</section>')
1692 ]))
1693 })
1694 })
1695 ]))
1696 }
1697
1698 /* Diff stat */
1699
1700 function renderDiffStat(req, repos, treeIds) {
1701 if (treeIds.length == 0) treeIds = [null]
1702 var id = treeIds[0]
1703 var lastI = treeIds.length - 1
1704 var oldTree = treeIds[0]
1705 var changedFiles = []
1706 return cat([
1707 pull(
1708 Repo.diffTrees(repos, treeIds, true),
1709 pull.map(function (item) {
1710 var filename = escapeHTML(item.filename = item.path.join('/'))
1711 var oldId = item.id && item.id[0]
1712 var newId = item.id && item.id[lastI]
1713 var oldMode = item.mode && item.mode[0]
1714 var newMode = item.mode && item.mode[lastI]
1715 var action =
1716 !oldId && newId ? req._t('action.added') :
1717 oldId && !newId ? req._t('action.deleted') :
1718 oldMode != newMode ? req._t('action.changedMode', {
1719 old: oldMode.toString(8),
1720 new: newMode.toString(8)
1721 }) : req._t('changed')
1722 if (item.id)
1723 changedFiles.push(item)
1724 var blobsPath = item.id[1]
1725 ? [repos[1].id, 'blob', treeIds[1]]
1726 : [repos[0].id, 'blob', treeIds[0]]
1727 var rawsPath = item.id[1]
1728 ? [repos[1].id, 'raw', treeIds[1]]
1729 : [repos[0].id, 'raw', treeIds[0]]
1730 item.blobPath = blobsPath.concat(item.path)
1731 item.rawPath = rawsPath.concat(item.path)
1732 var fileHref = item.id ?
1733 '#' + encodeURIComponent(item.path.join('/')) :
1734 encodeLink(item.blobPath)
1735 return ['<a href="' + fileHref + '">' + filename + '</a>', action]
1736 }),
1737 table()
1738 ),
1739 pull(
1740 pull.values(changedFiles),
1741 paramap(function (item, cb) {
1742 var extension = getExtension(item.filename)
1743 if (extension in imgMimes) {
1744 var filename = escapeHTML(item.filename)
1745 return cb(null,
1746 '<pre><table class="code">' +
1747 '<tr><th id="' + escapeHTML(item.filename) + '">' +
1748 filename + '</th></tr>' +
1749 '<tr><td><img src="' + encodeLink(item.rawPath) + '"' +
1750 ' alt="' + filename + '"/></td></tr>' +
1751 '</table></pre>')
1752 }
1753 var done = multicb({ pluck: 1, spread: true })
1754 getRepoObjectString(repos[0], item.id[0], done())
1755 getRepoObjectString(repos[1], item.id[lastI], done())
1756 done(function (err, strOld, strNew) {
1757 if (err) return cb(err)
1758 cb(null, htmlLineDiff(req, item.filename, item.filename,
1759 strOld, strNew,
1760 encodeLink(item.blobPath)))
1761 })
1762 }, 4)
1763 )
1764 ])
1765 }
1766
1767 function htmlLineDiff(req, filename, anchor, oldStr, newStr, blobHref) {
1768 var diff = JsDiff.structuredPatch('', '', oldStr, newStr)
1769 var groups = diff.hunks.map(function (hunk) {
1770 var oldLine = hunk.oldStart
1771 var newLine = hunk.newStart
1772 var header = '<tr class="diff-hunk-header"><td colspan=2></td><td>' +
1773 '@@ -' + oldLine + ',' + hunk.oldLines + ' ' +
1774 '+' + newLine + ',' + hunk.newLines + ' @@' +
1775 '</td></tr>'
1776 return [header].concat(hunk.lines.map(function (line) {
1777 var s = line[0]
1778 if (s == '\\') return
1779 var html = highlight(line, getExtension(filename))
1780 var trClass = s == '+' ? 'diff-new' : s == '-' ? 'diff-old' : ''
1781 var lineNums = [s == '+' ? '' : oldLine++, s == '-' ? '' : newLine++]
1782 var id = [filename].concat(lineNums).join('-')
1783 return '<tr id="' + escapeHTML(id) + '" class="' + trClass + '">' +
1784 lineNums.map(function (num) {
1785 return '<td class="code-linenum">' +
1786 (num ? '<a href="#' + encodeURIComponent(id) + '">' +
1787 num + '</a>' : '') + '</td>'
1788 }).join('') +
1789 '<td class="code-text">' + html + '</td></tr>'
1790 }))
1791 })
1792 return '<pre><table class="code">' +
1793 '<tr><th colspan=3 id="' + escapeHTML(anchor) + '">' + filename +
1794 '<span class="right-bar">' +
1795 '<a href="' + blobHref + '">' + req._t('View') + '</a> ' +
1796 '</span></th></tr>' +
1797 [].concat.apply([], groups).join('') +
1798 '</table></pre>'
1799 }
1800
1801 /* An unknown message linking to a repo */
1802
1803 function serveRepoSomething(req, repo, id, msg, path) {
1804 return renderRepoPage(req, repo, null, null,
1805 pull.once('<section><h3>' + link([id]) + '</h3>' +
1806 json(msg) + '</section>'))
1807 }
1808
1809 /* Repo update */
1810
1811 function objsArr(objs) {
1812 return Array.isArray(objs) ? objs :
1813 Object.keys(objs).map(function (sha1) {
1814 var obj = Object.create(objs[sha1])
1815 obj.sha1 = sha1
1816 return obj
1817 })
1818 }
1819
1820 function serveRepoUpdate(req, repo, id, msg, path) {
1821 var raw = req._u.query.raw != null
1822
1823 if (raw)
1824 return renderRepoPage(req, repo, 'activity', null, pull.once(
1825 '<a href="?" class="raw-link header-align">' +
1826 req._t('Info') + '</a>' +
1827 '<h3>' + req._t('Update') + '</h3>' +
1828 '<section class="collapse">' +
1829 json({key: id, value: msg}) + '</section>'))
1830
1831 // convert packs to old single-object style
1832 if (msg.content.indexes) {
1833 for (var i = 0; i < msg.content.indexes.length; i++) {
1834 msg.content.packs[i] = {
1835 pack: {link: msg.content.packs[i].link},
1836 idx: msg.content.indexes[i]
1837 }
1838 }
1839 }
1840
1841 return renderRepoPage(req, repo, 'activity', null, cat([
1842 pull.values([
1843 '<a href="?raw" class="raw-link header-align">' +
1844 req._t('Data') + '</a>',
1845 '<h3>' + req._t('Update') + '</h3>',
1846 renderRepoUpdate(req, repo, {key: id, value: msg}, true)
1847 ].concat(msg.content.objects ?
1848 ['<h3>' + req._t('Objects') + '</h3>'].concat(
1849 objsArr(msg.content.objects).map(renderObject.bind(null, req)))
1850 : [],
1851 msg.content.packs ? [
1852 '<h3>' + req._t('Packs') + '</h3>'
1853 ].concat(msg.content.packs.map(renderPack.bind(null, req)))
1854 : [])),
1855 msg.content.packs && cat([
1856 pull.once('<h3>' + req._t('Commits') + '</h3>'),
1857 pull(
1858 pull.values(msg.content.packs),
1859 pull.asyncMap(function (pack, cb) {
1860 var done = multicb({ pluck: 1, spread: true })
1861 getBlob(req, pack.pack.link, done())
1862 getBlob(req, pack.idx.link, done())
1863 done(function (err, readPack, readIdx) {
1864 if (err) return cb(renderError(err))
1865 cb(null, gitPack.decodeWithIndex(repo, readPack, readIdx))
1866 })
1867 }),
1868 pull.flatten(),
1869 pull.asyncMap(function (obj, cb) {
1870 if (obj.type == 'commit')
1871 Repo.getCommitParsed(obj, cb)
1872 else
1873 pull(obj.read, pull.drain(null, cb))
1874 }),
1875 pull.filter(),
1876 pull.map(function (commit) {
1877 return renderCommit(req, repo, commit)
1878 })
1879 )
1880 ])
1881 ]))
1882 }
1883
1884 function renderObject(req, obj) {
1885 return '<section class="collapse">' +
1886 obj.type + ' ' + link([obj.link], obj.sha1) + '<br>' +
1887 req._t('NumBytes', obj.length) +
1888 '</section>'
1889 }
1890
1891 function renderPack(req, info) {
1892 return '<section class="collapse">' +
1893 (info.pack ? req._t('Pack') + ': ' +
1894 link([info.pack.link]) + '<br>' : '') +
1895 (info.idx ? req._t('Index') + ': ' +
1896 link([info.idx.link]) : '') + '</section>'
1897 }
1898
1899 /* Blob */
1900
1901 function serveRepoBlob(req, repo, rev, path) {
1902 return readNext(function (cb) {
1903 repo.getFile(rev, path, function (err, object) {
1904 if (err) return cb(null, serveBlobNotFound(req, repo.id, err))
1905 var type = repo.isCommitHash(rev) ? 'Tree' : 'Branch'
1906 var pathLinks = path.length === 0 ? '' :
1907 ': ' + linkPath([repo.id, 'tree'], [rev].concat(path))
1908 var rawFilePath = [repo.id, 'raw', rev].concat(path)
1909 var dirPath = path.slice(0, path.length-1)
1910 var filename = path[path.length-1]
1911 var extension = getExtension(filename)
1912 cb(null, renderRepoPage(req, repo, 'code', rev, cat([
1913 pull.once('<section><form action="" method="get">' +
1914 '<h3>' + req._t(type) + ': ' + rev + ' '),
1915 revMenu(req, repo, rev),
1916 pull.once('</h3></form>'),
1917 type == 'Branch' && renderRepoLatest(req, repo, rev),
1918 pull.once('</section><section class="collapse">' +
1919 '<h3>' + req._t('Files') + pathLinks + '</h3>' +
1920 '<div>' + object.length + ' bytes' +
1921 '<span class="raw-link">' +
1922 link(rawFilePath, req._t('Raw')) + '</span>' +
1923 '</div></section>' +
1924 '<section>'),
1925 extension in imgMimes
1926 ? pull.once('<img src="' + encodeLink(rawFilePath) +
1927 '" alt="' + escapeHTML(filename) + '" />')
1928 : renderObjectData(object, filename, repo, rev, dirPath),
1929 pull.once('</section>')
1930 ])))
1931 })
1932 })
1933 }
1934
1935 function serveBlobNotFound(req, repoId, err) {
1936 return serveTemplate(req, req._t('error.BlobNotFound'), 404)(pull.once(
1937 '<h2>' + req._t('error.BlobNotFound') + '</h2>' +
1938 '<p>' + req._t('error.BlobNotFoundInRepo', {
1939 repo: link([repoId])
1940 }) + '</p>' +
1941 '<pre>' + escapeHTML(err.stack) + '</pre>'
1942 ))
1943 }
1944
1945 /* Raw blob */
1946
1947 function serveRepoRaw(req, repo, branch, path) {
1948 return readNext(function (cb) {
1949 repo.getFile(branch, path, function (err, object) {
1950 if (err) return cb(null,
1951 serveBuffer(404, req._t('error.BlobNotFound')))
1952 var extension = getExtension(path[path.length-1])
1953 var contentType = imgMimes[extension]
1954 cb(null, pull(object.read, serveRaw(object.length, contentType)))
1955 })
1956 })
1957 }
1958
1959 function serveRaw(length, contentType) {
1960 var headers = {
1961 'Content-Type': contentType || 'text/plain; charset=utf-8',
1962 'Cache-Control': 'max-age=31536000'
1963 }
1964 if (length != null)
1965 headers['Content-Length'] = length
1966 return function (read) {
1967 return cat([pull.once([200, headers]), read])
1968 }
1969 }
1970
1971 function getBlob(req, key, cb) {
1972 ssb.blobs.want(key, function (err, got) {
1973 if (err) cb(err)
1974 else if (!got) cb(new Error(req._t('error.MissingBlob', {key: key})))
1975 else cb(null, ssb.blobs.get(key))
1976 })
1977 }
1978
1979 function serveBlob(req, key) {
1980 return readNext(function (cb) {
1981 getBlob(req, key, function (err, read) {
1982 if (err) cb(null, serveError(req, err))
1983 else if (!read) cb(null, serve404(req))
1984 else cb(null, serveRaw()(read))
1985 })
1986 })
1987 }
1988
1989 /* Digs */
1990
1991 function serveRepoDigs(req, repo) {
1992 return readNext(function (cb) {
1993 getVotes(repo.id, function (err, votes) {
1994 cb(null, renderRepoPage(req, repo, null, null, cat([
1995 pull.once('<section><h3>' + req._t('Digs') + '</h3>' +
1996 '<div>' + req._t('Total') + ': ' + votes.upvotes + '</div>'),
1997 pull(
1998 pull.values(Object.keys(votes.upvoters)),
1999 paramap(function (feedId, cb) {
2000 about.getName(feedId, function (err, name) {
2001 if (err) return cb(err)
2002 cb(null, link([feedId], name))
2003 })
2004 }, 8),
2005 ul()
2006 ),
2007 pull.once('</section>')
2008 ])))
2009 })
2010 })
2011 }
2012
2013 /* Forks */
2014
2015 function getForks(repo, includeSelf) {
2016 return pull(
2017 cat([
2018 includeSelf && readOnce(function (cb) {
2019 getMsg(repo.id, function (err, value) {
2020 cb(err, value && {key: repo.id, value: value})
2021 })
2022 }),
2023 ssb.links({
2024 dest: repo.id,
2025 values: true,
2026 rel: 'upstream'
2027 })
2028 ]),
2029 pull.filter(function (msg) {
2030 return msg.value.content && msg.value.content.type == 'git-repo'
2031 }),
2032 paramap(function (msg, cb) {
2033 getRepoFullName(about, msg.value.author, msg.key,
2034 function (err, repoName, authorName) {
2035 if (err) return cb(err)
2036 cb(null, {
2037 key: msg.key,
2038 value: msg.value,
2039 repoName: repoName,
2040 authorName: authorName
2041 })
2042 })
2043 }, 8)
2044 )
2045 }
2046
2047 function serveRepoForks(req, repo) {
2048 var hasForks
2049 return renderRepoPage(req, repo, null, null, cat([
2050 pull.once('<h3>' + req._t('Forks') + '</h3>'),
2051 pull(
2052 getForks(repo),
2053 pull.map(function (msg) {
2054 hasForks = true
2055 return '<section class="collapse">' +
2056 link([msg.value.author], msg.authorName) + ' / ' +
2057 link([msg.key], msg.repoName) +
2058 '<span class="right-bar">' +
2059 timestamp(msg.value.timestamp, req) +
2060 '</span></section>'
2061 })
2062 ),
2063 readOnce(function (cb) {
2064 cb(null, hasForks ? '' : req._t('NoForks'))
2065 })
2066 ]))
2067 }
2068
2069 function serveRepoForkPrompt(req, repo) {
2070 return renderRepoPage(req, repo, null, null, pull.once(
2071 '<form action="" method="post" onreset="history.back()">' +
2072 '<h3>' + req._t('ForkRepoPrompt') + '</h3>' +
2073 '<p>' + hiddenInputs({ id: repo.id }) +
2074 '<button class="btn open" type="submit" name="action" value="fork">' +
2075 req._t('Fork') +
2076 '</button>' +
2077 ' <button class="btn" type="reset">' +
2078 req._t('Cancel') + '</button>' +
2079 '</p></form>'
2080 ))
2081 }
2082
2083 /* Issues */
2084
2085 function serveRepoIssues(req, repo, isPRs) {
2086 var count = 0
2087 var state = req._u.query.state || 'open'
2088 var newPath = isPRs ? [repo.id, 'compare'] : [repo.id, 'issues', 'new']
2089 return renderRepoPage(req, repo, isPRs ? 'pulls' : 'issues', null, cat([
2090 pull.once(
2091 (isPublic ? '' :
2092 '<form class="right-bar" method="get"' +
2093 ' action="' + encodeLink(newPath) + '">' +
2094 '<button class="btn">&plus; ' +
2095 req._t(isPRs ? 'pullRequest.New' : 'issue.New') +
2096 '</button>' +
2097 '</form>') +
2098 '<h3>' + req._t(isPRs ? 'PullRequests' : 'Issues') + '</h3>' +
2099 nav([
2100 ['?', req._t('issues.Open'), 'open'],
2101 ['?state=closed', req._t('issues.Closed'), 'closed'],
2102 ['?state=all', req._t('issues.All'), 'all']
2103 ], state)),
2104 pull(
2105 (isPRs ? pullReqs : issues).list({
2106 repo: repo.id,
2107 project: repo.id,
2108 open: {open: true, closed: false}[state]
2109 }),
2110 pull.map(function (issue) {
2111 count++
2112 var state = (issue.open ? 'open' : 'closed')
2113 var stateStr = req._t(issue.open ?
2114 'issue.state.Open' : 'issue.state.Closed')
2115 return '<section class="collapse">' +
2116 '<i class="issue-state issue-state-' + state + '"' +
2117 ' title="' + stateStr + '">โ—ผ</i> ' +
2118 '<a href="' + encodeLink(issue.id) + '">' +
2119 escapeHTML(issue.title) +
2120 '<span class="right-bar">' +
2121 new Date(issue.created_at).toLocaleString(req._locale) +
2122 '</span>' +
2123 '</a>' +
2124 '</section>'
2125 })
2126 ),
2127 readOnce(function (cb) {
2128 cb(null, count > 0 ? '' :
2129 '<p>' + req._t(isPRs ? 'NoPullRequests' : 'NoIssues') + '</p>')
2130 })
2131 ]))
2132 }
2133
2134 /* New Issue */
2135
2136 function serveRepoNewIssue(req, repo, issueId, path) {
2137 return renderRepoPage(req, repo, 'issues', null, pull.once(
2138 '<h3>' + req._t('issue.New') + '</h3>' +
2139 '<section><form action="" method="post">' +
2140 '<input type="hidden" name="action" value="new-issue">' +
2141 '<p><input class="wide-input" name="title" placeholder="' +
2142 req._t('issue.Title') + '" size="77" /></p>' +
2143 renderPostForm(req, repo, req._t('Description'), 8) +
2144 '<button type="submit" class="btn">' + req._t('Create') + '</button>' +
2145 '</form></section>'))
2146 }
2147
2148 /* Issue */
2149
2150 function serveRepoIssue(req, repo, issue, path, postId) {
2151 var isAuthor = (myId == issue.author) || (myId == repo.feed)
2152 var newestMsg = {key: issue.id, value: {timestamp: issue.created_at}}
2153 return renderRepoPage(req, repo, 'issues', null, cat([
2154 pull.once(
2155 renderNameForm(req, !isPublic, issue.id, issue.title, 'issue-title',
2156 null, req._t('issue.Rename'),
2157 '<h3>' + link([issue.id], issue.title) + '</h3>') +
2158 '<code>' + issue.id + '</code>' +
2159 '<section class="collapse">' +
2160 (issue.open
2161 ? '<strong class="issue-status open">' +
2162 req._t('issue.state.Open') + '</strong>'
2163 : '<strong class="issue-status closed">' +
2164 req._t('issue.state.Closed') + '</strong>')),
2165 readOnce(function (cb) {
2166 about.getName(issue.author, function (err, authorName) {
2167 if (err) return cb(err)
2168 var authorLink = link([issue.author], authorName)
2169 cb(null, req._t('issue.Opened',
2170 {name: authorLink, datetime: timestamp(issue.created_at, req)}))
2171 })
2172 }),
2173 pull.once('<hr/>' + markdown(issue.text, repo) + '</section>'),
2174 // render posts and edits
2175 pull(
2176 ssb.links({
2177 dest: issue.id,
2178 values: true
2179 }),
2180 pull.unique('key'),
2181 addAuthorName(about),
2182 sortMsgs(),
2183 pull.through(function (msg) {
2184 if (msg.value.timestamp > newestMsg.value.timestamp)
2185 newestMsg = msg
2186 }),
2187 pull.map(renderIssueActivityMsg.bind(null, req, repo, issue,
2188 req._t('issue.'), postId))
2189 ),
2190 isPublic ? pull.empty() : readOnce(function (cb) {
2191 cb(null, renderIssueCommentForm(req, issue, repo, newestMsg.key,
2192 isAuthor, req._t('issue.')))
2193 })
2194 ]))
2195 }
2196
2197 function renderIssueActivityMsg(req, repo, issue, type, postId, msg) {
2198 var authorLink = link([msg.value.author], msg.authorName)
2199 var msgHref = encodeLink(msg.key) + '#' + encodeURIComponent(msg.key)
2200 var msgTimeLink = '<a href="' + msgHref + '"' +
2201 ' name="' + escapeHTML(msg.key) + '">' +
2202 new Date(msg.value.timestamp).toLocaleString(req._locale) + '</a>'
2203 var c = msg.value.content
2204 switch (c.type) {
2205 case 'post':
2206 if (c.root == issue.id) {
2207 var changed = issues.isStatusChanged(msg, issue)
2208 return '<section class="collapse">' +
2209 (msg.key == postId ? '<div class="highlight">' : '') +
2210 '<tt class="right-bar item-id">' + msg.key + '</tt> ' +
2211 (changed == null ? authorLink : req._t(
2212 changed ? 'issue.Reopened' : 'issue.Closed',
2213 {name: authorLink, type: type})) +
2214 ' &middot; ' + msgTimeLink +
2215 (msg.key == postId ? '</div>' : '') +
2216 markdown(c.text, repo) +
2217 '</section>'
2218 } else {
2219 var text = c.text || (c.type + ' ' + msg.key)
2220 return '<section class="collapse mention-preview">' +
2221 req._t('issue.MentionedIn', {
2222 name: authorLink,
2223 type: type,
2224 post: '<a href="/' + msg.key + '#' + msg.key + '">' +
2225 String(text).substr(0, 140) + '</a>'
2226 }) + '</section>'
2227 }
2228 case 'issue':
2229 case 'pull-request':
2230 return '<section class="collapse mention-preview">' +
2231 req._t('issue.MentionedIn', {
2232 name: authorLink,
2233 type: type,
2234 post: link([msg.key], String(c.title || msg.key).substr(0, 140))
2235 }) + '</section>'
2236 case 'issue-edit':
2237 return '<section class="collapse">' +
2238 (msg.key == postId ? '<div class="highlight">' : '') +
2239 (c.title == null ? '' : req._t('issue.Renamed', {
2240 name: authorLink,
2241 type: type,
2242 name: '<q>' + escapeHTML(c.title) + '</q>'
2243 })) + ' &middot; ' + msgTimeLink +
2244 (msg.key == postId ? '</div>' : '') +
2245 '</section>'
2246 case 'git-update':
2247 var mention = issues.getMention(msg, issue)
2248 if (mention) {
2249 var commitLink = link([repo.id, 'commit', mention.object],
2250 mention.label || mention.object)
2251 return '<section class="collapse">' +
2252 req._t(mention.open ? 'issue.Reopened' : 'issue.Closed', {
2253 name: authorLink,
2254 type: type
2255 }) + ' &middot; ' + msgTimeLink + '<br/>' +
2256 commitLink +
2257 '</section>'
2258 } else if ((mention = getMention(msg, issue.id))) {
2259 var commitLink = link(mention.object ?
2260 [repo.id, 'commit', mention.object] : [msg.key],
2261 mention.label || mention.object || msg.key)
2262 return '<section class="collapse">' +
2263 req._t('issue.Mentioned', {
2264 name: authorLink,
2265 type: type
2266 }) + ' &middot; ' + msgTimeLink + '<br/>' +
2267 commitLink +
2268 '</section>'
2269 } else {
2270 // fallthrough
2271 }
2272
2273 default:
2274 return '<section class="collapse">' +
2275 authorLink +
2276 ' &middot; ' + msgTimeLink +
2277 json(c) +
2278 '</section>'
2279 }
2280 }
2281
2282 function renderIssueCommentForm(req, issue, repo, branch, isAuthor, type) {
2283 return '<section><form action="" method="post">' +
2284 '<input type="hidden" name="action" value="comment">' +
2285 '<input type="hidden" name="id" value="' + issue.id + '">' +
2286 '<input type="hidden" name="issue" value="' + issue.id + '">' +
2287 '<input type="hidden" name="repo" value="' + repo.id + '">' +
2288 '<input type="hidden" name="branch" value="' + branch + '">' +
2289 renderPostForm(req, repo) +
2290 '<input type="submit" class="btn open" value="' +
2291 req._t('issue.Comment') + '" />' +
2292 (isAuthor ?
2293 '<input type="submit" class="btn"' +
2294 ' name="' + (issue.open ? 'close' : 'open') + '"' +
2295 ' value="' + req._t(
2296 issue.open ? 'issue.Close' : 'issue.Reopen', {type: type}
2297 ) + '"/>' : '') +
2298 '</form></section>'
2299 }
2300
2301 /* Pull Request */
2302
2303 function serveRepoPullReq(req, repo, pr, path, postId) {
2304 var headRepo, authorLink
2305 var page = path[0] || 'activity'
2306 return renderRepoPage(req, repo, 'pulls', null, cat([
2307 pull.once('<div class="pull-request">' +
2308 renderNameForm(req, !isPublic, pr.id, pr.title, 'issue-title', null,
2309 req._t('pullRequest.Rename'),
2310 '<h3>' + link([pr.id], pr.title) + '</h3>') +
2311 '<code>' + pr.id + '</code>'),
2312 readOnce(function (cb) {
2313 var done = multicb({ pluck: 1, spread: true })
2314 var gotHeadRepo = done()
2315 about.getName(pr.author, done())
2316 var sameRepo = (pr.headRepo == pr.baseRepo)
2317 getRepo(pr.headRepo, function (err, headRepo) {
2318 if (err) return cb(err)
2319 getRepoName(about, headRepo.feed, headRepo.id, done())
2320 about.getName(headRepo.feed, done())
2321 gotHeadRepo(null, Repo(headRepo))
2322 })
2323
2324 done(function (err, _headRepo, issueAuthorName,
2325 headRepoName, headRepoAuthorName) {
2326 if (err) return cb(err)
2327 headRepo = _headRepo
2328 authorLink = link([pr.author], issueAuthorName)
2329 var repoLink = link([pr.headRepo], headRepoName)
2330 var headRepoAuthorLink = link([headRepo.feed], headRepoAuthorName)
2331 var headRepoLink = link([headRepo.id], headRepoName)
2332 var headBranchLink = link([headRepo.id, 'tree', pr.headBranch])
2333 var baseBranchLink = link([repo.id, 'tree', pr.baseBranch])
2334 cb(null, '<section class="collapse">' +
2335 '<strong class="issue-status ' +
2336 (pr.open ? 'open' : 'closed') + '">' +
2337 req._t(pr.open ? 'issue.state.Open' : 'issue.state.Closed') +
2338 '</strong> ' +
2339 req._t('pullRequest.WantToMerge', {
2340 name: authorLink,
2341 base: '<code>' + baseBranchLink + '</code>',
2342 head: (sameRepo ?
2343 '<code>' + headBranchLink + '</code>' :
2344 '<code class="bgslash">' +
2345 headRepoAuthorLink + ' / ' +
2346 headRepoLink + ' / ' +
2347 headBranchLink + '</code>')
2348 }) + '</section>')
2349 })
2350 }),
2351 pull.once(
2352 nav([
2353 [[pr.id], req._t('Discussion'), 'activity'],
2354 [[pr.id, 'commits'], req._t('Commits'), 'commits'],
2355 [[pr.id, 'files'], req._t('Files'), 'files']
2356 ], page)),
2357 readNext(function (cb) {
2358 if (page == 'commits')
2359 renderPullReqCommits(req, pr, repo, headRepo, cb)
2360 else if (page == 'files')
2361 renderPullReqFiles(req, pr, repo, headRepo, cb)
2362 else cb(null,
2363 renderPullReqActivity(req, pr, repo, headRepo, authorLink, postId))
2364 })
2365 ]))
2366 }
2367
2368 function renderPullReqCommits(req, pr, baseRepo, headRepo, cb) {
2369 pullReqs.getRevs(pr.id, function (err, revs) {
2370 if (err) return cb(null, renderError(err))
2371 cb(null, cat([
2372 pull.once('<section>'),
2373 renderCommitLog(req, baseRepo, revs.base, headRepo, revs.head),
2374 pull.once('</section>')
2375 ]))
2376 })
2377 }
2378
2379 function renderPullReqFiles(req, pr, baseRepo, headRepo, cb) {
2380 pullReqs.getRevs(pr.id, function (err, revs) {
2381 if (err) return cb(null, renderError(err))
2382 cb(null, cat([
2383 pull.once('<section>'),
2384 renderDiffStat(req,
2385 [baseRepo, headRepo], [revs.base, revs.head]),
2386 pull.once('</section>')
2387 ]))
2388 })
2389 }
2390
2391 function renderPullReqActivity(req, pr, repo, headRepo, authorLink, postId) {
2392 var msgTimeLink = link([pr.id],
2393 new Date(pr.created_at).toLocaleString(req._locale))
2394 var newestMsg = {key: pr.id, value: {timestamp: pr.created_at}}
2395 var isAuthor = (myId == pr.author) || (myId == repo.feed)
2396 return cat([
2397 readOnce(function (cb) {
2398 cb(null,
2399 '<section class="collapse">' +
2400 authorLink + ' &middot; ' + msgTimeLink +
2401 markdown(pr.text, repo) + '</section>')
2402 }),
2403 // render posts, edits, and updates
2404 pull(
2405 many([
2406 ssb.links({
2407 dest: pr.id,
2408 values: true
2409 }),
2410 readNext(function (cb) {
2411 cb(null, pull(
2412 ssb.links({
2413 dest: headRepo.id,
2414 source: headRepo.feed,
2415 rel: 'repo',
2416 values: true,
2417 reverse: true
2418 }),
2419 pull.take(function (link) {
2420 return link.value.timestamp > pr.created_at
2421 }),
2422 pull.filter(function (link) {
2423 return link.value.content.type == 'git-update'
2424 && ('refs/heads/' + pr.headBranch) in link.value.content.refs
2425 })
2426 ))
2427 })
2428 ]),
2429 addAuthorName(about),
2430 pull.unique('key'),
2431 pull.through(function (msg) {
2432 if (msg.value.timestamp > newestMsg.value.timestamp)
2433 newestMsg = msg
2434 }),
2435 sortMsgs(),
2436 pull.map(function (item) {
2437 if (item.value.content.type == 'git-update')
2438 return renderBranchUpdate(req, pr, item)
2439 return renderIssueActivityMsg(req, repo, pr,
2440 req._t('pull request'), postId, item)
2441 })
2442 ),
2443 !isPublic && isAuthor && pr.open && pull.once(
2444 '<section class="merge-instructions">' +
2445 '<input type="checkbox" class="toggle" id="merge-instructions"/>' +
2446 '<h4><label for="merge-instructions" class="toggle-link"><a>' +
2447 req._t('mergeInstructions.MergeViaCmdLine') +
2448 '</a></label></h4>' +
2449 '<div class="contents">' +
2450 '<p>' + req._t('mergeInstructions.CheckOut') + '</p>' +
2451 '<pre>' +
2452 'git fetch ssb://' + escapeHTML(pr.headRepo) + ' ' +
2453 escapeHTML(pr.headBranch) + '\n' +
2454 'git checkout -b ' + escapeHTML(pr.headBranch) + ' FETCH_HEAD' +
2455 '</pre>' +
2456 '<p>' + req._t('mergeInstructions.MergeAndPush') + '</p>' +
2457 '<pre>' +
2458 'git checkout ' + escapeHTML(pr.baseBranch) + '\n' +
2459 'git merge ' + escapeHTML(pr.headBranch) + '\n' +
2460 'git push ssb ' + escapeHTML(pr.baseBranch) +
2461 '</pre>' +
2462 '</div></section>'),
2463 !isPublic && readOnce(function (cb) {
2464 cb(null, renderIssueCommentForm(req, pr, repo, newestMsg.key, isAuthor,
2465 req._t('pull request')))
2466 })
2467 ])
2468 }
2469
2470 function renderBranchUpdate(req, pr, msg) {
2471 var authorLink = link([msg.value.author], msg.authorName)
2472 var msgLink = link([msg.key],
2473 new Date(msg.value.timestamp).toLocaleString(req._locale))
2474 var rev = msg.value.content.refs['refs/heads/' + pr.headBranch]
2475 if (!rev)
2476 return '<section class="collapse">' +
2477 req._t('NameDeletedBranch', {
2478 name: authorLink,
2479 branch: '<code>' + pr.headBranch + '</code>'
2480 }) + ' &middot; ' + msgLink +
2481 '</section>'
2482
2483 var revLink = link([pr.headRepo, 'commit', rev], rev.substr(0, 8))
2484 return '<section class="collapse">' +
2485 req._t('NameUpdatedBranch', {
2486 name: authorLink,
2487 rev: '<code>' + revLink + '</code>'
2488 }) + ' &middot; ' + msgLink +
2489 '</section>'
2490 }
2491
2492 /* Compare changes */
2493
2494 function serveRepoCompare(req, repo) {
2495 var query = req._u.query
2496 var base
2497 var count = 0
2498
2499 return renderRepoPage(req, repo, 'pulls', null, cat([
2500 pull.once('<h3>' + req._t('CompareChanges') + '</h3>' +
2501 '<form action="' + encodeLink(repo.id) + '/comparing" method="get">' +
2502 '<section>'),
2503 pull.once(req._t('BaseBranch') + ': '),
2504 readNext(function (cb) {
2505 if (query.base) gotBase(null, query.base)
2506 else repo.getSymRef('HEAD', true, gotBase)
2507 function gotBase(err, ref) {
2508 if (err) return cb(err)
2509 cb(null, branchMenu(repo, 'base', base = ref || 'HEAD'))
2510 }
2511 }),
2512 pull.once('<br/>' + req._t('ComparisonRepoBranch') + ':'),
2513 pull(
2514 getForks(repo, true),
2515 pull.asyncMap(function (msg, cb) {
2516 getRepo(msg.key, function (err, repo) {
2517 if (err) return cb(err)
2518 cb(null, {
2519 msg: msg,
2520 repo: repo
2521 })
2522 })
2523 }),
2524 pull.map(renderFork),
2525 pull.flatten()
2526 ),
2527 pull.once('</section>'),
2528 readOnce(function (cb) {
2529 cb(null, count == 0 ? req._t('NoBranches') :
2530 '<button type="submit" class="btn">' +
2531 req._t('Compare') + '</button>')
2532 }),
2533 pull.once('</form>')
2534 ]))
2535
2536 function renderFork(fork) {
2537 return pull(
2538 fork.repo.refs(),
2539 pull.map(function (ref) {
2540 var m = /^refs\/([^\/]*)\/(.*)$/.exec(ref.name) || [,ref.name]
2541 return {
2542 type: m[1],
2543 name: m[2],
2544 value: ref.value
2545 }
2546 }),
2547 pull.filter(function (ref) {
2548 return ref.type == 'heads'
2549 && !(ref.name == base && fork.msg.key == repo.id)
2550 }),
2551 pull.map(function (ref) {
2552 var branchLink = link([fork.msg.key, 'tree', ref.name], ref.name)
2553 var authorLink = link([fork.msg.value.author], fork.msg.authorName)
2554 var repoLink = link([fork.msg.key], fork.msg.repoName)
2555 var value = fork.msg.key + ':' + ref.name
2556 count++
2557 return '<div class="bgslash">' +
2558 '<input type="radio" name="head"' +
2559 ' value="' + escapeHTML(value) + '"' +
2560 (query.head == value ? ' checked="checked"' : '') + '> ' +
2561 authorLink + ' / ' + repoLink + ' / ' + branchLink + '</div>'
2562 })
2563 )
2564 }
2565 }
2566
2567 function serveRepoComparing(req, repo) {
2568 var query = req._u.query
2569 var baseBranch = query.base
2570 var s = (query.head || '').split(':')
2571
2572 if (!s || !baseBranch)
2573 return serveRedirect(req, encodeLink([repo.id, 'compare']))
2574
2575 var headRepoId = s[0]
2576 var headBranch = s[1]
2577 var baseLink = link([repo.id, 'tree', baseBranch])
2578 var headBranchLink = link([headRepoId, 'tree', headBranch])
2579 var backHref = encodeLink([repo.id, 'compare']) + req._u.search
2580
2581 return renderRepoPage(req, repo, 'pulls', null, cat([
2582 pull.once('<h3>' +
2583 req._t(query.expand ? 'OpenPullRequest': 'ComparingChanges') +
2584 '</h3>'),
2585 readNext(function (cb) {
2586 getRepo(headRepoId, function (err, headRepo) {
2587 if (err) return cb(err)
2588 getRepoFullName(about, headRepo.feed, headRepo.id,
2589 function (err, repoName, authorName) {
2590 if (err) return cb(err)
2591 cb(null, renderRepoInfo(Repo(headRepo), repoName, authorName))
2592 }
2593 )
2594 })
2595 })
2596 ]))
2597
2598 function renderRepoInfo(headRepo, headRepoName, headRepoAuthorName) {
2599 var authorLink = link([headRepo.feed], headRepoAuthorName)
2600 var repoLink = link([headRepoId], headRepoName)
2601 return cat([
2602 pull.once('<section>' +
2603 req._t('Base') + ': ' + baseLink + '<br/>' +
2604 req._t('Head') + ': ' +
2605 '<span class="bgslash">' + authorLink + ' / ' + repoLink +
2606 ' / ' + headBranchLink + '</span>' +
2607 '</section>' +
2608 (query.expand ? '<section><form method="post" action="">' +
2609 hiddenInputs({
2610 action: 'new-pull',
2611 branch: baseBranch,
2612 head_repo: headRepoId,
2613 head_branch: headBranch
2614 }) +
2615 '<input class="wide-input" name="title"' +
2616 ' placeholder="' + req._t('Title') + '" size="77"/>' +
2617 renderPostForm(req, repo, req._t('Description'), 8) +
2618 '<button type="submit" class="btn open">' +
2619 req._t('Create') + '</button>' +
2620 '</form></section>'
2621 : '<section><form method="get" action="">' +
2622 hiddenInputs({
2623 base: baseBranch,
2624 head: query.head
2625 }) +
2626 '<button class="btn open" type="submit" name="expand" value="1">' +
2627 '<i>โŽ‡</i> ' + req._t('CreatePullRequest') + '</button> ' +
2628 '<a href="' + backHref + '">' + req._t('Back') + '</a>' +
2629 '</form></section>') +
2630 '<div id="commits"></div>' +
2631 '<div class="tab-links">' +
2632 '<a href="#" id="files-link">' + req._t('FilesChanged') + '</a> ' +
2633 '<a href="#commits" id="commits-link">' +
2634 req._t('Commits') + '</a>' +
2635 '</div>' +
2636 '<section id="files-tab">'),
2637 renderDiffStat(req, [repo, headRepo], [baseBranch, headBranch]),
2638 pull.once('</section>' +
2639 '<section id="commits-tab">'),
2640 renderCommitLog(req, repo, baseBranch, headRepo, headBranch),
2641 pull.once('</section>')
2642 ])
2643 }
2644 }
2645
2646 function renderCommitLog(req, baseRepo, baseBranch, headRepo, headBranch) {
2647 return cat([
2648 pull.once('<table class="compare-commits">'),
2649 readNext(function (cb) {
2650 baseRepo.resolveRef(baseBranch, function (err, baseBranchRev) {
2651 if (err) return cb(err)
2652 var currentDay
2653 return cb(null, pull(
2654 headRepo.readLog(headBranch),
2655 pull.take(function (rev) { return rev != baseBranchRev }),
2656 pullReverse(),
2657 paramap(headRepo.getCommitParsed.bind(headRepo), 8),
2658 pull.map(function (commit) {
2659 var commitPath = [headRepo.id, 'commit', commit.id]
2660 var commitIdShort = '<tt>' + commit.id.substr(0, 8) + '</tt>'
2661 var day = Math.floor(commit.author.date / 86400000)
2662 var dateRow = day == currentDay ? '' :
2663 '<tr><th colspan=3 class="date-info">' +
2664 commit.author.date.toLocaleDateString(req._locale) +
2665 '</th><tr>'
2666 currentDay = day
2667 return dateRow + '<tr>' +
2668 '<td>' + escapeHTML(commit.author.name) + '</td>' +
2669 '<td>' + link(commitPath, commit.title) + '</td>' +
2670 '<td>' + link(commitPath, commitIdShort, true) + '</td>' +
2671 '</tr>'
2672 })
2673 ))
2674 })
2675 }),
2676 pull.once('</table>')
2677 ])
2678 }
2679}
2680

Built with git-ssb-web