git ssb

30+

cel / git-ssb-web



Tree: 351e84025393b1505cc97f2d743bc5be6c97b6ac

Files: 351e84025393b1505cc97f2d743bc5be6c97b6ac / index.js

90972 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 (this.options.repo && this.options.rev && this.options.path)
47 url = path.join('/', encodeURIComponent(this.options.repo.id),
48 'raw', this.options.rev, this.options.path.join('/'), href)
49 else
50 return text
51 return '<img src="' + escapeHTML(url) + '" alt="' + text + '"' +
52 (title ? ' title="' + title + '"' : '') + '/>'
53}
54
55function getExtension(filename) {
56 return (/\.([^.]+)$/.exec(filename) || [,filename])[1]
57}
58
59function highlight(code, lang) {
60 try {
61 return lang
62 ? Highlight.highlight(lang, code).value
63 : Highlight.highlightAuto(code).value
64 } catch(e) {
65 if (/^Unknown language/.test(e.message))
66 return escapeHTML(code)
67 throw e
68 }
69}
70
71marked.setOptions({
72 gfm: true,
73 mentions: true,
74 tables: true,
75 breaks: true,
76 pedantic: false,
77 sanitize: true,
78 smartLists: true,
79 smartypants: false,
80 highlight: highlight,
81 renderer: blockRenderer
82})
83
84// hack to make git link mentions work
85var mdRules = new marked.InlineLexer(1, marked.defaults).rules
86mdRules.mention =
87 /^(\s)?([@%&][A-Za-z0-9\._\-+=\/]*[A-Za-z0-9_\-+=\/]|[0-9a-f]{40})/
88mdRules.text = /^[\s\S]+?(?=[\\<!\[_*`]| {2,}\n| [@%&]|[0-9a-f]{40}|$)/
89
90function markdown(text, options, cb) {
91 if (!text) return ''
92 if (typeof text != 'string') text = String(text)
93 if (!options) options = {}
94 else if (options.id) options = {repo: options}
95 if (!options.rev) options.rev = 'HEAD'
96 if (!options.path) options.path = []
97
98 return marked(text, options, cb)
99}
100
101function ParamError(msg) {
102 var err = Error.call(this, msg)
103 err.name = ParamError.name
104 return err
105}
106util.inherits(ParamError, Error)
107
108function parseAddr(str, def) {
109 if (!str) return def
110 var i = str.lastIndexOf(':')
111 if (~i) return {host: str.substr(0, i), port: str.substr(i+1)}
112 if (isNaN(str)) return {host: str, port: def.port}
113 return {host: def.host, port: str}
114}
115
116function isArray(arr) {
117 return Object.prototype.toString.call(arr) == '[object Array]'
118}
119
120function encodeLink(url) {
121 if (!isArray(url)) url = [url]
122 return '/' + url.map(encodeURIComponent).join('/')
123}
124
125function link(parts, text, raw, props) {
126 if (text == null) text = parts[parts.length-1]
127 if (!raw) text = escapeHTML(text)
128 return '<a href="' + encodeLink(parts) + '"' +
129 (props ? ' ' + props : '') +
130 '>' + text + '</a>'
131}
132
133function linkify(text) {
134 // regex is from ssb-ref
135 return text.replace(/(@|%|&|&amp;)[A-Za-z0-9\/+]{43}=\.[\w\d]+/g, function (str) {
136 return '<a href="/' + encodeURIComponent(str) + '">' + str + '</a>'
137 })
138}
139
140function timestamp(time, req) {
141 time = Number(time)
142 var d = new Date(time)
143 return '<span title="' + time + '">' +
144 d.toLocaleString(req._locale) + '</span>'
145}
146
147function pre(text) {
148 return '<pre>' + escapeHTML(text) + '</pre>'
149}
150
151function json(obj) {
152 return linkify(pre(JSON.stringify(obj, null, 2)))
153}
154
155function escapeHTML(str) {
156 return String(str)
157 .replace(/&/g, '&amp;')
158 .replace(/</g, '&lt;')
159 .replace(/>/g, '&gt;')
160 .replace(/"/g, '&quot;')
161}
162
163function table(props) {
164 return function (read) {
165 return cat([
166 pull.once('<table' + (props ? ' ' + props : '') + '>'),
167 pull(
168 read,
169 pull.map(function (row) {
170 return row ? '<tr>' + row.map(function (cell) {
171 return '<td>' + cell + '</td>'
172 }).join('') + '</tr>' : ''
173 })
174 ),
175 pull.once('</table>')
176 ])
177 }
178}
179
180function ul(props) {
181 return function (read) {
182 return cat([
183 pull.once('<ul' + (props ? ' ' + props : '') + '>'),
184 pull(
185 read,
186 pull.map(function (li) {
187 return '<li>' + li + '</li>'
188 })
189 ),
190 pull.once('</ul>')
191 ])
192 }
193}
194
195function nav(links, page, after) {
196 return ['<nav>'].concat(
197 links.map(function (link) {
198 var href = typeof link[0] == 'string' ? link[0] : encodeLink(link[0])
199 var props = link[2] == page ? ' class="active"' : ''
200 return '<a href="' + href + '"' + props + '>' + link[1] + '</a>'
201 }), after || '', '</nav>').join('')
202}
203
204function renderNameForm(req, enabled, id, name, action, inputId, title, header) {
205 if (!inputId) inputId = action
206 return '<form class="petname" action="" method="post">' +
207 (enabled ?
208 '<input type="checkbox" class="name-checkbox" id="' + inputId + '" ' +
209 'onfocus="this.form.name.focus()" />' +
210 '<input name="name" class="name" value="' + escapeHTML(name) + '" ' +
211 'onkeyup="if (event.keyCode == 27) this.form.reset()" />' +
212 '<input type="hidden" name="action" value="' + action + '">' +
213 '<input type="hidden" name="id" value="' +
214 escapeHTML(id) + '">' +
215 '<label class="name-toggle" for="' + inputId + '" ' +
216 'title="' + title + '"><i>โœ</i></label> ' +
217 '<input class="btn name-btn" type="submit" value="' +
218 req._t('Rename') + '">' +
219 header :
220 header + '<br clear="all"/>'
221 ) +
222 '</form>'
223}
224
225function renderPostForm(req, repo, placeholder, rows) {
226 return '<input type="radio" class="tab-radio" id="tab1" name="tab" checked="checked"/>' +
227 '<input type="radio" class="tab-radio" id="tab2" name="tab"/>' +
228 '<div id="tab-links" class="tab-links" style="display:none">' +
229 '<label for="tab1" id="write-tab-link" class="tab1-link">' +
230 req._t('post.Write') + '</label>' +
231 '<label for="tab2" id="preview-tab-link" class="tab2-link">' +
232 req._t('post.Preview') + '</label>' +
233 '</div>' +
234 '<input type="hidden" id="repo-id" value="' + repo.id + '"/>' +
235 '<div id="write-tab" class="tab1">' +
236 '<textarea id="post-text" name="text" class="wide-input"' +
237 ' rows="' + (rows||4) + '" cols="77"' +
238 (placeholder ? ' placeholder="' + placeholder + '"' : '') +
239 '></textarea>' +
240 '</div>' +
241 '<div class="preview-text tab2" id="preview-tab"></div>' +
242 '<script>' + issueCommentScript + '</script>'
243}
244
245function hiddenInputs(values) {
246 return Object.keys(values).map(function (key) {
247 return '<input type="hidden"' +
248 ' name="' + escapeHTML(key) + '"' +
249 ' value="' + escapeHTML(values[key]) + '"/>'
250 }).join('')
251}
252
253function readNext(fn) {
254 var next
255 return function (end, cb) {
256 if (next) return next(end, cb)
257 fn(function (err, _next) {
258 if (err) return cb(err)
259 next = _next
260 next(null, cb)
261 })
262 }
263}
264
265function readOnce(fn) {
266 var ended
267 return function (end, cb) {
268 fn(function (err, data) {
269 if (err || ended) return cb(err || ended)
270 ended = true
271 cb(null, data)
272 })
273 }
274}
275
276function paginate(onFirst, through, onLast, onEmpty) {
277 var ended, last, first = true, queue = []
278 return function (read) {
279 var mappedRead = through(function (end, cb) {
280 if (ended = end) return read(ended, cb)
281 if (queue.length)
282 return cb(null, queue.shift())
283 read(null, function (end, data) {
284 if (end) return cb(end)
285 last = data
286 cb(null, data)
287 })
288 })
289 return function (end, cb) {
290 var tmp
291 if (ended) return cb(ended)
292 if (ended = end) return read(ended, cb)
293 if (first)
294 return read(null, function (end, data) {
295 if (ended = end) {
296 if (end === true && onEmpty)
297 return onEmpty(cb)
298 return cb(ended)
299 }
300 first = false
301 last = data
302 queue.push(data)
303 if (onFirst)
304 onFirst(data, cb)
305 else
306 mappedRead(null, cb)
307 })
308 mappedRead(null, function (end, data) {
309 if (ended = end) {
310 if (end === true && last)
311 return onLast(last, cb)
312 }
313 cb(end, data)
314 })
315 }
316 }
317}
318
319function readObjectString(obj, cb) {
320 pull(obj.read, pull.collect(function (err, bufs) {
321 if (err) return cb(err)
322 cb(null, Buffer.concat(bufs, obj.length).toString('utf8'))
323 }))
324}
325
326function getRepoObjectString(repo, id, cb) {
327 if (!id) return cb(null, '')
328 repo.getObjectFromAny(id, function (err, obj) {
329 if (err) return cb(err)
330 readObjectString(obj, cb)
331 })
332}
333
334function compareMsgs(a, b) {
335 return (a.value.timestamp - b.value.timestamp) || (a.key - b.key)
336}
337
338function pullSort(comparator) {
339 return function (read) {
340 return readNext(function (cb) {
341 pull(read, pull.collect(function (err, items) {
342 if (err) return cb(err)
343 items.sort(comparator)
344 cb(null, pull.values(items))
345 }))
346 })
347 }
348}
349
350function sortMsgs() {
351 return pullSort(compareMsgs)
352}
353
354function pullReverse() {
355 return function (read) {
356 return readNext(function (cb) {
357 pull(read, pull.collect(function (err, items) {
358 cb(err, items && pull.values(items.reverse()))
359 }))
360 })
361 }
362}
363
364function tryDecodeURIComponent(str) {
365 if (!str || (str[0] == '%' && ref.isBlobId(str)))
366 return str
367 try {
368 str = decodeURIComponent(str)
369 } finally {
370 return str
371 }
372}
373
374function getRepoName(about, ownerId, repoId, cb) {
375 about.getName({
376 owner: ownerId,
377 target: repoId,
378 toString: function () {
379 // hack to fit two parameters into asyncmemo
380 return ownerId + '/' + repoId
381 }
382 }, cb)
383}
384
385function getRepoFullName(about, author, repoId, cb) {
386 var done = multicb({ pluck: 1, spread: true })
387 getRepoName(about, author, repoId, done())
388 about.getName(author, done())
389 done(cb)
390}
391
392function addAuthorName(about) {
393 return paramap(function (msg, cb) {
394 var author = msg && msg.value && msg.value.author
395 if (!author) return cb(null, msg)
396 about.getName(author, function (err, authorName) {
397 msg.authorName = authorName
398 cb(err, msg)
399 })
400 }, 8)
401}
402
403function getMention(msg, id) {
404 if (msg.key == id) return msg
405 var mentions = msg.value.content.mentions
406 if (mentions) for (var i = 0; i < mentions.length; i++) {
407 var mention = mentions[i]
408 if (mention.link == id)
409 return mention
410 }
411 return null
412}
413
414var hasOwnProp = Object.prototype.hasOwnProperty
415
416function getContentType(filename) {
417 var ext = getExtension(filename)
418 return contentTypes[ext] || imgMimes[ext] || 'text/plain; charset=utf-8'
419}
420
421var contentTypes = {
422 css: 'text/css'
423}
424
425function readReqForm(req, cb) {
426 pull(
427 toPull(req),
428 pull.collect(function (err, bufs) {
429 if (err) return cb(err)
430 var data
431 try {
432 data = qs.parse(Buffer.concat(bufs).toString('ascii'))
433 } catch(e) {
434 return cb(e)
435 }
436 cb(null, data)
437 })
438 )
439}
440
441var issueCommentScript = '(' + function () {
442 var $ = document.getElementById.bind(document)
443 $('tab-links').style.display = 'block'
444 $('preview-tab-link').onclick = function (e) {
445 with (new XMLHttpRequest()) {
446 open('POST', '', true)
447 onload = function() {
448 $('preview-tab').innerHTML = responseText
449 }
450 send('action=markdown' +
451 '&repo=' + encodeURIComponent($('repo-id').value) +
452 '&text=' + encodeURIComponent($('post-text').value))
453 }
454 }
455}.toString() + ')()'
456
457var msgTypes = {
458 'git-repo': true,
459 'git-update': true,
460 'issue': true,
461 'pull-request': true
462}
463
464var imgMimes = {
465 png: 'image/png',
466 jpeg: 'image/jpeg',
467 jpg: 'image/jpeg',
468 gif: 'image/gif',
469 tif: 'image/tiff',
470 svg: 'image/svg+xml',
471 bmp: 'image/bmp'
472}
473
474module.exports = function (opts, cb) {
475 var ssb, reconnect, myId, getRepo, getVotes, getMsg, issues
476 var about = function (id, cb) { cb(null, {name: id}) }
477 var reqQueue = []
478 var isPublic = opts.public
479 var ssbAppname = opts.appname || 'ssb'
480
481 var addr = parseAddr(opts.listenAddr, {host: 'localhost', port: 7718})
482 http.createServer(onRequest).listen(addr.port, addr.host, onListening)
483
484 var server = {
485 setSSB: function (_ssb, _reconnect) {
486 _ssb.whoami(function (err, feed) {
487 if (err) throw err
488 ssb = _ssb
489 reconnect = _reconnect
490 myId = feed.id
491 about = ssbAbout(ssb, myId)
492 while (reqQueue.length)
493 onRequest.apply(this, reqQueue.shift())
494 getRepo = asyncMemo(function (id, cb) {
495 getMsg(id, function (err, msg) {
496 if (err) return cb(err)
497 ssbGit.getRepo(ssb, {key: id, value: msg}, {live: true}, cb)
498 })
499 })
500 getVotes = ssbVotes(ssb)
501 getMsg = asyncMemo(ssb.get)
502 issues = Issues.init(ssb)
503 pullReqs = PullRequests.init(ssb)
504 })
505 }
506 }
507
508 function onListening() {
509 var host = ~addr.host.indexOf(':') ? '[' + addr.host + ']' : addr.host
510 console.log('Listening on http://' + host + ':' + addr.port + '/')
511 cb(null, server)
512 }
513
514 /* Serving a request */
515
516 function onRequest(req, res) {
517 console.log(req.method, req.url)
518 if (!ssb) return reqQueue.push(arguments)
519 req._u = url.parse(req.url, true)
520 var locale = req._u.query.locale ||
521 (/locale=([^;]*)/.exec(req.headers.cookie) || [])[1]
522 var reqLocales = req.headers['accept-language']
523 i18n.pickCatalog(reqLocales, locale, function (err, t) {
524 if (err) return pull(serveError(req, err, 500), serve(req, res))
525 req._t = t
526 req._locale = t.locale
527 pull(handleRequest(req), serve(req, res))
528 })
529 }
530
531 function serve(req, res) {
532 return pull(
533 pull.filter(function (data) {
534 if (Array.isArray(data)) {
535 res.writeHead.apply(res, data)
536 return false
537 }
538 return true
539 }),
540 toPull(res)
541 )
542 }
543
544 function handleRequest(req) {
545 var path = req._u.pathname.slice(1)
546 var dirs = ref.isLink(path) ? [path] :
547 path.split(/\/+/).map(tryDecodeURIComponent)
548 var dir = dirs[0]
549
550 if (req.method == 'POST') {
551 if (isPublic)
552 return serveBuffer(405, req._t('error.POSTNotAllowed'))
553 return readNext(function (cb) {
554 readReqForm(req, function (err, data) {
555 if (err) return cb(null, serveError(req, err, 400))
556 if (!data) return cb(null, serveError(req,
557 new ParamError(req._t('error.MissingData')), 400))
558
559 switch (data.action) {
560 case 'fork-prompt':
561 return cb(null, serveRedirect(req,
562 encodeLink([data.id, 'fork'])))
563
564 case 'fork':
565 if (!data.id)
566 return cb(null, serveError(req,
567 new ParamError(req._t('error.MissingId')), 400))
568 return ssbGit.createRepo(ssb, {upstream: data.id},
569 function (err, repo) {
570 if (err) return cb(null, serveError(req, err))
571 cb(null, serveRedirect(req, encodeLink(repo.id)))
572 })
573
574 case 'vote':
575 var voteValue = +data.value || 0
576 if (!data.id)
577 return cb(null, serveError(req,
578 new ParamError(req._t('error.MissingId')), 400))
579 var msg = schemas.vote(data.id, voteValue)
580 return ssb.publish(msg, function (err) {
581 if (err) return cb(null, serveError(req, err))
582 cb(null, serveRedirect(req, req.url))
583 })
584
585 case 'repo-name':
586 if (!data.id)
587 return cb(null, serveError(req,
588 new ParamError(req._t('error.MissingId')), 400))
589 if (!data.name)
590 return cb(null, serveError(req,
591 new ParamError(req._t('error.MissingName')), 400))
592 var msg = schemas.name(data.id, data.name)
593 return ssb.publish(msg, function (err) {
594 if (err) return cb(null, serveError(req, err))
595 cb(null, serveRedirect(req, req.url))
596 })
597
598 case 'issue-title':
599 if (!data.id)
600 return cb(null, serveError(req,
601 new ParamError(req._t('error.MissingId')), 400))
602 if (!data.name)
603 return cb(null, serveError(req,
604 new ParamError(req._t('error.MissingName')), 400))
605 var msg = Issues.schemas.edit(data.id, {title: data.name})
606 return ssb.publish(msg, function (err) {
607 if (err) return cb(null, serveError(req, err))
608 cb(null, serveRedirect(req, req.url))
609 })
610
611 case 'comment':
612 if (!data.id)
613 return cb(null, serveError(req,
614 new ParamError(req._t('error.MissingId')), 400))
615 var msg = schemas.post(data.text, data.id, data.branch || data.id)
616 msg.issue = data.issue
617 msg.repo = data.repo
618 if (data.open != null)
619 Issues.schemas.opens(msg, data.id)
620 if (data.close != null)
621 Issues.schemas.closes(msg, data.id)
622 var mentions = Mentions(data.text)
623 if (mentions.length)
624 msg.mentions = mentions
625 return ssb.publish(msg, function (err) {
626 if (err) return cb(null, serveError(req, err))
627 cb(null, serveRedirect(req, req.url))
628 })
629
630 case 'new-issue':
631 var msg = Issues.schemas.new(dir, data.title, data.text)
632 var mentions = Mentions(data.text)
633 if (mentions.length)
634 msg.mentions = mentions
635 return ssb.publish(msg, function (err, msg) {
636 if (err) return cb(null, serveError(req, err))
637 cb(null, serveRedirect(req, encodeLink(msg.key)))
638 })
639
640 case 'new-pull':
641 var msg = PullRequests.schemas.new(dir, data.branch,
642 data.head_repo, data.head_branch, data.title, data.text)
643 var mentions = Mentions(data.text)
644 if (mentions.length)
645 msg.mentions = mentions
646 return ssb.publish(msg, function (err, msg) {
647 if (err) return cb(null, serveError(req, err))
648 cb(null, serveRedirect(req, encodeLink(msg.key)))
649 })
650
651 case 'markdown':
652 return cb(null, serveMarkdown(data.text, {id: data.repo}))
653
654 default:
655 cb(null, serveBuffer(400, req._t('error.UnknownAction', data)))
656 }
657 })
658 })
659 }
660
661 if (dir == '')
662 return serveIndex(req)
663 else if (dir == 'search')
664 return serveSearch(req)
665 else if (ref.isBlobId(dir))
666 return serveBlob(req, dir)
667 else if (ref.isMsgId(dir))
668 return serveMessage(req, dir, dirs.slice(1))
669 else if (ref.isFeedId(dir))
670 return serveUserPage(req, dir, dirs.slice(1))
671 else if (dir == 'static')
672 return serveFile(req, dirs)
673 else if (dir == 'highlight')
674 return serveFile(req, [hlCssPath].concat(dirs.slice(1)), true)
675 else
676 return serve404(req)
677 }
678
679 function serveFile(req, dirs, outside) {
680 var filename = path.resolve.apply(path, [__dirname].concat(dirs))
681 // prevent escaping base dir
682 if (!outside && filename.indexOf('../') === 0)
683 return serveBuffer(403, req._t("error.403Forbidden"))
684
685 return readNext(function (cb) {
686 fs.stat(filename, function (err, stats) {
687 cb(null, err ?
688 err.code == 'ENOENT' ? serve404(req)
689 : serveBuffer(500, err.message)
690 : 'if-modified-since' in req.headers &&
691 new Date(req.headers['if-modified-since']) >= stats.mtime ?
692 pull.once([304])
693 : stats.isDirectory() ?
694 serveBuffer(403, req._t('error.DirectoryNotListable'))
695 : cat([
696 pull.once([200, {
697 'Content-Type': getContentType(filename),
698 'Content-Length': stats.size,
699 'Last-Modified': stats.mtime.toGMTString()
700 }]),
701 toPull(fs.createReadStream(filename))
702 ]))
703 })
704 })
705 }
706
707 function servePlainError(code, msg) {
708 return pull.values([
709 [code, {
710 'Content-Length': Buffer.byteLength(msg),
711 'Content-Type': 'text/plain; charset=utf-8'
712 }],
713 msg
714 ])
715 }
716
717 function serveBuffer(code, buf, contentType, headers) {
718 headers = headers || {}
719 headers['Content-Type'] = contentType || 'text/plain; charset=utf-8'
720 headers['Content-Length'] = Buffer.byteLength(buf)
721 return pull.values([
722 [code, headers],
723 buf
724 ])
725 }
726
727 function serve404(req) {
728 return serveBuffer(404, req._t("error.404NotFound"))
729 }
730
731 function serveRedirect(req, path) {
732 return serveBuffer(302,
733 '<!doctype><html><head>' +
734 '<title>' + req._t('Redirect') + '</title></head><body>' +
735 '<p><a href="' + escapeHTML(path) + '">' +
736 req._t('Continue') + '</a></p>' +
737 '</body></html>', 'text/html; charset=utf-8', {Location: path})
738 }
739
740 function serveMarkdown(text, repo) {
741 return serveBuffer(200, markdown(text, repo), 'text/html; charset=utf-8')
742 }
743
744 function renderError(err, tag) {
745 tag = tag || 'h3'
746 return '<' + tag + '>' + err.name + '</' + tag + '>' +
747 '<pre>' + escapeHTML(err.stack) + '</pre>'
748 }
749
750 function renderTry(read) {
751 var ended
752 return function (end, cb) {
753 if (ended) return cb(ended)
754 read(end, function (err, data) {
755 if (err === true)
756 cb(true)
757 else if (err) {
758 ended = true
759 cb(null, renderError(err, 'h3'))
760 } else
761 cb(null, data)
762 })
763 }
764 }
765
766 function serveTemplate(req, title, code, read) {
767 if (read === undefined) return serveTemplate.bind(this, req, title, code)
768 var q = req._u.query.q && escapeHTML(req._u.query.q) || ''
769 var app = 'git ssb'
770 if (req._t) app = req._t(app)
771 return cat([
772 pull.values([
773 [code || 200, {
774 'Content-Type': 'text/html'
775 }],
776 '<!doctype html><html><head><meta charset=utf-8>',
777 '<title>' + (title || app) + '</title>',
778 '<link rel=stylesheet href="/static/styles.css"/>',
779 '<link rel=stylesheet href="/highlight/github.css"/>',
780 '</head>\n',
781 '<body>',
782 '<header><form action="/search" method="get">' +
783 '<h1><a href="/">' + app + '' +
784 (ssbAppname != 'ssb' ? ' <sub>' + ssbAppname + '</sub>' : '') +
785 '</a> ' +
786 '<input class="search-bar" name="q" size="60"' +
787 ' placeholder="๐Ÿ”" value="' + q + '" />' +
788 '</h1>',
789 '</form></header>',
790 '<article>']),
791 renderTry(read),
792 pull.once('<hr/></article></body></html>')
793 ])
794 }
795
796 function serveError(req, err, status) {
797 if (err.message == 'stream is closed')
798 reconnect()
799 return pull(
800 pull.once(renderError(err, 'h2')),
801 serveTemplate(req, err.name, status || 500)
802 )
803 }
804
805 function renderObjectData(obj, filename, repo, rev, path) {
806 var ext = getExtension(filename)
807 return readOnce(function (cb) {
808 readObjectString(obj, function (err, buf) {
809 buf = buf.toString('utf8')
810 if (err) return cb(err)
811 cb(null, (ext == 'md' || ext == 'markdown')
812 ? markdown(buf, {repo: repo, rev: rev, path: path})
813 : renderCodeTable(buf, ext))
814 })
815 })
816 }
817
818 function renderCodeTable(buf, ext) {
819 return '<pre><table class="code">' +
820 highlight(buf, ext).split('\n').map(function (line, i) {
821 i++
822 return '<tr id="L' + i + '">' +
823 '<td class="code-linenum">' + '<a href="#L' + i + '">' + i + '</td>' +
824 '<td class="code-text">' + line + '</td></tr>'
825 }).join('') +
826 '</table></pre>'
827 }
828
829 /* Feed */
830
831 function renderFeed(req, feedId, filter) {
832 var query = req._u.query
833 var opts = {
834 reverse: !query.forwards,
835 lt: query.lt && +query.lt || Date.now(),
836 gt: query.gt && +query.gt,
837 id: feedId
838 }
839 return pull(
840 feedId ? ssb.createUserStream(opts) : ssb.createFeedStream(opts),
841 pull.filter(function (msg) {
842 return msg.value.content.type in msgTypes
843 }),
844 typeof filter == 'function' ? filter(opts) : filter,
845 pull.take(20),
846 addAuthorName(about),
847 query.forwards && pullReverse(),
848 paginate(
849 function (first, cb) {
850 if (!query.lt && !query.gt) return cb(null, '')
851 var gt = feedId ? first.value.sequence : first.value.timestamp + 1
852 query.gt = gt
853 query.forwards = 1
854 delete query.lt
855 cb(null, '<a href="?' + qs.stringify(query) + '">' +
856 req._t('Newer') + '</a>')
857 },
858 paramap(renderFeedItem.bind(null, req), 8),
859 function (last, cb) {
860 query.lt = feedId ? last.value.sequence : last.value.timestamp - 1
861 delete query.gt
862 delete query.forwards
863 cb(null, '<a href="?' + qs.stringify(query) + '">' +
864 req._t('Older') + '</a>')
865 },
866 function (cb) {
867 if (query.forwards) {
868 delete query.gt
869 delete query.forwards
870 query.lt = opts.gt + 1
871 } else {
872 delete query.lt
873 query.gt = opts.lt - 1
874 query.forwards = 1
875 }
876 cb(null, '<a href="?' + qs.stringify(query) + '">' +
877 req._t(query.forwards ? 'Older' : 'Newer') + '</a>')
878 }
879 )
880 )
881 }
882
883 function renderFeedItem(req, msg, cb) {
884 var c = msg.value.content
885 var msgLink = link([msg.key],
886 new Date(msg.value.timestamp).toLocaleString(req._locale))
887 var author = msg.value.author
888 var authorLink = link([msg.value.author], msg.authorName)
889 switch (c.type) {
890 case 'git-repo':
891 var done = multicb({ pluck: 1, spread: true })
892 getRepoName(about, author, msg.key, done())
893 if (c.upstream) {
894 return getMsg(c.upstream, function (err, upstreamMsg) {
895 if (err) return cb(null, serveError(req, err))
896 getRepoName(about, upstreamMsg.author, c.upstream, done())
897 done(function (err, repoName, upstreamName) {
898 cb(null, '<section class="collapse">' + msgLink + '<br>' +
899 req._t('Forked', {
900 name: authorLink,
901 upstream: link([c.upstream], upstreamName),
902 repo: link([msg.key], repoName)
903 }) + '</section>')
904 })
905 })
906 } else {
907 return done(function (err, repoName) {
908 if (err) return cb(err)
909 var repoLink = link([msg.key], repoName)
910 cb(null, '<section class="collapse">' + msgLink + '<br>' +
911 req._t('CreatedRepo', {
912 name: authorLink,
913 repo: repoLink
914 }) + '</section>')
915 })
916 }
917 case 'git-update':
918 return getRepoName(about, author, c.repo, function (err, repoName) {
919 if (err) return cb(err)
920 var repoLink = link([c.repo], repoName)
921 cb(null, '<section class="collapse">' + msgLink + '<br>' +
922 req._t('Pushed', {
923 name: authorLink,
924 repo: repoLink
925 }) + '</section>')
926 })
927 case 'issue':
928 case 'pull-request':
929 var issueLink = link([msg.key], c.title)
930 return getMsg(c.project, function (err, projectMsg) {
931 if (err) return cb(null, serveRepoNotFound(req, c.repo, err))
932 getRepoName(about, projectMsg.author, c.project,
933 function (err, repoName) {
934 if (err) return cb(err)
935 var repoLink = link([c.project], repoName)
936 cb(null, '<section class="collapse">' + msgLink + '<br>' +
937 req._t('OpenedIssue', {
938 name: authorLink,
939 type: req._t(c.type == 'pull-request' ?
940 'pull request' : 'issue.'),
941 title: issueLink,
942 project: repoLink
943 }) + '</section>')
944 })
945 })
946 case 'about':
947 return cb(null, '<section class="collapse">' + msgLink + '<br>' +
948 req._t('Named', {
949 author: authorLink,
950 target: '<tt>' + escapeHTML(c.about) + '</tt>',
951 name: link([c.about], c.name)
952 }) + '</section>')
953 default:
954 return cb(null, json(msg))
955 }
956 }
957
958 /* Index */
959
960 function serveIndex(req) {
961 return serveTemplate(req)(renderFeed(req))
962 }
963
964 function serveUserPage(req, feedId, dirs) {
965 switch (dirs[0]) {
966 case undefined:
967 case '':
968 case 'activity':
969 return serveUserActivity(req, feedId)
970 case 'repos':
971 return serveUserRepos(req, feedId)
972 }
973 }
974
975 function renderUserPage(req, feedId, page, body) {
976 return serveTemplate(req, feedId)(cat([
977 readOnce(function (cb) {
978 about.getName(feedId, function (err, name) {
979 cb(null, '<h2>' + link([feedId], name) +
980 '<code class="user-id">' + feedId + '</code></h2>' +
981 nav([
982 [[feedId], req._t('Activity'), 'activity'],
983 [[feedId, 'repos'], req._t('Repos'), 'repos']
984 ], page))
985 })
986 }),
987 body,
988 ]))
989 }
990
991 function serveUserActivity(req, feedId) {
992 return renderUserPage(req, feedId, 'activity', renderFeed(req, feedId))
993 }
994
995 function serveUserRepos(req, feedId) {
996 return renderUserPage(req, feedId, 'repos', pull(
997 ssb.messagesByType({
998 type: 'git-repo',
999 reverse: true
1000 }),
1001 pull.filter(function (msg) {
1002 return msg.value.author == feedId
1003 }),
1004 pull.take(20),
1005 paramap(function (msg, cb) {
1006 var done = multicb({ pluck: 1, spread: true })
1007 getRepoName(about, feedId, msg.key, done())
1008 getVotes(msg.key, done())
1009 done(function (err, repoName, votes) {
1010 if (err) return cb(err)
1011 cb(null, '<section class="collapse">' +
1012 '<span class="right-bar">' +
1013 '<i>โœŒ</i> ' +
1014 link([msg.key, 'digs'], votes.upvotes, true,
1015 ' title="' + req._t('Digs') + '"') +
1016 '</span>' +
1017 link([msg.key], repoName) +
1018 '</section>')
1019 })
1020 }, 8)
1021 ))
1022 }
1023
1024 /* Message */
1025
1026 function serveMessage(req, id, path) {
1027 return readNext(function (cb) {
1028 ssb.get(id, function (err, msg) {
1029 if (err) return cb(null, serveError(req, err))
1030 var c = msg.content || {}
1031 switch (c.type) {
1032 case 'git-repo':
1033 return getRepo(id, function (err, repo) {
1034 if (err) return cb(null, serveError(req, err))
1035 cb(null, serveRepoPage(req, Repo(repo), path))
1036 })
1037 case 'git-update':
1038 return getRepo(c.repo, function (err, repo) {
1039 if (err) return cb(null, serveRepoNotFound(req, c.repo, err))
1040 cb(null, serveRepoUpdate(req, Repo(repo), id, msg, path))
1041 })
1042 case 'issue':
1043 return getRepo(c.project, function (err, repo) {
1044 if (err) return cb(null, serveRepoNotFound(req, c.project, err))
1045 issues.get(id, function (err, issue) {
1046 if (err) return cb(null, serveError(req, err))
1047 cb(null, serveRepoIssue(req, Repo(repo), issue, path))
1048 })
1049 })
1050 case 'pull-request':
1051 return getRepo(c.repo, function (err, repo) {
1052 if (err) return cb(null, serveRepoNotFound(req, c.project, err))
1053 pullReqs.get(id, function (err, pr) {
1054 if (err) return cb(null, serveError(req, err))
1055 cb(null, serveRepoPullReq(req, Repo(repo), pr, path))
1056 })
1057 })
1058 case 'issue-edit':
1059 if (ref.isMsgId(c.issue)) {
1060 return pullReqs.get(c.issue, function (err, issue) {
1061 if (err) return cb(err)
1062 var serve = issue.msg.value.content.type == 'pull-request' ?
1063 serveRepoPullReq : serveRepoIssue
1064 getRepo(issue.project, function (err, repo) {
1065 if (err) {
1066 if (!repo) return cb(null,
1067 serveRepoNotFound(req, c.repo, err))
1068 return cb(null, serveError(req, err))
1069 }
1070 cb(null, serve(req, Repo(repo), issue, path, id))
1071 })
1072 })
1073 }
1074 // fallthrough
1075 case 'post':
1076 if (ref.isMsgId(c.issue) && ref.isMsgId(c.repo)) {
1077 // comment on an issue
1078 var done = multicb({ pluck: 1, spread: true })
1079 getRepo(c.repo, done())
1080 pullReqs.get(c.issue, done())
1081 return done(function (err, repo, issue) {
1082 if (err) {
1083 if (!repo) return cb(null,
1084 serveRepoNotFound(req, c.repo, err))
1085 return cb(null, serveError(req, err))
1086 }
1087 var serve = issue.msg.value.content.type == 'pull-request' ?
1088 serveRepoPullReq : serveRepoIssue
1089 cb(null, serve(req, Repo(repo), issue, path, id))
1090 })
1091 } else if (ref.isMsgId(c.root)) {
1092 // comment on issue from patchwork?
1093 return getMsg(c.root, function (err, root) {
1094 if (err) return cb(null, serveError(req, err))
1095 var repoId = root.content.repo || root.content.project
1096 if (!ref.isMsgId(repoId))
1097 return cb(null, serveGenericMessage(req, id, msg, path))
1098 getRepo(repoId, function (err, repo) {
1099 if (err) return cb(null, serveError(req, err))
1100 switch (root.content && root.content.type) {
1101 case 'issue':
1102 return issues.get(c.root, function (err, issue) {
1103 if (err) return cb(null, serveError(req, err))
1104 return cb(null,
1105 serveRepoIssue(req, Repo(repo), issue, path, id))
1106 })
1107 case 'pull-request':
1108 pullReqs.get(c.root, function (err, pr) {
1109 if (err) return cb(null, serveError(req, err))
1110 return cb(null,
1111 serveRepoPullReq(req, Repo(repo), pr, path, id))
1112 })
1113 }
1114 })
1115 })
1116 }
1117 // fallthrough
1118 default:
1119 if (ref.isMsgId(c.repo))
1120 return getRepo(c.repo, function (err, repo) {
1121 if (err) return cb(null, serveRepoNotFound(req, c.repo, err))
1122 cb(null, serveRepoSomething(req, Repo(repo), id, msg, path))
1123 })
1124 else
1125 return cb(null, serveGenericMessage(req, id, msg, path))
1126 }
1127 })
1128 })
1129 }
1130
1131 function serveGenericMessage(req, id, msg, path) {
1132 return serveTemplate(req, id)(pull.once(
1133 '<section><h2>' + link([id]) + '</h2>' +
1134 json(msg) +
1135 '</section>'))
1136 }
1137
1138 /* Repo */
1139
1140 function serveRepoPage(req, repo, path) {
1141 var defaultBranch = 'master'
1142 var query = req._u.query
1143
1144 if (query.rev != null) {
1145 // Allow navigating revs using GET query param.
1146 // Replace the branch in the path with the rev query value
1147 path[0] = path[0] || 'tree'
1148 path[1] = query.rev
1149 req._u.pathname = encodeLink([repo.id].concat(path))
1150 delete req._u.query.rev
1151 delete req._u.search
1152 return serveRedirect(req, url.format(req._u))
1153 }
1154
1155 // get branch
1156 return path[1] ?
1157 serveRepoPage2(req, repo, path) :
1158 readNext(function (cb) {
1159 // TODO: handle this in pull-git-repo or ssb-git-repo
1160 repo.getSymRef('HEAD', true, function (err, ref) {
1161 if (err) return cb(err)
1162 repo.resolveRef(ref, function (err, rev) {
1163 path[1] = rev ? ref : null
1164 cb(null, serveRepoPage2(req, repo, path))
1165 })
1166 })
1167 })
1168 }
1169
1170 function serveRepoPage2(req, repo, path) {
1171 var branch = path[1]
1172 var filePath = path.slice(2)
1173 switch (path[0]) {
1174 case undefined:
1175 case '':
1176 return serveRepoTree(req, repo, branch, [])
1177 case 'activity':
1178 return serveRepoActivity(req, repo, branch)
1179 case 'commits':
1180 return serveRepoCommits(req, repo, branch)
1181 case 'commit':
1182 return serveRepoCommit(req, repo, path[1])
1183 case 'tree':
1184 return serveRepoTree(req, repo, branch, filePath)
1185 case 'blob':
1186 return serveRepoBlob(req, repo, branch, filePath)
1187 case 'raw':
1188 return serveRepoRaw(req, repo, branch, filePath)
1189 case 'digs':
1190 return serveRepoDigs(req, repo)
1191 case 'fork':
1192 return serveRepoForkPrompt(req, repo)
1193 case 'forks':
1194 return serveRepoForks(req, repo)
1195 case 'issues':
1196 switch (path[1]) {
1197 case 'new':
1198 if (filePath.length == 0)
1199 return serveRepoNewIssue(req, repo)
1200 break
1201 default:
1202 return serveRepoIssues(req, repo, filePath)
1203 }
1204 case 'pulls':
1205 return serveRepoPullReqs(req, repo)
1206 case 'compare':
1207 return serveRepoCompare(req, repo)
1208 case 'comparing':
1209 return serveRepoComparing(req, repo)
1210 default:
1211 return serve404(req)
1212 }
1213 }
1214
1215 function serveRepoNotFound(req, id, err) {
1216 return serveTemplate(req, req._t('error.RepoNotFound'), 404)(pull.values([
1217 '<h2>' + req._t('error.RepoNotFound') + '</h2>',
1218 '<p>' + req._t('error.RepoNameNotFound') + '</p>',
1219 '<pre>' + escapeHTML(err.stack) + '</pre>'
1220 ]))
1221 }
1222
1223 function renderRepoPage(req, repo, page, branch, body) {
1224 var gitUrl = 'ssb://' + repo.id
1225 var gitLink = '<input class="clone-url" readonly="readonly" ' +
1226 'value="' + gitUrl + '" size="45" ' +
1227 'onclick="this.select()"/>'
1228 var digsPath = [repo.id, 'digs']
1229
1230 var done = multicb({ pluck: 1, spread: true })
1231 getRepoName(about, repo.feed, repo.id, done())
1232 about.getName(repo.feed, done())
1233 getVotes(repo.id, done())
1234
1235 if (repo.upstream) {
1236 getRepoName(about, repo.upstream.feed, repo.upstream.id, done())
1237 about.getName(repo.upstream.feed, done())
1238 }
1239
1240 return readNext(function (cb) {
1241 done(function (err, repoName, authorName, votes,
1242 upstreamName, upstreamAuthorName) {
1243 if (err) return cb(null, serveError(req, err))
1244 var upvoted = votes.upvoters[myId] > 0
1245 var upstreamLink = !repo.upstream ? '' :
1246 link([repo.upstream])
1247 cb(null, serveTemplate(req, repo.id)(cat([
1248 pull.once(
1249 '<div class="repo-title">' +
1250 '<form class="right-bar" action="" method="post">' +
1251 '<button class="btn" name="action" value="vote" ' +
1252 (isPublic ? 'disabled="disabled"' : ' type="submit"') + '>' +
1253 '<i>โœŒ</i> ' + req._t(!isPublic && upvoted ? 'Undig' : 'Dig') +
1254 '</button>' +
1255 (isPublic ? '' : '<input type="hidden" name="value" value="' +
1256 (upvoted ? '0' : '1') + '">' +
1257 '<input type="hidden" name="id" value="' +
1258 escapeHTML(repo.id) + '">') + ' ' +
1259 '<strong>' + link(digsPath, votes.upvotes) + '</strong> ' +
1260 (isPublic ? '' : '<button class="btn" type="submit" ' +
1261 ' name="action" value="fork-prompt">' +
1262 '<i>โ‘‚</i> ' + req._t('Fork') +
1263 '</button>') + ' ' +
1264 link([repo.id, 'forks'], '+', false, ' title="' +
1265 req._t('Forks') + '"') +
1266 '</form>' +
1267 renderNameForm(req, !isPublic, repo.id, repoName, 'repo-name',
1268 null, req._t('repo.Rename'),
1269 '<h2 class="bgslash">' + link([repo.feed], authorName) + ' / ' +
1270 link([repo.id], repoName) + '</h2>') +
1271 '</div>' +
1272 (repo.upstream ? '<small class="bgslash">' + req._t('ForkedFrom', {
1273 repo: link([repo.upstream.feed], upstreamAuthorName) + '/' +
1274 link([repo.upstream.id], upstreamName)
1275 }) + '</small>' : '') +
1276 nav([
1277 [[repo.id], req._t('Code'), 'code'],
1278 [[repo.id, 'activity'], req._t('Activity'), 'activity'],
1279 [[repo.id, 'commits', branch||''], req._t('Commits'), 'commits'],
1280 [[repo.id, 'issues'], req._t('Issues'), 'issues'],
1281 [[repo.id, 'pulls'], req._t('PullRequests'), 'pulls']
1282 ], page, gitLink)),
1283 body
1284 ])))
1285 })
1286 })
1287 }
1288
1289 function serveEmptyRepo(req, repo) {
1290 if (repo.feed != myId)
1291 return renderRepoPage(req, repo, 'code', null, pull.once(
1292 '<section>' +
1293 '<h3>' + req._t('EmptyRepo') + '</h3>' +
1294 '</section>'))
1295
1296 var gitUrl = 'ssb://' + repo.id
1297 return renderRepoPage(req, repo, 'code', null, pull.once(
1298 '<section>' +
1299 '<h3>' + req._t('initRepo.GettingStarted') + '</h3>' +
1300 '<h4>' + req._t('initRepo.CreateNew') + '</h4><pre>' +
1301 'touch ' + req._t('initRepo.README') + '.md\n' +
1302 'git init\n' +
1303 'git add ' + req._t('initRepo.README') + '.md\n' +
1304 'git commit -m "' + req._t('initRepo.InitialCommit') + '"\n' +
1305 'git remote add origin ' + gitUrl + '\n' +
1306 'git push -u origin master</pre>\n' +
1307 '<h4>' + req._t('initRepo.PushExisting') + '</h4>\n' +
1308 '<pre>git remote add origin ' + gitUrl + '\n' +
1309 'git push -u origin master</pre>' +
1310 '</section>'))
1311 }
1312
1313 function serveRepoTree(req, repo, rev, path) {
1314 if (!rev) return serveEmptyRepo(req, repo)
1315 var type = repo.isCommitHash(rev) ? 'Tree' : 'Branch'
1316 return renderRepoPage(req, repo, 'code', rev, cat([
1317 pull.once('<section><form action="" method="get">' +
1318 '<h3>' + req._t(type) + ': ' + rev + ' '),
1319 revMenu(req, repo, rev),
1320 pull.once('</h3></form>'),
1321 type == 'Branch' && renderRepoLatest(req, repo, rev),
1322 pull.once('</section><section>'),
1323 renderRepoTree(req, repo, rev, path),
1324 pull.once('</section>'),
1325 renderRepoReadme(req, repo, rev, path)
1326 ]))
1327 }
1328
1329 /* Search */
1330
1331 function serveSearch(req) {
1332 var q = String(req._u.query.q || '')
1333 if (!q) return serveIndex(req)
1334 var qId = q.replace(/^ssb:\/*/, '')
1335 if (ref.type(qId))
1336 return serveRedirect(req, encodeURI(qId))
1337
1338 var search = new RegExp(q, 'i')
1339 return serveTemplate(req, req._t('Search') + ' &middot; ' + q, 200)(
1340 renderFeed(req, null, function (opts) {
1341 opts.type == 'about'
1342 return function (read) {
1343 return pull(
1344 many([
1345 getRepoNames(opts),
1346 read
1347 ]),
1348 pull.filter(function (msg) {
1349 var c = msg.value.content
1350 return (
1351 search.test(msg.key) ||
1352 c.text && search.test(c.text) ||
1353 c.name && search.test(c.name) ||
1354 c.title && search.test(c.title))
1355 })
1356 )
1357 }
1358 })
1359 )
1360 }
1361
1362 function getRepoNames(opts) {
1363 return pull(
1364 ssb.messagesByType({
1365 type: 'about',
1366 reverse: opts.reverse,
1367 lt: opts.lt,
1368 gt: opts.gt,
1369 }),
1370 pull.filter(function (msg) {
1371 return '%' == String(msg.value.content.about)[0]
1372 && msg.value.content.name
1373 })
1374 )
1375 }
1376
1377 /* Repo activity */
1378
1379 function serveRepoActivity(req, repo, branch) {
1380 return renderRepoPage(req, repo, 'activity', branch, cat([
1381 pull.once('<h3>' + req._t('Activity') + '</h3>'),
1382 pull(
1383 ssb.links({
1384 dest: repo.id,
1385 source: repo.feed,
1386 rel: 'repo',
1387 values: true,
1388 reverse: true
1389 }),
1390 pull.map(renderRepoUpdate.bind(this, req, repo))
1391 ),
1392 readOnce(function (cb) {
1393 var done = multicb({ pluck: 1, spread: true })
1394 about.getName(repo.feed, done())
1395 getMsg(repo.id, done())
1396 done(function (err, authorName, msg) {
1397 if (err) return cb(err)
1398 renderFeedItem(req, {
1399 key: repo.id,
1400 value: msg,
1401 authorName: authorName
1402 }, cb)
1403 })
1404 })
1405 ]))
1406 }
1407
1408 function renderRepoUpdate(req, repo, msg, full) {
1409 var c = msg.value.content
1410
1411 if (c.type != 'git-update') {
1412 return ''
1413 // return renderFeedItem(msg, cb)
1414 // TODO: render post, issue, pull-request
1415 }
1416
1417 var refs = c.refs ? Object.keys(c.refs).map(function (ref) {
1418 return {name: ref, value: c.refs[ref]}
1419 }) : []
1420 var numObjects = c.objects ? Object.keys(c.objects).length : 0
1421
1422 var dateStr = new Date(msg.value.timestamp).toLocaleString(req._locale)
1423 return '<section class="collapse">' +
1424 link([msg.key], dateStr) + '<br>' +
1425 (numObjects ? req._t('PushedObjects', numObjects) + '<br/>': '') +
1426 refs.map(function (update) {
1427 var name = escapeHTML(update.name)
1428 if (!update.value) {
1429 return req._t('DeletedBranch', {branch: name})
1430 } else {
1431 var commitLink = link([repo.id, 'commit', update.value])
1432 return name + ' &rarr; ' + commitLink
1433 }
1434 }).join('<br>') +
1435 '</section>'
1436 }
1437
1438 /* Repo commits */
1439
1440 function serveRepoCommits(req, repo, branch) {
1441 var query = req._u.query
1442 return renderRepoPage(req, repo, 'commits', branch, cat([
1443 pull.once('<h3>' + req._t('Commits') + '</h3>'),
1444 pull(
1445 repo.readLog(query.start || branch),
1446 pull.take(20),
1447 paramap(repo.getCommitParsed.bind(repo), 8),
1448 paginate(
1449 !query.start ? '' : function (first, cb) {
1450 cb(null, '&hellip;')
1451 },
1452 pull.map(renderCommit.bind(this, req, repo)),
1453 function (commit, cb) {
1454 cb(null, commit.parents && commit.parents[0] ?
1455 '<a href="?start=' + commit.id + '">' +
1456 req._t('Older') + '</a>' : '')
1457 }
1458 )
1459 )
1460 ]))
1461 }
1462
1463 function renderCommit(req, repo, commit) {
1464 var commitPath = [repo.id, 'commit', commit.id]
1465 var treePath = [repo.id, 'tree', commit.id]
1466 return '<section class="collapse">' +
1467 '<strong>' + link(commitPath, commit.title) + '</strong><br>' +
1468 '<tt>' + commit.id + '</tt> ' +
1469 link(treePath, req._t('Tree')) + '<br>' +
1470 escapeHTML(commit.author.name) + ' &middot; ' +
1471 commit.author.date.toLocaleString(req._locale) +
1472 (commit.separateAuthor ? '<br>' + req._t('CommittedOn', {
1473 name: escapeHTML(commit.committer.name),
1474 date: commit.committer.date.toLocaleString(req._locale)
1475 }) : '') +
1476 '</section>'
1477 }
1478
1479 /* Branch menu */
1480
1481 function formatRevOptions(currentName) {
1482 return function (name) {
1483 var htmlName = escapeHTML(name)
1484 return '<option value="' + htmlName + '"' +
1485 (name == currentName ? ' selected="selected"' : '') +
1486 '>' + htmlName + '</option>'
1487 }
1488 }
1489
1490 function formatRevType(req, type) {
1491 return (
1492 type == 'heads' ? req._t('Branches') :
1493 type == 'tags' ? req._t('Tags') :
1494 type)
1495 }
1496
1497 function revMenu(req, repo, currentName) {
1498 return readOnce(function (cb) {
1499 repo.getRefNames(function (err, refs) {
1500 if (err) return cb(err)
1501 cb(null, '<select name="rev" onchange="this.form.submit()">' +
1502 Object.keys(refs).map(function (group) {
1503 return '<optgroup label="' + formatRevType(req, group) + '">' +
1504 refs[group].map(formatRevOptions(currentName)).join('') +
1505 '</optgroup>'
1506 }).join('') +
1507 '</select><noscript> ' +
1508 '<input type="submit" value="' + req._t('Go') + '"/></noscript>')
1509 })
1510 })
1511 }
1512
1513 function branchMenu(repo, name, currentName) {
1514 return cat([
1515 pull.once('<select name="' + name + '">'),
1516 pull(
1517 repo.refs(),
1518 pull.map(function (ref) {
1519 var m = ref.name.match(/^refs\/([^\/]*)\/(.*)$/) || [,, ref.name]
1520 return m[1] == 'heads' && m[2]
1521 }),
1522 pull.filter(Boolean),
1523 pullSort(),
1524 pull.map(formatRevOptions(currentName))
1525 ),
1526 pull.once('</select>')
1527 ])
1528 }
1529
1530 /* Repo tree */
1531
1532 function renderRepoLatest(req, repo, rev) {
1533 return readOnce(function (cb) {
1534 repo.getCommitParsed(rev, function (err, commit) {
1535 if (err) return cb(err)
1536 var commitPath = [repo.id, 'commit', commit.id]
1537 cb(null,
1538 req._t('Latest') + ': ' +
1539 '<strong>' + link(commitPath, commit.title) + '</strong><br/>' +
1540 '<tt>' + commit.id + '</tt><br/> ' +
1541 req._t('CommittedOn', {
1542 name: escapeHTML(commit.committer.name),
1543 date: commit.committer.date.toLocaleString(req._locale)
1544 }) +
1545 (commit.separateAuthor ? '<br/>' + req._t('AuthoredOn', {
1546 name: escapeHTML(commit.author.name),
1547 date: commit.author.date.toLocaleString(req._locale)
1548 }) : ''))
1549 })
1550 })
1551 }
1552
1553 // breadcrumbs
1554 function linkPath(basePath, path) {
1555 path = path.slice()
1556 var last = path.pop()
1557 return path.map(function (dir, i) {
1558 return link(basePath.concat(path.slice(0, i+1)), dir)
1559 }).concat(last).join(' / ')
1560 }
1561
1562 function renderRepoTree(req, repo, rev, path) {
1563 var pathLinks = path.length === 0 ? '' :
1564 ': ' + linkPath([repo.id, 'tree'], [rev].concat(path))
1565 return cat([
1566 pull.once('<h3>' + req._t('Files') + pathLinks + '</h3>'),
1567 pull(
1568 repo.readDir(rev, path),
1569 pull.map(function (file) {
1570 var type = (file.mode === 040000) ? 'tree' :
1571 (file.mode === 0160000) ? 'commit' : 'blob'
1572 if (type == 'commit')
1573 return [
1574 '<span title="' + req._t('gitCommitLink') + '">๐Ÿ–ˆ</span>',
1575 '<span title="' + escapeHTML(file.id) + '">' +
1576 escapeHTML(file.name) + '</span>']
1577 var filePath = [repo.id, type, rev].concat(path, file.name)
1578 return ['<i>' + (type == 'tree' ? '๐Ÿ“' : '๐Ÿ“„') + '</i>',
1579 link(filePath, file.name)]
1580 }),
1581 table('class="files"')
1582 )
1583 ])
1584 }
1585
1586 /* Repo readme */
1587
1588 function renderRepoReadme(req, repo, branch, path) {
1589 return readNext(function (cb) {
1590 pull(
1591 repo.readDir(branch, path),
1592 pull.filter(function (file) {
1593 return /readme(\.|$)/i.test(file.name)
1594 }),
1595 pull.take(1),
1596 pull.collect(function (err, files) {
1597 if (err) return cb(null, pull.empty())
1598 var file = files[0]
1599 if (!file)
1600 return cb(null, pull.once(path.length ? '' :
1601 '<p>' + req._t('NoReadme') + '</p>'))
1602 repo.getObjectFromAny(file.id, function (err, obj) {
1603 if (err) return cb(err)
1604 cb(null, cat([
1605 pull.once('<section><h4><a name="readme">' +
1606 escapeHTML(file.name) + '</a></h4><hr/>'),
1607 renderObjectData(obj, file.name, repo, branch, path),
1608 pull.once('</section>')
1609 ]))
1610 })
1611 })
1612 )
1613 })
1614 }
1615
1616 /* Repo commit */
1617
1618 function serveRepoCommit(req, repo, rev) {
1619 return renderRepoPage(req, repo, null, rev, cat([
1620 readNext(function (cb) {
1621 repo.getCommitParsed(rev, function (err, commit) {
1622 if (err) return cb(err)
1623 var commitPath = [repo.id, 'commit', commit.id]
1624 var treePath = [repo.id, 'tree', commit.id]
1625 cb(null, cat([pull.once(
1626 '<h3>' + link(commitPath,
1627 req._t('CommitRev', {rev: rev})) + '</h3>' +
1628 '<section class="collapse">' +
1629 '<div class="right-bar">' +
1630 link(treePath, req._t('BrowseFiles')) +
1631 '</div>' +
1632 '<h4>' + linkify(escapeHTML(commit.title)) + '</h4>' +
1633 (commit.body ? linkify(pre(commit.body)) : '') +
1634 (commit.separateAuthor ? req._t('AuthoredOn', {
1635 name: escapeHTML(commit.author.name),
1636 date: commit.author.date.toLocaleString(req._locale)
1637 }) + '<br/>' : '') +
1638 req._t('CommittedOn', {
1639 name: escapeHTML(commit.committer.name),
1640 date: commit.committer.date.toLocaleString(req._locale)
1641 }) + '<br/>' +
1642 commit.parents.map(function (id) {
1643 return req._t('Parent') + ': ' +
1644 link([repo.id, 'commit', id], id)
1645 }).join('<br>') +
1646 '</section>' +
1647 '<section><h3>' + req._t('FilesChanged') + '</h3>'),
1648 // TODO: show diff from all parents (merge commits)
1649 renderDiffStat(req, [repo, repo], [commit.parents[0], commit.id]),
1650 pull.once('</section>')
1651 ]))
1652 })
1653 })
1654 ]))
1655 }
1656
1657 /* Diff stat */
1658
1659 function renderDiffStat(req, repos, treeIds) {
1660 if (treeIds.length == 0) treeIds = [null]
1661 var id = treeIds[0]
1662 var lastI = treeIds.length - 1
1663 var oldTree = treeIds[0]
1664 var changedFiles = []
1665 return cat([
1666 pull(
1667 Repo.diffTrees(repos, treeIds, true),
1668 pull.map(function (item) {
1669 var filename = escapeHTML(item.filename = item.path.join('/'))
1670 var oldId = item.id && item.id[0]
1671 var newId = item.id && item.id[lastI]
1672 var oldMode = item.mode && item.mode[0]
1673 var newMode = item.mode && item.mode[lastI]
1674 var action =
1675 !oldId && newId ? req._t('action.added') :
1676 oldId && !newId ? req._t('action.deleted') :
1677 oldMode != newMode ? req._t('action.changedMode', {
1678 old: oldMode.toString(8),
1679 new: newMode.toString(8)
1680 }) : req._t('changed')
1681 if (item.id)
1682 changedFiles.push(item)
1683 var blobsPath = item.id[1]
1684 ? [repos[1].id, 'blob', treeIds[1]]
1685 : [repos[0].id, 'blob', treeIds[0]]
1686 var rawsPath = item.id[1]
1687 ? [repos[1].id, 'raw', treeIds[1]]
1688 : [repos[0].id, 'raw', treeIds[0]]
1689 item.blobPath = blobsPath.concat(item.path)
1690 item.rawPath = rawsPath.concat(item.path)
1691 var fileHref = item.id ?
1692 '#' + encodeURIComponent(item.path.join('/')) :
1693 encodeLink(item.blobPath)
1694 return ['<a href="' + fileHref + '">' + filename + '</a>', action]
1695 }),
1696 table()
1697 ),
1698 pull(
1699 pull.values(changedFiles),
1700 paramap(function (item, cb) {
1701 var extension = getExtension(item.filename)
1702 if (extension in imgMimes) {
1703 var filename = escapeHTML(item.filename)
1704 return cb(null,
1705 '<pre><table class="code">' +
1706 '<tr><th id="' + escapeHTML(item.filename) + '">' +
1707 filename + '</th></tr>' +
1708 '<tr><td><img src="' + encodeLink(item.rawPath) + '"' +
1709 ' alt="' + filename + '"/></td></tr>' +
1710 '</table></pre>')
1711 }
1712 var done = multicb({ pluck: 1, spread: true })
1713 getRepoObjectString(repos[0], item.id[0], done())
1714 getRepoObjectString(repos[1], item.id[lastI], done())
1715 done(function (err, strOld, strNew) {
1716 if (err) return cb(err)
1717 cb(null, htmlLineDiff(req, item.filename, item.filename,
1718 strOld, strNew,
1719 encodeLink(item.blobPath)))
1720 })
1721 }, 4)
1722 )
1723 ])
1724 }
1725
1726 function htmlLineDiff(req, filename, anchor, oldStr, newStr, blobHref) {
1727 var diff = JsDiff.structuredPatch('', '', oldStr, newStr)
1728 var groups = diff.hunks.map(function (hunk) {
1729 var oldLine = hunk.oldStart
1730 var newLine = hunk.newStart
1731 var header = '<tr class="diff-hunk-header"><td colspan=2></td><td>' +
1732 '@@ -' + oldLine + ',' + hunk.oldLines + ' ' +
1733 '+' + newLine + ',' + hunk.newLines + ' @@' +
1734 '</td></tr>'
1735 return [header].concat(hunk.lines.map(function (line) {
1736 var s = line[0]
1737 if (s == '\\') return
1738 var html = highlight(line, getExtension(filename))
1739 var trClass = s == '+' ? 'diff-new' : s == '-' ? 'diff-old' : ''
1740 var lineNums = [s == '+' ? '' : oldLine++, s == '-' ? '' : newLine++]
1741 var id = [filename].concat(lineNums).join('-')
1742 return '<tr id="' + escapeHTML(id) + '" class="' + trClass + '">' +
1743 lineNums.map(function (num) {
1744 return '<td class="code-linenum">' +
1745 (num ? '<a href="#' + encodeURIComponent(id) + '">' +
1746 num + '</a>' : '') + '</td>'
1747 }).join('') +
1748 '<td class="code-text">' + html + '</td></tr>'
1749 }))
1750 })
1751 return '<pre><table class="code">' +
1752 '<tr><th colspan=3 id="' + escapeHTML(anchor) + '">' + filename +
1753 '<span class="right-bar">' +
1754 '<a href="' + blobHref + '">' + req._t('View') + '</a> ' +
1755 '</span></th></tr>' +
1756 [].concat.apply([], groups).join('') +
1757 '</table></pre>'
1758 }
1759
1760 /* An unknown message linking to a repo */
1761
1762 function serveRepoSomething(req, repo, id, msg, path) {
1763 return renderRepoPage(req, repo, null, null,
1764 pull.once('<section><h3>' + link([id]) + '</h3>' +
1765 json(msg) + '</section>'))
1766 }
1767
1768 /* Repo update */
1769
1770 function objsArr(objs) {
1771 return Array.isArray(objs) ? objs :
1772 Object.keys(objs).map(function (sha1) {
1773 var obj = Object.create(objs[sha1])
1774 obj.sha1 = sha1
1775 return obj
1776 })
1777 }
1778
1779 function serveRepoUpdate(req, repo, id, msg, path) {
1780 var raw = req._u.query.raw != null
1781
1782 if (raw)
1783 return renderRepoPage(req, repo, 'activity', null, pull.once(
1784 '<a href="?" class="raw-link header-align">' +
1785 req._t('Info') + '</a>' +
1786 '<h3>' + req._t('Update') + '</h3>' +
1787 '<section class="collapse">' +
1788 json({key: id, value: msg}) + '</section>'))
1789
1790 // convert packs to old single-object style
1791 if (msg.content.indexes) {
1792 for (var i = 0; i < msg.content.indexes.length; i++) {
1793 msg.content.packs[i] = {
1794 pack: {link: msg.content.packs[i].link},
1795 idx: msg.content.indexes[i]
1796 }
1797 }
1798 }
1799
1800 return renderRepoPage(req, repo, 'activity', null, cat([
1801 pull.values([
1802 '<a href="?raw" class="raw-link header-align">' +
1803 req._t('Data') + '</a>',
1804 '<h3>' + req._t('Update') + '</h3>',
1805 renderRepoUpdate(req, repo, {key: id, value: msg}, true)
1806 ].concat(msg.content.objects ?
1807 ['<h3>' + req._t('Objects') + '</h3>'].concat(
1808 objsArr(msg.content.objects).map(renderObject.bind(null, req)))
1809 : [],
1810 msg.content.packs ? [
1811 '<h3>' + req._t('Packs') + '</h3>'
1812 ].concat(msg.content.packs.map(renderPack.bind(null, req)))
1813 : [])),
1814 msg.content.packs && cat([
1815 pull.once('<h3>' + req._t('Commits') + '</h3>'),
1816 pull(
1817 pull.values(msg.content.packs),
1818 pull.asyncMap(function (pack, cb) {
1819 var done = multicb({ pluck: 1, spread: true })
1820 getBlob(req, pack.pack.link, done())
1821 getBlob(req, pack.idx.link, done())
1822 done(function (err, readPack, readIdx) {
1823 if (err) return cb(renderError(err))
1824 cb(null, gitPack.decodeWithIndex(repo, readPack, readIdx))
1825 })
1826 }),
1827 pull.flatten(),
1828 pull.asyncMap(function (obj, cb) {
1829 if (obj.type == 'commit')
1830 Repo.getCommitParsed(obj, cb)
1831 else
1832 pull(obj.read, pull.drain(null, cb))
1833 }),
1834 pull.filter(),
1835 pull.map(function (commit) {
1836 return renderCommit(req, repo, commit)
1837 })
1838 )
1839 ])
1840 ]))
1841 }
1842
1843 function renderObject(req, obj) {
1844 return '<section class="collapse">' +
1845 obj.type + ' ' + link([obj.link], obj.sha1) + '<br>' +
1846 req._t('NumBytes', obj.length) +
1847 '</section>'
1848 }
1849
1850 function renderPack(req, info) {
1851 return '<section class="collapse">' +
1852 (info.pack ? req._t('Pack') + ': ' +
1853 link([info.pack.link]) + '<br>' : '') +
1854 (info.idx ? req._t('Index') + ': ' +
1855 link([info.idx.link]) : '') + '</section>'
1856 }
1857
1858 /* Blob */
1859
1860 function serveRepoBlob(req, repo, rev, path) {
1861 return readNext(function (cb) {
1862 repo.getFile(rev, path, function (err, object) {
1863 if (err) return cb(null, serveBlobNotFound(req, repo.id, err))
1864 var type = repo.isCommitHash(rev) ? 'Tree' : 'Branch'
1865 var pathLinks = path.length === 0 ? '' :
1866 ': ' + linkPath([repo.id, 'tree'], [rev].concat(path))
1867 var rawFilePath = [repo.id, 'raw', rev].concat(path)
1868 var dirPath = path.slice(0, path.length-1)
1869 var filename = path[path.length-1]
1870 var extension = getExtension(filename)
1871 cb(null, renderRepoPage(req, repo, 'code', rev, cat([
1872 pull.once('<section><form action="" method="get">' +
1873 '<h3>' + req._t(type) + ': ' + rev + ' '),
1874 revMenu(req, repo, rev),
1875 pull.once('</h3></form>'),
1876 type == 'Branch' && renderRepoLatest(req, repo, rev),
1877 pull.once('</section><section class="collapse">' +
1878 '<h3>' + req._t('Files') + pathLinks + '</h3>' +
1879 '<div>' + object.length + ' bytes' +
1880 '<span class="raw-link">' +
1881 link(rawFilePath, req._t('Raw')) + '</span>' +
1882 '</div></section>' +
1883 '<section>'),
1884 extension in imgMimes
1885 ? pull.once('<img src="' + encodeLink(rawFilePath) +
1886 '" alt="' + escapeHTML(filename) + '" />')
1887 : renderObjectData(object, filename, repo, rev, dirPath),
1888 pull.once('</section>')
1889 ])))
1890 })
1891 })
1892 }
1893
1894 function serveBlobNotFound(req, repoId, err) {
1895 return serveTemplate(req, req._t('error.BlobNotFound'), 404)(pull.once(
1896 '<h2>' + req._t('error.BlobNotFound') + '</h2>' +
1897 '<p>' + req._t('error.BlobNotFoundInRepo', {
1898 repo: link([repoId])
1899 }) + '</p>' +
1900 '<pre>' + escapeHTML(err.stack) + '</pre>'
1901 ))
1902 }
1903
1904 /* Raw blob */
1905
1906 function serveRepoRaw(req, repo, branch, path) {
1907 return readNext(function (cb) {
1908 repo.getFile(branch, path, function (err, object) {
1909 if (err) return cb(null,
1910 serveBuffer(404, req._t('error.BlobNotFound')))
1911 var extension = getExtension(path[path.length-1])
1912 var contentType = imgMimes[extension]
1913 cb(null, pull(object.read, serveRaw(object.length, contentType)))
1914 })
1915 })
1916 }
1917
1918 function serveRaw(length, contentType) {
1919 var headers = {
1920 'Content-Type': contentType || 'text/plain; charset=utf-8',
1921 'Cache-Control': 'max-age=31536000'
1922 }
1923 if (length != null)
1924 headers['Content-Length'] = length
1925 return function (read) {
1926 return cat([pull.once([200, headers]), read])
1927 }
1928 }
1929
1930 function getBlob(req, key, cb) {
1931 ssb.blobs.want(key, function (err, got) {
1932 if (err) cb(err)
1933 else if (!got) cb(new Error(req._t('error.MissingBlob', {key: key})))
1934 else cb(null, ssb.blobs.get(key))
1935 })
1936 }
1937
1938 function serveBlob(req, key) {
1939 return readNext(function (cb) {
1940 getBlob(req, key, function (err, read) {
1941 if (err) cb(null, serveError(req, err))
1942 else if (!read) cb(null, serve404(req))
1943 else cb(null, serveRaw()(read))
1944 })
1945 })
1946 }
1947
1948 /* Digs */
1949
1950 function serveRepoDigs(req, repo) {
1951 return readNext(function (cb) {
1952 getVotes(repo.id, function (err, votes) {
1953 cb(null, renderRepoPage(req, repo, null, null, cat([
1954 pull.once('<section><h3>' + req._t('Digs') + '</h3>' +
1955 '<div>' + req._t('Total') + ': ' + votes.upvotes + '</div>'),
1956 pull(
1957 pull.values(Object.keys(votes.upvoters)),
1958 paramap(function (feedId, cb) {
1959 about.getName(feedId, function (err, name) {
1960 if (err) return cb(err)
1961 cb(null, link([feedId], name))
1962 })
1963 }, 8),
1964 ul()
1965 ),
1966 pull.once('</section>')
1967 ])))
1968 })
1969 })
1970 }
1971
1972 /* Forks */
1973
1974 function getForks(repo, includeSelf) {
1975 return pull(
1976 cat([
1977 includeSelf && readOnce(function (cb) {
1978 getMsg(repo.id, function (err, value) {
1979 cb(err, value && {key: repo.id, value: value})
1980 })
1981 }),
1982 ssb.links({
1983 dest: repo.id,
1984 values: true,
1985 rel: 'upstream'
1986 })
1987 ]),
1988 pull.filter(function (msg) {
1989 return msg.value.content && msg.value.content.type == 'git-repo'
1990 }),
1991 paramap(function (msg, cb) {
1992 getRepoFullName(about, msg.value.author, msg.key,
1993 function (err, repoName, authorName) {
1994 if (err) return cb(err)
1995 cb(null, {
1996 key: msg.key,
1997 value: msg.value,
1998 repoName: repoName,
1999 authorName: authorName
2000 })
2001 })
2002 }, 8)
2003 )
2004 }
2005
2006 function serveRepoForks(req, repo) {
2007 var hasForks
2008 return renderRepoPage(req, repo, null, null, cat([
2009 pull.once('<h3>' + req._t('Forks') + '</h3>'),
2010 pull(
2011 getForks(repo),
2012 pull.map(function (msg) {
2013 hasForks = true
2014 return '<section class="collapse">' +
2015 link([msg.value.author], msg.authorName) + ' / ' +
2016 link([msg.key], msg.repoName) +
2017 '<span class="right-bar">' +
2018 timestamp(msg.value.timestamp, req) +
2019 '</span></section>'
2020 })
2021 ),
2022 readOnce(function (cb) {
2023 cb(null, hasForks ? '' : req._t('NoForks'))
2024 })
2025 ]))
2026 }
2027
2028 function serveRepoForkPrompt(req, repo) {
2029 return renderRepoPage(req, repo, null, null, pull.once(
2030 '<form action="" method="post" onreset="history.back()">' +
2031 '<h3>' + req._t('ForkRepoPrompt') + '</h3>' +
2032 '<p>' + hiddenInputs({ id: repo.id }) +
2033 '<button class="btn open" type="submit" name="action" value="fork">' +
2034 req._t('Fork') +
2035 '</button>' +
2036 ' <button class="btn" type="reset">' +
2037 req._t('Cancel') + '</button>' +
2038 '</p></form>'
2039 ))
2040 }
2041
2042 /* Issues */
2043
2044 function serveRepoIssues(req, repo, path) {
2045 var numIssues = 0
2046 var state = req._u.query.state || 'open'
2047 return renderRepoPage(req, repo, 'issues', null, cat([
2048 pull.once(
2049 (isPublic ? '' :
2050 '<form class="right-bar" method="get"' +
2051 ' action="' + encodeLink([repo.id, 'issues', 'new']) + '">' +
2052 '<button class="btn">&plus; ' + req._t('issue.New') + '</button>' +
2053 '</form>') +
2054 '<h3>' + req._t('Issues') + '</h3>' +
2055 nav([
2056 ['?', req._t('issues.Open'), 'open'],
2057 ['?state=closed', req._t('issues.Closed'), 'closed'],
2058 ['?state=all', req._t('issues.All'), 'all']
2059 ], state)),
2060 pull(
2061 issues.createFeedStream({ project: repo.id }),
2062 pull.filter(function (issue) {
2063 return state == 'all' ? true : (state == 'closed') == !issue.open
2064 }),
2065 pull.map(function (issue) {
2066 numIssues++
2067 var state = (issue.open ? 'open' : 'closed')
2068 var stateStr = req._t(issue.open ?
2069 'issue.state.Open' : 'issue.state.Closed')
2070 return '<section class="collapse">' +
2071 '<i class="issue-state issue-state-' + state + '"' +
2072 ' title="' + stateStr + '">โ—ผ</i> ' +
2073 '<a href="' + encodeLink(issue.id) + '">' +
2074 escapeHTML(issue.title) +
2075 '<span class="right-bar">' +
2076 new Date(issue.created_at).toLocaleString(req._locale) +
2077 '</span>' +
2078 '</a>' +
2079 '</section>'
2080 })
2081 ),
2082 readOnce(function (cb) {
2083 cb(null, numIssues > 0 ? '' : '<p>' + req._t('NoIssues') + '</p>')
2084 })
2085 ]))
2086 }
2087
2088 /* Pull Requests */
2089
2090 function serveRepoPullReqs(req, repo) {
2091 var count = 0
2092 var state = req._u.query.state || 'open'
2093 return renderRepoPage(req, repo, 'pulls', null, cat([
2094 pull.once(
2095 (isPublic ? '' :
2096 '<form class="right-bar" method="get"' +
2097 ' action="' + encodeLink([repo.id, 'compare']) + '">' +
2098 '<button class="btn">&plus; ' + req._t('pullRequest.New') +
2099 '</button>' +
2100 '</form>') +
2101 '<h3>' + req._t('PullRequests') + '</h3>' +
2102 nav([
2103 ['?', req._t('issues.Open'), 'open'],
2104 ['?state=closed', req._t('issues.Closed'), 'closed'],
2105 ['?state=all', req._t('issues.All'), 'all']
2106 ], state)),
2107 pull(
2108 pullReqs.list({
2109 repo: repo.id,
2110 open: {open: true, closed: false}[state]
2111 }),
2112 pull.map(function (issue) {
2113 count++
2114 var state = (issue.open ? 'open' : 'closed')
2115 var stateStr = req._t(issue.open ?
2116 'issue.state.Open' : 'issue.state.Closed')
2117 return '<section class="collapse">' +
2118 '<i class="issue-state issue-state-' + state + '"' +
2119 ' title="' + stateStr + '">โ—ผ</i> ' +
2120 '<a href="' + encodeLink(issue.id) + '">' +
2121 escapeHTML(issue.title) +
2122 '<span class="right-bar">' +
2123 new Date(issue.created_at).toLocaleString(req._locale) +
2124 '</span>' +
2125 '</a>' +
2126 '</section>'
2127 })
2128 ),
2129 readOnce(function (cb) {
2130 cb(null, count > 0 ? '' : '<p>' + req._t('NoPullRequests') + '</p>')
2131 })
2132 ]))
2133 }
2134
2135 /* New Issue */
2136
2137 function serveRepoNewIssue(req, repo, issueId, path) {
2138 return renderRepoPage(req, repo, 'issues', null, pull.once(
2139 '<h3>' + req._t('issue.New') + '</h3>' +
2140 '<section><form action="" method="post">' +
2141 '<input type="hidden" name="action" value="new-issue">' +
2142 '<p><input class="wide-input" name="title" placeholder="' +
2143 req._t('issue.Title') + '" size="77" /></p>' +
2144 renderPostForm(req, repo, req._t('Description'), 8) +
2145 '<button type="submit" class="btn">' + req._t('Create') + '</button>' +
2146 '</form></section>'))
2147 }
2148
2149 /* Issue */
2150
2151 function serveRepoIssue(req, repo, issue, path, postId) {
2152 var isAuthor = (myId == issue.author) || (myId == repo.feed)
2153 var newestMsg = {key: issue.id, value: {timestamp: issue.created_at}}
2154 return renderRepoPage(req, repo, 'issues', null, cat([
2155 pull.once(
2156 renderNameForm(req, !isPublic, issue.id, issue.title, 'issue-title',
2157 null, req._t('issue.Rename'),
2158 '<h3>' + link([issue.id], issue.title) + '</h3>') +
2159 '<code>' + issue.id + '</code>' +
2160 '<section class="collapse">' +
2161 (issue.open
2162 ? '<strong class="issue-status open">' +
2163 req._t('issue.state.Open') + '</strong>'
2164 : '<strong class="issue-status closed">' +
2165 req._t('issue.state.Closed') + '</strong>')),
2166 readOnce(function (cb) {
2167 about.getName(issue.author, function (err, authorName) {
2168 if (err) return cb(err)
2169 var authorLink = link([issue.author], authorName)
2170 cb(null, req._t('issue.Opened',
2171 {name: authorLink, datetime: timestamp(issue.created_at, req)}))
2172 })
2173 }),
2174 pull.once('<hr/>' + markdown(issue.text, repo) + '</section>'),
2175 // render posts and edits
2176 pull(
2177 ssb.links({
2178 dest: issue.id,
2179 values: true
2180 }),
2181 pull.unique('key'),
2182 addAuthorName(about),
2183 sortMsgs(),
2184 pull.through(function (msg) {
2185 if (msg.value.timestamp > newestMsg.value.timestamp)
2186 newestMsg = msg
2187 }),
2188 pull.map(renderIssueActivityMsg.bind(null, req, repo, issue,
2189 req._t('issue.'), postId))
2190 ),
2191 isPublic ? pull.empty() : readOnce(function (cb) {
2192 cb(null, renderIssueCommentForm(req, issue, repo, newestMsg.key,
2193 isAuthor, req._t('issue.')))
2194 })
2195 ]))
2196 }
2197
2198 function renderIssueActivityMsg(req, repo, issue, type, postId, msg) {
2199 var authorLink = link([msg.value.author], msg.authorName)
2200 var msgHref = encodeLink(msg.key) + '#' + encodeURIComponent(msg.key)
2201 var msgTimeLink = '<a href="' + msgHref + '"' +
2202 ' name="' + escapeHTML(msg.key) + '">' +
2203 new Date(msg.value.timestamp).toLocaleString(req._locale) + '</a>'
2204 var c = msg.value.content
2205 switch (c.type) {
2206 case 'post':
2207 if (c.root == issue.id) {
2208 var changed = issues.isStatusChanged(msg, issue)
2209 return '<section class="collapse">' +
2210 (msg.key == postId ? '<div class="highlight">' : '') +
2211 '<tt class="right-bar item-id">' + msg.key + '</tt> ' +
2212 (changed == null ? authorLink : req._t(
2213 changed ? 'issue.Reopened' : 'issue.Closed',
2214 {name: authorLink, type: type})) +
2215 ' &middot; ' + msgTimeLink +
2216 (msg.key == postId ? '</div>' : '') +
2217 markdown(c.text, repo) +
2218 '</section>'
2219 } else {
2220 var text = c.text || (c.type + ' ' + msg.key)
2221 return '<section class="collapse mention-preview">' +
2222 req._t('issue.MentionedIn', {
2223 name: authorLink,
2224 type: type,
2225 post: '<a href="/' + msg.key + '#' + msg.key + '">' +
2226 String(text).substr(0, 140) + '</a>'
2227 }) + '</section>'
2228 }
2229 case 'issue':
2230 case 'pull-request':
2231 return '<section class="collapse mention-preview">' +
2232 req._t('issue.MentionedIn', {
2233 name: authorLink,
2234 type: type,
2235 post: link([msg.key], String(c.title || msg.key).substr(0, 140))
2236 }) + '</section>'
2237 case 'issue-edit':
2238 return '<section class="collapse">' +
2239 (msg.key == postId ? '<div class="highlight">' : '') +
2240 (c.title == null ? '' : req._t('issue.Renamed', {
2241 name: authorLink,
2242 type: type,
2243 name: '<q>' + escapeHTML(c.title) + '</q>'
2244 })) + ' &middot; ' + msgTimeLink +
2245 (msg.key == postId ? '</div>' : '') +
2246 '</section>'
2247 case 'git-update':
2248 var mention = issues.getMention(msg, issue)
2249 if (mention) {
2250 var commitLink = link([repo.id, 'commit', mention.object],
2251 mention.label || mention.object)
2252 return '<section class="collapse">' +
2253 req._t(mention.open ? 'issue.Reopened' : 'issue.Closed', {
2254 name: authorLink,
2255 type: type
2256 }) + ' &middot; ' + msgTimeLink + '<br/>' +
2257 commitLink +
2258 '</section>'
2259 } else if ((mention = getMention(msg, issue.id))) {
2260 var commitLink = link(mention.object ?
2261 [repo.id, 'commit', mention.object] : [msg.key],
2262 mention.label || mention.object || msg.key)
2263 return '<section class="collapse">' +
2264 req._t('issue.Mentioned', {
2265 name: authorLink,
2266 type: type
2267 }) + ' &middot; ' + msgTimeLink + '<br/>' +
2268 commitLink +
2269 '</section>'
2270 } else {
2271 // fallthrough
2272 }
2273
2274 default:
2275 return '<section class="collapse">' +
2276 authorLink +
2277 ' &middot; ' + msgTimeLink +
2278 json(c) +
2279 '</section>'
2280 }
2281 }
2282
2283 function renderIssueCommentForm(req, issue, repo, branch, isAuthor, type) {
2284 return '<section><form action="" method="post">' +
2285 '<input type="hidden" name="action" value="comment">' +
2286 '<input type="hidden" name="id" value="' + issue.id + '">' +
2287 '<input type="hidden" name="issue" value="' + issue.id + '">' +
2288 '<input type="hidden" name="repo" value="' + repo.id + '">' +
2289 '<input type="hidden" name="branch" value="' + branch + '">' +
2290 renderPostForm(req, repo) +
2291 '<input type="submit" class="btn open" value="' +
2292 req._t('issue.Comment') + '" />' +
2293 (isAuthor ?
2294 '<input type="submit" class="btn"' +
2295 ' name="' + (issue.open ? 'close' : 'open') + '"' +
2296 ' value="' + req._t(
2297 issue.open ? 'issue.Close' : 'issue.Reopen', {type: type}
2298 ) + '"/>' : '') +
2299 '</form></section>'
2300 }
2301
2302 /* Pull Request */
2303
2304 function serveRepoPullReq(req, repo, pr, path, postId) {
2305 var headRepo, authorLink
2306 var page = path[0] || 'activity'
2307 return renderRepoPage(req, repo, 'pulls', null, cat([
2308 pull.once('<div class="pull-request">' +
2309 renderNameForm(req, !isPublic, pr.id, pr.title, 'issue-title', null,
2310 req._t('pullRequest.Rename'),
2311 '<h3>' + link([pr.id], pr.title) + '</h3>') +
2312 '<code>' + pr.id + '</code>'),
2313 readOnce(function (cb) {
2314 var done = multicb({ pluck: 1, spread: true })
2315 var gotHeadRepo = done()
2316 about.getName(pr.author, done())
2317 var sameRepo = (pr.headRepo == pr.baseRepo)
2318 getRepo(pr.headRepo, function (err, headRepo) {
2319 if (err) return cb(err)
2320 getRepoName(about, headRepo.feed, headRepo.id, done())
2321 about.getName(headRepo.feed, done())
2322 gotHeadRepo(null, Repo(headRepo))
2323 })
2324
2325 done(function (err, _headRepo, issueAuthorName,
2326 headRepoName, headRepoAuthorName) {
2327 if (err) return cb(err)
2328 headRepo = _headRepo
2329 authorLink = link([pr.author], issueAuthorName)
2330 var repoLink = link([pr.headRepo], headRepoName)
2331 var headRepoAuthorLink = link([headRepo.feed], headRepoAuthorName)
2332 var headRepoLink = link([headRepo.id], headRepoName)
2333 var headBranchLink = link([headRepo.id, 'tree', pr.headBranch])
2334 var baseBranchLink = link([repo.id, 'tree', pr.baseBranch])
2335 cb(null, '<section class="collapse">' +
2336 '<strong class="issue-status ' +
2337 (pr.open ? 'open' : 'closed') + '">' +
2338 req._t(pr.open ? 'issue.state.Open' : 'issue.state.Closed') +
2339 '</strong> ' +
2340 req._t('pullRequest.WantToMerge', {
2341 name: authorLink,
2342 base: '<code>' + baseBranchLink + '</code>',
2343 head: (sameRepo ?
2344 '<code>' + headBranchLink + '</code>' :
2345 '<code class="bgslash">' +
2346 headRepoAuthorLink + ' / ' +
2347 headRepoLink + ' / ' +
2348 headBranchLink + '</code>')
2349 }) + '</section>')
2350 })
2351 }),
2352 pull.once(
2353 nav([
2354 [[pr.id], req._t('Discussion'), 'activity'],
2355 [[pr.id, 'commits'], req._t('Commits'), 'commits'],
2356 [[pr.id, 'files'], req._t('Files'), 'files']
2357 ], page)),
2358 readNext(function (cb) {
2359 if (page == 'commits') cb(null,
2360 renderPullReqCommits(req, pr, repo, headRepo))
2361 else if (page == 'files') cb(null,
2362 renderPullReqFiles(req, pr, repo, headRepo))
2363 else cb(null,
2364 renderPullReqActivity(req, pr, repo, headRepo, authorLink, postId))
2365 })
2366 ]))
2367 }
2368
2369 function renderPullReqCommits(req, pr, baseRepo, headRepo) {
2370 return cat([
2371 pull.once('<section>'),
2372 renderCommitLog(req, baseRepo, pr.baseBranch, headRepo, pr.headBranch),
2373 pull.once('</section>')
2374 ])
2375 }
2376
2377 function renderPullReqFiles(req, pr, baseRepo, headRepo) {
2378 return cat([
2379 pull.once('<section>'),
2380 renderDiffStat(req,
2381 [baseRepo, headRepo], [pr.baseBranch, pr.headBranch]),
2382 pull.once('</section>')
2383 ])
2384 }
2385
2386 function renderPullReqActivity(req, pr, repo, headRepo, authorLink, postId) {
2387 var msgTimeLink = link([pr.id],
2388 new Date(pr.created_at).toLocaleString(req._locale))
2389 var newestMsg = {key: pr.id, value: {timestamp: pr.created_at}}
2390 var isAuthor = (myId == pr.author) || (myId == repo.feed)
2391 return cat([
2392 readOnce(function (cb) {
2393 cb(null,
2394 '<section class="collapse">' +
2395 authorLink + ' &middot; ' + msgTimeLink +
2396 markdown(pr.text, repo) + '</section>')
2397 }),
2398 // render posts, edits, and updates
2399 pull(
2400 many([
2401 ssb.links({
2402 dest: pr.id,
2403 values: true
2404 }),
2405 readNext(function (cb) {
2406 cb(null, pull(
2407 ssb.links({
2408 dest: headRepo.id,
2409 source: headRepo.feed,
2410 rel: 'repo',
2411 values: true,
2412 reverse: true
2413 }),
2414 pull.take(function (link) {
2415 return link.value.timestamp > pr.created_at
2416 }),
2417 pull.filter(function (link) {
2418 return link.value.content.type == 'git-update'
2419 && ('refs/heads/' + pr.headBranch) in link.value.content.refs
2420 })
2421 ))
2422 })
2423 ]),
2424 addAuthorName(about),
2425 pull.unique('key'),
2426 pull.through(function (msg) {
2427 if (msg.value.timestamp > newestMsg.value.timestamp)
2428 newestMsg = msg
2429 }),
2430 sortMsgs(),
2431 pull.map(function (item) {
2432 if (item.value.content.type == 'git-update')
2433 return renderBranchUpdate(req, pr, item)
2434 return renderIssueActivityMsg(req, repo, pr,
2435 req._t('pull request'), postId, item)
2436 })
2437 ),
2438 !isPublic && isAuthor && pr.open && pull.once(
2439 '<section class="merge-instructions">' +
2440 '<input type="checkbox" class="toggle" id="merge-instructions"/>' +
2441 '<h4><label for="merge-instructions" class="toggle-link"><a>' +
2442 req._t('mergeInstructions.MergeViaCmdLine') +
2443 '</a></label></h4>' +
2444 '<div class="contents">' +
2445 '<p>' + req._t('mergeInstructions.CheckOut') + '</p>' +
2446 '<pre>' +
2447 'git fetch ssb://' + escapeHTML(pr.headRepo) + ' ' +
2448 escapeHTML(pr.headBranch) + '\n' +
2449 'git checkout -b ' + escapeHTML(pr.headBranch) + ' FETCH_HEAD' +
2450 '</pre>' +
2451 '<p>' + req._t('mergeInstructions.MergeAndPush') + '</p>' +
2452 '<pre>' +
2453 'git checkout ' + escapeHTML(pr.baseBranch) + '\n' +
2454 'git merge ' + escapeHTML(pr.headBranch) + '\n' +
2455 'git push ssb ' + escapeHTML(pr.baseBranch) +
2456 '</pre>' +
2457 '</div></section>'),
2458 !isPublic && readOnce(function (cb) {
2459 cb(null, renderIssueCommentForm(req, pr, repo, newestMsg.key, isAuthor,
2460 req._t('pull request')))
2461 })
2462 ])
2463 }
2464
2465 function renderBranchUpdate(req, pr, msg) {
2466 var authorLink = link([msg.value.author], msg.authorName)
2467 var msgLink = link([msg.key],
2468 new Date(msg.value.timestamp).toLocaleString(req._locale))
2469 var rev = msg.value.content.refs['refs/heads/' + pr.headBranch]
2470 if (!rev)
2471 return '<section class="collapse">' +
2472 req._t('NameDeletedBranch', {
2473 name: authorLink,
2474 branch: '<code>' + pr.headBranch + '</code>'
2475 }) + ' &middot; ' + msgLink +
2476 '</section>'
2477
2478 var revLink = link([pr.headRepo, 'commit', rev], rev.substr(0, 8))
2479 return '<section class="collapse">' +
2480 req._t('NameUpdatedBranch', {
2481 name: authorLink,
2482 rev: '<code>' + revLink + '</code>'
2483 }) + ' &middot; ' + msgLink +
2484 '</section>'
2485 }
2486
2487 /* Compare changes */
2488
2489 function serveRepoCompare(req, repo) {
2490 var query = req._u.query
2491 var base
2492 var count = 0
2493
2494 return renderRepoPage(req, repo, 'pulls', null, cat([
2495 pull.once('<h3>' + req._t('CompareChanges') + '</h3>' +
2496 '<form action="' + encodeLink(repo.id) + '/comparing" method="get">' +
2497 '<section>'),
2498 pull.once(req._t('BaseBranch') + ': '),
2499 readNext(function (cb) {
2500 if (query.base) gotBase(null, query.base)
2501 else repo.getSymRef('HEAD', true, gotBase)
2502 function gotBase(err, ref) {
2503 if (err) return cb(err)
2504 cb(null, branchMenu(repo, 'base', base = ref || 'HEAD'))
2505 }
2506 }),
2507 pull.once('<br/>' + req._t('ComparisonRepoBranch') + ':'),
2508 pull(
2509 getForks(repo, true),
2510 pull.asyncMap(function (msg, cb) {
2511 getRepo(msg.key, function (err, repo) {
2512 if (err) return cb(err)
2513 cb(null, {
2514 msg: msg,
2515 repo: repo
2516 })
2517 })
2518 }),
2519 pull.map(renderFork),
2520 pull.flatten()
2521 ),
2522 pull.once('</section>'),
2523 readOnce(function (cb) {
2524 cb(null, count == 0 ? req._t('NoBranches') :
2525 '<button type="submit" class="btn">' +
2526 req._t('Compare') + '</button>')
2527 }),
2528 pull.once('</form>')
2529 ]))
2530
2531 function renderFork(fork) {
2532 return pull(
2533 fork.repo.refs(),
2534 pull.map(function (ref) {
2535 var m = /^refs\/([^\/]*)\/(.*)$/.exec(ref.name) || [,ref.name]
2536 return {
2537 type: m[1],
2538 name: m[2],
2539 value: ref.value
2540 }
2541 }),
2542 pull.filter(function (ref) {
2543 return ref.type == 'heads'
2544 && !(ref.name == base && fork.msg.key == repo.id)
2545 }),
2546 pull.map(function (ref) {
2547 var branchLink = link([fork.msg.key, 'tree', ref.name], ref.name)
2548 var authorLink = link([fork.msg.value.author], fork.msg.authorName)
2549 var repoLink = link([fork.msg.key], fork.msg.repoName)
2550 var value = fork.msg.key + ':' + ref.name
2551 count++
2552 return '<div class="bgslash">' +
2553 '<input type="radio" name="head"' +
2554 ' value="' + escapeHTML(value) + '"' +
2555 (query.head == value ? ' checked="checked"' : '') + '> ' +
2556 authorLink + ' / ' + repoLink + ' / ' + branchLink + '</div>'
2557 })
2558 )
2559 }
2560 }
2561
2562 function serveRepoComparing(req, repo) {
2563 var query = req._u.query
2564 var baseBranch = query.base
2565 var s = (query.head || '').split(':')
2566
2567 if (!s || !baseBranch)
2568 return serveRedirect(req, encodeLink([repo.id, 'compare']))
2569
2570 var headRepoId = s[0]
2571 var headBranch = s[1]
2572 var baseLink = link([repo.id, 'tree', baseBranch])
2573 var headBranchLink = link([headRepoId, 'tree', headBranch])
2574 var backHref = encodeLink([repo.id, 'compare']) + req._u.search
2575
2576 return renderRepoPage(req, repo, 'pulls', null, cat([
2577 pull.once('<h3>' +
2578 req._t(query.expand ? 'OpenPullRequest': 'ComparingChanges') +
2579 '</h3>'),
2580 readNext(function (cb) {
2581 getRepo(headRepoId, function (err, headRepo) {
2582 if (err) return cb(err)
2583 getRepoFullName(about, headRepo.feed, headRepo.id,
2584 function (err, repoName, authorName) {
2585 if (err) return cb(err)
2586 cb(null, renderRepoInfo(Repo(headRepo), repoName, authorName))
2587 }
2588 )
2589 })
2590 })
2591 ]))
2592
2593 function renderRepoInfo(headRepo, headRepoName, headRepoAuthorName) {
2594 var authorLink = link([headRepo.feed], headRepoAuthorName)
2595 var repoLink = link([headRepoId], headRepoName)
2596 return cat([
2597 pull.once('<section>' +
2598 req._t('Base') + ': ' + baseLink + '<br/>' +
2599 req._t('Head') + ': ' +
2600 '<span class="bgslash">' + authorLink + ' / ' + repoLink +
2601 ' / ' + headBranchLink + '</span>' +
2602 '</section>' +
2603 (query.expand ? '<section><form method="post" action="">' +
2604 hiddenInputs({
2605 action: 'new-pull',
2606 branch: baseBranch,
2607 head_repo: headRepoId,
2608 head_branch: headBranch
2609 }) +
2610 '<input class="wide-input" name="title"' +
2611 ' placeholder="' + req._t('Title') + '" size="77"/>' +
2612 renderPostForm(req, repo, req._t('Description'), 8) +
2613 '<button type="submit" class="btn open">' +
2614 req._t('Create') + '</button>' +
2615 '</form></section>'
2616 : '<section><form method="get" action="">' +
2617 hiddenInputs({
2618 base: baseBranch,
2619 head: query.head
2620 }) +
2621 '<button class="btn open" type="submit" name="expand" value="1">' +
2622 '<i>โŽ‡</i> ' + req._t('CreatePullRequest') + '</button> ' +
2623 '<a href="' + backHref + '">' + req._t('Back') + '</a>' +
2624 '</form></section>') +
2625 '<div id="commits"></div>' +
2626 '<div class="tab-links">' +
2627 '<a href="#" id="files-link">' + req._t('FilesChanged') + '</a> ' +
2628 '<a href="#commits" id="commits-link">' +
2629 req._t('Commits') + '</a>' +
2630 '</div>' +
2631 '<section id="files-tab">'),
2632 renderDiffStat(req, [repo, headRepo], [baseBranch, headBranch]),
2633 pull.once('</section>' +
2634 '<section id="commits-tab">'),
2635 renderCommitLog(req, repo, baseBranch, headRepo, headBranch),
2636 pull.once('</section>')
2637 ])
2638 }
2639 }
2640
2641 function renderCommitLog(req, baseRepo, baseBranch, headRepo, headBranch) {
2642 return cat([
2643 pull.once('<table class="compare-commits">'),
2644 readNext(function (cb) {
2645 baseRepo.resolveRef(baseBranch, function (err, baseBranchRev) {
2646 if (err) return cb(err)
2647 var currentDay
2648 return cb(null, pull(
2649 headRepo.readLog(headBranch),
2650 pull.take(function (rev) { return rev != baseBranchRev }),
2651 pullReverse(),
2652 paramap(headRepo.getCommitParsed.bind(headRepo), 8),
2653 pull.map(function (commit) {
2654 var commitPath = [headRepo.id, 'commit', commit.id]
2655 var commitIdShort = '<tt>' + commit.id.substr(0, 8) + '</tt>'
2656 var day = Math.floor(commit.author.date / 86400000)
2657 var dateRow = day == currentDay ? '' :
2658 '<tr><th colspan=3 class="date-info">' +
2659 commit.author.date.toLocaleDateString(req._locale) +
2660 '</th><tr>'
2661 currentDay = day
2662 return dateRow + '<tr>' +
2663 '<td>' + escapeHTML(commit.author.name) + '</td>' +
2664 '<td>' + link(commitPath, commit.title) + '</td>' +
2665 '<td>' + link(commitPath, commitIdShort, true) + '</td>' +
2666 '</tr>'
2667 })
2668 ))
2669 })
2670 }),
2671 pull.once('</table>')
2672 ])
2673 }
2674}
2675

Built with git-ssb-web