git ssb

30+

cel / git-ssb-web



Tree: 1b7253ee8c5a23c7ec17bd44c7a7753732087c88

Files: 1b7253ee8c5a23c7ec17bd44c7a7753732087c88 / index.js

91936 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 'tag':
1225 return serveRepoTag(req, repo, branch)
1226 case 'tree':
1227 return serveRepoTree(req, repo, branch, filePath)
1228 case 'blob':
1229 return serveRepoBlob(req, repo, branch, filePath)
1230 case 'raw':
1231 return serveRepoRaw(req, repo, branch, filePath)
1232 case 'digs':
1233 return serveRepoDigs(req, repo)
1234 case 'fork':
1235 return serveRepoForkPrompt(req, repo)
1236 case 'forks':
1237 return serveRepoForks(req, repo)
1238 case 'issues':
1239 switch (path[1]) {
1240 case 'new':
1241 if (filePath.length == 0)
1242 return serveRepoNewIssue(req, repo)
1243 break
1244 default:
1245 return serveRepoIssues(req, repo, false)
1246 }
1247 case 'pulls':
1248 return serveRepoIssues(req, repo, true)
1249 case 'compare':
1250 return serveRepoCompare(req, repo)
1251 case 'comparing':
1252 return serveRepoComparing(req, repo)
1253 default:
1254 return serve404(req)
1255 }
1256 }
1257
1258 function serveRepoNotFound(req, id, err) {
1259 return serveTemplate(req, req._t('error.RepoNotFound'), 404)(pull.values([
1260 '<h2>' + req._t('error.RepoNotFound') + '</h2>',
1261 '<p>' + req._t('error.RepoNameNotFound') + '</p>',
1262 '<pre>' + escapeHTML(err.stack) + '</pre>'
1263 ]))
1264 }
1265
1266 function renderRepoPage(req, repo, page, branch, body) {
1267 var gitUrl = 'ssb://' + repo.id
1268 var gitLink = '<input class="clone-url" readonly="readonly" ' +
1269 'value="' + gitUrl + '" size="45" ' +
1270 'onclick="this.select()"/>'
1271 var digsPath = [repo.id, 'digs']
1272
1273 var done = multicb({ pluck: 1, spread: true })
1274 getRepoName(about, repo.feed, repo.id, done())
1275 about.getName(repo.feed, done())
1276 getVotes(repo.id, done())
1277
1278 if (repo.upstream) {
1279 getRepoName(about, repo.upstream.feed, repo.upstream.id, done())
1280 about.getName(repo.upstream.feed, done())
1281 }
1282
1283 return readNext(function (cb) {
1284 done(function (err, repoName, authorName, votes,
1285 upstreamName, upstreamAuthorName) {
1286 if (err) return cb(null, serveError(req, err))
1287 var upvoted = votes.upvoters[myId] > 0
1288 var upstreamLink = !repo.upstream ? '' :
1289 link([repo.upstream])
1290 cb(null, serveTemplate(req, repo.id)(cat([
1291 pull.once(
1292 '<div class="repo-title">' +
1293 '<form class="right-bar" action="" method="post">' +
1294 '<button class="btn" name="action" value="vote" ' +
1295 (isPublic ? 'disabled="disabled"' : ' type="submit"') + '>' +
1296 '<i>โœŒ</i> ' + req._t(!isPublic && upvoted ? 'Undig' : 'Dig') +
1297 '</button>' +
1298 (isPublic ? '' : '<input type="hidden" name="value" value="' +
1299 (upvoted ? '0' : '1') + '">' +
1300 '<input type="hidden" name="id" value="' +
1301 escapeHTML(repo.id) + '">') + ' ' +
1302 '<strong>' + link(digsPath, votes.upvotes) + '</strong> ' +
1303 (isPublic ? '' : '<button class="btn" type="submit" ' +
1304 ' name="action" value="fork-prompt">' +
1305 '<i>โ‘‚</i> ' + req._t('Fork') +
1306 '</button>') + ' ' +
1307 link([repo.id, 'forks'], '+', false, ' title="' +
1308 req._t('Forks') + '"') +
1309 '</form>' +
1310 renderNameForm(req, !isPublic, repo.id, repoName, 'repo-name',
1311 null, req._t('repo.Rename'),
1312 '<h2 class="bgslash">' + link([repo.feed], authorName) + ' / ' +
1313 link([repo.id], repoName) + '</h2>') +
1314 '</div>' +
1315 (repo.upstream ? '<small class="bgslash">' + req._t('ForkedFrom', {
1316 repo: link([repo.upstream.feed], upstreamAuthorName) + '/' +
1317 link([repo.upstream.id], upstreamName)
1318 }) + '</small>' : '') +
1319 nav([
1320 [[repo.id], req._t('Code'), 'code'],
1321 [[repo.id, 'activity'], req._t('Activity'), 'activity'],
1322 [[repo.id, 'commits', branch||''], req._t('Commits'), 'commits'],
1323 [[repo.id, 'issues'], req._t('Issues'), 'issues'],
1324 [[repo.id, 'pulls'], req._t('PullRequests'), 'pulls']
1325 ], page, gitLink)),
1326 body
1327 ])))
1328 })
1329 })
1330 }
1331
1332 function serveEmptyRepo(req, repo) {
1333 if (repo.feed != myId)
1334 return renderRepoPage(req, repo, 'code', null, pull.once(
1335 '<section>' +
1336 '<h3>' + req._t('EmptyRepo') + '</h3>' +
1337 '</section>'))
1338
1339 var gitUrl = 'ssb://' + repo.id
1340 return renderRepoPage(req, repo, 'code', null, pull.once(
1341 '<section>' +
1342 '<h3>' + req._t('initRepo.GettingStarted') + '</h3>' +
1343 '<h4>' + req._t('initRepo.CreateNew') + '</h4><pre>' +
1344 'touch ' + req._t('initRepo.README') + '.md\n' +
1345 'git init\n' +
1346 'git add ' + req._t('initRepo.README') + '.md\n' +
1347 'git commit -m "' + req._t('initRepo.InitialCommit') + '"\n' +
1348 'git remote add origin ' + gitUrl + '\n' +
1349 'git push -u origin master</pre>\n' +
1350 '<h4>' + req._t('initRepo.PushExisting') + '</h4>\n' +
1351 '<pre>git remote add origin ' + gitUrl + '\n' +
1352 'git push -u origin master</pre>' +
1353 '</section>'))
1354 }
1355
1356 function serveRepoTree(req, repo, rev, path) {
1357 if (!rev) return serveEmptyRepo(req, repo)
1358 var type = repo.isCommitHash(rev) ? 'Tree' : 'Branch'
1359 return renderRepoPage(req, repo, 'code', rev, cat([
1360 pull.once('<section><form action="" method="get">' +
1361 '<h3>' + req._t(type) + ': ' + rev + ' '),
1362 revMenu(req, repo, rev),
1363 pull.once('</h3></form>'),
1364 type == 'Branch' && renderRepoLatest(req, repo, rev),
1365 pull.once('</section><section>'),
1366 renderRepoTree(req, repo, rev, path),
1367 pull.once('</section>'),
1368 renderRepoReadme(req, repo, rev, path)
1369 ]))
1370 }
1371
1372 /* Search */
1373
1374 function serveSearch(req) {
1375 var q = String(req._u.query.q || '')
1376 if (!q) return serveIndex(req)
1377 var qId = q.replace(/^ssb:\/*/, '')
1378 if (ref.type(qId))
1379 return serveRedirect(req, encodeURIComponent(qId))
1380
1381 var search = new RegExp(q, 'i')
1382 return serveTemplate(req, req._t('Search') + ' &middot; ' + q, 200)(
1383 renderFeed(req, null, function (opts) {
1384 opts.type == 'about'
1385 return function (read) {
1386 return pull(
1387 many([
1388 getRepoNames(opts),
1389 read
1390 ]),
1391 pull.filter(function (msg) {
1392 var c = msg.value.content
1393 return (
1394 search.test(msg.key) ||
1395 c.text && search.test(c.text) ||
1396 c.name && search.test(c.name) ||
1397 c.title && search.test(c.title))
1398 })
1399 )
1400 }
1401 })
1402 )
1403 }
1404
1405 function getRepoNames(opts) {
1406 return pull(
1407 ssb.messagesByType({
1408 type: 'about',
1409 reverse: opts.reverse,
1410 lt: opts.lt,
1411 gt: opts.gt,
1412 }),
1413 pull.filter(function (msg) {
1414 return '%' == String(msg.value.content.about)[0]
1415 && msg.value.content.name
1416 })
1417 )
1418 }
1419
1420 /* Repo activity */
1421
1422 function serveRepoActivity(req, repo, branch) {
1423 return renderRepoPage(req, repo, 'activity', branch, cat([
1424 pull.once('<h3>' + req._t('Activity') + '</h3>'),
1425 pull(
1426 ssb.links({
1427 dest: repo.id,
1428 source: repo.feed,
1429 rel: 'repo',
1430 values: true,
1431 reverse: true
1432 }),
1433 pull.map(renderRepoUpdate.bind(this, req, repo))
1434 ),
1435 readOnce(function (cb) {
1436 var done = multicb({ pluck: 1, spread: true })
1437 about.getName(repo.feed, done())
1438 getMsg(repo.id, done())
1439 done(function (err, authorName, msg) {
1440 if (err) return cb(err)
1441 renderFeedItem(req, {
1442 key: repo.id,
1443 value: msg,
1444 authorName: authorName
1445 }, cb)
1446 })
1447 })
1448 ]))
1449 }
1450
1451 function renderRepoUpdate(req, repo, msg, full) {
1452 var c = msg.value.content
1453
1454 if (c.type != 'git-update') {
1455 return ''
1456 // return renderFeedItem(msg, cb)
1457 // TODO: render post, issue, pull-request
1458 }
1459
1460 var branches = []
1461 var tags = []
1462 if (c.refs) for (var name in c.refs) {
1463 var m = name.match(/^refs\/(heads|tags)\/(.*)$/) || [,, name]
1464 ;(m[1] == 'tags' ? tags : branches)
1465 .push({name: m[2], value: c.refs[name]})
1466 }
1467 var numObjects = c.objects ? Object.keys(c.objects).length : 0
1468
1469 var dateStr = new Date(msg.value.timestamp).toLocaleString(req._locale)
1470 return '<section class="collapse">' +
1471 link([msg.key], dateStr) + '<br>' +
1472 branches.map(function (update) {
1473 if (!update.value) {
1474 return '<s>' + escapeHTML(update.name) + '</s><br/>'
1475 } else {
1476 var commitLink = link([repo.id, 'commit', update.value])
1477 var branchLink = link([repo.id, 'tree', update.name])
1478 return branchLink + ' &rarr; <tt>' + commitLink + '</tt><br/>'
1479 }
1480 }).join('') +
1481 tags.map(function (update) {
1482 return update.value
1483 ? link([repo.id, 'tag', update.value], update.name)
1484 : '<s>' + escapeHTML(update.name) + '</s>'
1485 }).join(', ') +
1486 '</section>'
1487 }
1488
1489 /* Repo commits */
1490
1491 function serveRepoCommits(req, repo, branch) {
1492 var query = req._u.query
1493 return renderRepoPage(req, repo, 'commits', branch, cat([
1494 pull.once('<h3>' + req._t('Commits') + '</h3>'),
1495 pull(
1496 repo.readLog(query.start || branch),
1497 pull.take(20),
1498 paramap(repo.getCommitParsed.bind(repo), 8),
1499 paginate(
1500 !query.start ? '' : function (first, cb) {
1501 cb(null, '&hellip;')
1502 },
1503 pull.map(renderCommit.bind(this, req, repo)),
1504 function (commit, cb) {
1505 cb(null, commit.parents && commit.parents[0] ?
1506 '<a href="?start=' + commit.id + '">' +
1507 req._t('Older') + '</a>' : '')
1508 }
1509 )
1510 )
1511 ]))
1512 }
1513
1514 function renderCommit(req, repo, commit) {
1515 var commitPath = [repo.id, 'commit', commit.id]
1516 var treePath = [repo.id, 'tree', commit.id]
1517 return '<section class="collapse">' +
1518 '<strong>' + link(commitPath, commit.title) + '</strong><br>' +
1519 '<tt>' + commit.id + '</tt> ' +
1520 link(treePath, req._t('Tree')) + '<br>' +
1521 escapeHTML(commit.author.name) + ' &middot; ' +
1522 commit.author.date.toLocaleString(req._locale) +
1523 (commit.separateAuthor ? '<br>' + req._t('CommittedOn', {
1524 name: escapeHTML(commit.committer.name),
1525 date: commit.committer.date.toLocaleString(req._locale)
1526 }) : '') +
1527 '</section>'
1528 }
1529
1530 /* Branch menu */
1531
1532 function formatRevOptions(currentName) {
1533 return function (name) {
1534 var htmlName = escapeHTML(name)
1535 return '<option value="' + htmlName + '"' +
1536 (name == currentName ? ' selected="selected"' : '') +
1537 '>' + htmlName + '</option>'
1538 }
1539 }
1540
1541 function formatRevType(req, type) {
1542 return (
1543 type == 'heads' ? req._t('Branches') :
1544 type == 'tags' ? req._t('Tags') :
1545 type)
1546 }
1547
1548 function revMenu(req, repo, currentName) {
1549 return readOnce(function (cb) {
1550 repo.getRefNames(function (err, refs) {
1551 if (err) return cb(err)
1552 cb(null, '<select name="rev" onchange="this.form.submit()">' +
1553 Object.keys(refs).map(function (group) {
1554 return '<optgroup label="' + formatRevType(req, group) + '">' +
1555 refs[group].map(formatRevOptions(currentName)).join('') +
1556 '</optgroup>'
1557 }).join('') +
1558 '</select><noscript> ' +
1559 '<input type="submit" value="' + req._t('Go') + '"/></noscript>')
1560 })
1561 })
1562 }
1563
1564 function branchMenu(repo, name, currentName) {
1565 return cat([
1566 pull.once('<select name="' + name + '">'),
1567 pull(
1568 repo.refs(),
1569 pull.map(function (ref) {
1570 var m = ref.name.match(/^refs\/([^\/]*)\/(.*)$/) || [,, ref.name]
1571 return m[1] == 'heads' && m[2]
1572 }),
1573 pull.filter(Boolean),
1574 pullSort(),
1575 pull.map(formatRevOptions(currentName))
1576 ),
1577 pull.once('</select>')
1578 ])
1579 }
1580
1581 /* Repo tree */
1582
1583 function renderRepoLatest(req, repo, rev) {
1584 return readOnce(function (cb) {
1585 repo.getCommitParsed(rev, function (err, commit) {
1586 if (err) return cb(err)
1587 var commitPath = [repo.id, 'commit', commit.id]
1588 cb(null,
1589 req._t('Latest') + ': ' +
1590 '<strong>' + link(commitPath, commit.title) + '</strong><br/>' +
1591 '<tt>' + commit.id + '</tt><br/> ' +
1592 req._t('CommittedOn', {
1593 name: escapeHTML(commit.committer.name),
1594 date: commit.committer.date.toLocaleString(req._locale)
1595 }) +
1596 (commit.separateAuthor ? '<br/>' + req._t('AuthoredOn', {
1597 name: escapeHTML(commit.author.name),
1598 date: commit.author.date.toLocaleString(req._locale)
1599 }) : ''))
1600 })
1601 })
1602 }
1603
1604 // breadcrumbs
1605 function linkPath(basePath, path) {
1606 path = path.slice()
1607 var last = path.pop()
1608 return path.map(function (dir, i) {
1609 return link(basePath.concat(path.slice(0, i+1)), dir)
1610 }).concat(last).join(' / ')
1611 }
1612
1613 function renderRepoTree(req, repo, rev, path) {
1614 var pathLinks = path.length === 0 ? '' :
1615 ': ' + linkPath([repo.id, 'tree'], [rev].concat(path))
1616 return cat([
1617 pull.once('<h3>' + req._t('Files') + pathLinks + '</h3>'),
1618 pull(
1619 repo.readDir(rev, path),
1620 pull.map(function (file) {
1621 var type = (file.mode === 040000) ? 'tree' :
1622 (file.mode === 0160000) ? 'commit' : 'blob'
1623 if (type == 'commit')
1624 return [
1625 '<span title="' + req._t('gitCommitLink') + '">๐Ÿ–ˆ</span>',
1626 '<span title="' + escapeHTML(file.id) + '">' +
1627 escapeHTML(file.name) + '</span>']
1628 var filePath = [repo.id, type, rev].concat(path, file.name)
1629 return ['<i>' + (type == 'tree' ? '๐Ÿ“' : '๐Ÿ“„') + '</i>',
1630 link(filePath, file.name)]
1631 }),
1632 table('class="files"')
1633 )
1634 ])
1635 }
1636
1637 /* Repo readme */
1638
1639 function renderRepoReadme(req, repo, branch, path) {
1640 return readNext(function (cb) {
1641 pull(
1642 repo.readDir(branch, path),
1643 pull.filter(function (file) {
1644 return /readme(\.|$)/i.test(file.name)
1645 }),
1646 pull.take(1),
1647 pull.collect(function (err, files) {
1648 if (err) return cb(null, pull.empty())
1649 var file = files[0]
1650 if (!file)
1651 return cb(null, pull.once(path.length ? '' :
1652 '<p>' + req._t('NoReadme') + '</p>'))
1653 repo.getObjectFromAny(file.id, function (err, obj) {
1654 if (err) return cb(err)
1655 cb(null, cat([
1656 pull.once('<section><h4><a name="readme">' +
1657 escapeHTML(file.name) + '</a></h4><hr/>'),
1658 renderObjectData(obj, file.name, repo, branch, path),
1659 pull.once('</section>')
1660 ]))
1661 })
1662 })
1663 )
1664 })
1665 }
1666
1667 /* Repo commit */
1668
1669 function serveRepoCommit(req, repo, rev) {
1670 return renderRepoPage(req, repo, null, rev, cat([
1671 readNext(function (cb) {
1672 repo.getCommitParsed(rev, function (err, commit) {
1673 if (err) return cb(err)
1674 var commitPath = [repo.id, 'commit', commit.id]
1675 var treePath = [repo.id, 'tree', commit.id]
1676 cb(null, cat([pull.once(
1677 '<h3>' + link(commitPath,
1678 req._t('CommitRev', {rev: rev})) + '</h3>' +
1679 '<section class="collapse">' +
1680 '<div class="right-bar">' +
1681 link(treePath, req._t('BrowseFiles')) +
1682 '</div>' +
1683 '<h4>' + linkify(escapeHTML(commit.title)) + '</h4>' +
1684 (commit.body ? linkify(pre(commit.body)) : '') +
1685 (commit.separateAuthor ? req._t('AuthoredOn', {
1686 name: escapeHTML(commit.author.name),
1687 date: commit.author.date.toLocaleString(req._locale)
1688 }) + '<br/>' : '') +
1689 req._t('CommittedOn', {
1690 name: escapeHTML(commit.committer.name),
1691 date: commit.committer.date.toLocaleString(req._locale)
1692 }) + '<br/>' +
1693 commit.parents.map(function (id) {
1694 return req._t('Parent') + ': ' +
1695 link([repo.id, 'commit', id], id)
1696 }).join('<br>') +
1697 '</section>' +
1698 '<section><h3>' + req._t('FilesChanged') + '</h3>'),
1699 // TODO: show diff from all parents (merge commits)
1700 renderDiffStat(req, [repo, repo], [commit.parents[0], commit.id]),
1701 pull.once('</section>')
1702 ]))
1703 })
1704 })
1705 ]))
1706 }
1707
1708 /* Repo tag */
1709
1710 function serveRepoTag(req, repo, rev) {
1711 return renderRepoPage(req, repo, 'tags', rev, readNext(function (cb) {
1712 repo.getTagParsed(rev, function (err, tag) {
1713 if (err) return cb(err)
1714 var body = (tag.title + '\n\n' +
1715 tag.body.replace(/-----BEGIN PGP SIGNATURE-----\n[^.]*?\n-----END PGP SIGNATURE-----\s*$/, '')).trim()
1716 cb(null, pull.once(
1717 '<section class="collapse">' +
1718 '<h3>' + link([repo.id, 'tag', rev], tag.tag) + '</h3>' +
1719 req._t('TaggedOn', {
1720 name: escapeHTML(tag.tagger.name),
1721 date: tag.tagger.date.toLocaleString(req._locale)
1722 }) + '<br/>' +
1723 link([repo.id, tag.type, tag.object]) +
1724 linkify(pre(body)) +
1725 '</section>'))
1726 })
1727 }))
1728 }
1729
1730
1731 /* Diff stat */
1732
1733 function renderDiffStat(req, repos, treeIds) {
1734 if (treeIds.length == 0) treeIds = [null]
1735 var id = treeIds[0]
1736 var lastI = treeIds.length - 1
1737 var oldTree = treeIds[0]
1738 var changedFiles = []
1739 return cat([
1740 pull(
1741 Repo.diffTrees(repos, treeIds, true),
1742 pull.map(function (item) {
1743 var filename = escapeHTML(item.filename = item.path.join('/'))
1744 var oldId = item.id && item.id[0]
1745 var newId = item.id && item.id[lastI]
1746 var oldMode = item.mode && item.mode[0]
1747 var newMode = item.mode && item.mode[lastI]
1748 var action =
1749 !oldId && newId ? req._t('action.added') :
1750 oldId && !newId ? req._t('action.deleted') :
1751 oldMode != newMode ? req._t('action.changedMode', {
1752 old: oldMode.toString(8),
1753 new: newMode.toString(8)
1754 }) : req._t('changed')
1755 if (item.id)
1756 changedFiles.push(item)
1757 var blobsPath = item.id[1]
1758 ? [repos[1].id, 'blob', treeIds[1]]
1759 : [repos[0].id, 'blob', treeIds[0]]
1760 var rawsPath = item.id[1]
1761 ? [repos[1].id, 'raw', treeIds[1]]
1762 : [repos[0].id, 'raw', treeIds[0]]
1763 item.blobPath = blobsPath.concat(item.path)
1764 item.rawPath = rawsPath.concat(item.path)
1765 var fileHref = item.id ?
1766 '#' + encodeURIComponent(item.path.join('/')) :
1767 encodeLink(item.blobPath)
1768 return ['<a href="' + fileHref + '">' + filename + '</a>', action]
1769 }),
1770 table()
1771 ),
1772 pull(
1773 pull.values(changedFiles),
1774 paramap(function (item, cb) {
1775 var extension = getExtension(item.filename)
1776 if (extension in imgMimes) {
1777 var filename = escapeHTML(item.filename)
1778 return cb(null,
1779 '<pre><table class="code">' +
1780 '<tr><th id="' + escapeHTML(item.filename) + '">' +
1781 filename + '</th></tr>' +
1782 '<tr><td><img src="' + encodeLink(item.rawPath) + '"' +
1783 ' alt="' + filename + '"/></td></tr>' +
1784 '</table></pre>')
1785 }
1786 var done = multicb({ pluck: 1, spread: true })
1787 getRepoObjectString(repos[0], item.id[0], done())
1788 getRepoObjectString(repos[1], item.id[lastI], done())
1789 done(function (err, strOld, strNew) {
1790 if (err) return cb(err)
1791 cb(null, htmlLineDiff(req, item.filename, item.filename,
1792 strOld, strNew,
1793 encodeLink(item.blobPath)))
1794 })
1795 }, 4)
1796 )
1797 ])
1798 }
1799
1800 function htmlLineDiff(req, filename, anchor, oldStr, newStr, blobHref) {
1801 var diff = JsDiff.structuredPatch('', '', oldStr, newStr)
1802 var groups = diff.hunks.map(function (hunk) {
1803 var oldLine = hunk.oldStart
1804 var newLine = hunk.newStart
1805 var header = '<tr class="diff-hunk-header"><td colspan=2></td><td>' +
1806 '@@ -' + oldLine + ',' + hunk.oldLines + ' ' +
1807 '+' + newLine + ',' + hunk.newLines + ' @@' +
1808 '</td></tr>'
1809 return [header].concat(hunk.lines.map(function (line) {
1810 var s = line[0]
1811 if (s == '\\') return
1812 var html = highlight(line, getExtension(filename))
1813 var trClass = s == '+' ? 'diff-new' : s == '-' ? 'diff-old' : ''
1814 var lineNums = [s == '+' ? '' : oldLine++, s == '-' ? '' : newLine++]
1815 var id = [filename].concat(lineNums).join('-')
1816 return '<tr id="' + escapeHTML(id) + '" class="' + trClass + '">' +
1817 lineNums.map(function (num) {
1818 return '<td class="code-linenum">' +
1819 (num ? '<a href="#' + encodeURIComponent(id) + '">' +
1820 num + '</a>' : '') + '</td>'
1821 }).join('') +
1822 '<td class="code-text">' + html + '</td></tr>'
1823 }))
1824 })
1825 return '<pre><table class="code">' +
1826 '<tr><th colspan=3 id="' + escapeHTML(anchor) + '">' + filename +
1827 '<span class="right-bar">' +
1828 '<a href="' + blobHref + '">' + req._t('View') + '</a> ' +
1829 '</span></th></tr>' +
1830 [].concat.apply([], groups).join('') +
1831 '</table></pre>'
1832 }
1833
1834 /* An unknown message linking to a repo */
1835
1836 function serveRepoSomething(req, repo, id, msg, path) {
1837 return renderRepoPage(req, repo, null, null,
1838 pull.once('<section><h3>' + link([id]) + '</h3>' +
1839 json(msg) + '</section>'))
1840 }
1841
1842 /* Repo update */
1843
1844 function objsArr(objs) {
1845 return Array.isArray(objs) ? objs :
1846 Object.keys(objs).map(function (sha1) {
1847 var obj = Object.create(objs[sha1])
1848 obj.sha1 = sha1
1849 return obj
1850 })
1851 }
1852
1853 function serveRepoUpdate(req, repo, id, msg, path) {
1854 var raw = req._u.query.raw != null
1855
1856 if (raw)
1857 return renderRepoPage(req, repo, 'activity', null, pull.once(
1858 '<a href="?" class="raw-link header-align">' +
1859 req._t('Info') + '</a>' +
1860 '<h3>' + req._t('Update') + '</h3>' +
1861 '<section class="collapse">' +
1862 json({key: id, value: msg}) + '</section>'))
1863
1864 // convert packs to old single-object style
1865 if (msg.content.indexes) {
1866 for (var i = 0; i < msg.content.indexes.length; i++) {
1867 msg.content.packs[i] = {
1868 pack: {link: msg.content.packs[i].link},
1869 idx: msg.content.indexes[i]
1870 }
1871 }
1872 }
1873
1874 var commits = cat([
1875 msg.content.objects && pull(
1876 pull.values(msg.content.objects),
1877 pull.filter(function (obj) { return obj.type == 'commit' }),
1878 paramap(function (obj, cb) {
1879 getBlob(req, obj.link || obj.key, function (err, readObject) {
1880 if (err) return cb(err)
1881 Repo.getCommitParsed({read: readObject}, cb)
1882 })
1883 }, 8)
1884 ),
1885 msg.content.packs && pull(
1886 pull.values(msg.content.packs),
1887 paramap(function (pack, cb) {
1888 var done = multicb({ pluck: 1, spread: true })
1889 getBlob(req, pack.pack.link, done())
1890 getBlob(req, pack.idx.link, done())
1891 done(function (err, readPack, readIdx) {
1892 if (err) return cb(renderError(err))
1893 cb(null, gitPack.decodeWithIndex(repo, readPack, readIdx))
1894 })
1895 }, 4),
1896 pull.flatten(),
1897 pull.asyncMap(function (obj, cb) {
1898 if (obj.type == 'commit')
1899 Repo.getCommitParsed(obj, cb)
1900 else
1901 pull(obj.read, pull.drain(null, cb))
1902 }),
1903 pull.filter()
1904 )
1905 ])
1906
1907 return renderRepoPage(req, repo, 'activity', null, cat([
1908 pull.once('<a href="?raw" class="raw-link header-align">' +
1909 req._t('Data') + '</a>' +
1910 '<h3>' + req._t('Update') + '</h3>' +
1911 renderRepoUpdate(req, repo, {key: id, value: msg}, true)),
1912 (msg.content.objects || msg.content.packs) &&
1913 pull.once('<h3>' + req._t('Commits') + '</h3>'),
1914 pull(commits, pull.map(function (commit) {
1915 return renderCommit(req, repo, commit)
1916 }))
1917 ]))
1918 }
1919
1920 /* Blob */
1921
1922 function serveRepoBlob(req, repo, rev, path) {
1923 return readNext(function (cb) {
1924 repo.getFile(rev, path, function (err, object) {
1925 if (err) return cb(null, serveBlobNotFound(req, repo.id, err))
1926 var type = repo.isCommitHash(rev) ? 'Tree' : 'Branch'
1927 var pathLinks = path.length === 0 ? '' :
1928 ': ' + linkPath([repo.id, 'tree'], [rev].concat(path))
1929 var rawFilePath = [repo.id, 'raw', rev].concat(path)
1930 var dirPath = path.slice(0, path.length-1)
1931 var filename = path[path.length-1]
1932 var extension = getExtension(filename)
1933 cb(null, renderRepoPage(req, repo, 'code', rev, cat([
1934 pull.once('<section><form action="" method="get">' +
1935 '<h3>' + req._t(type) + ': ' + rev + ' '),
1936 revMenu(req, repo, rev),
1937 pull.once('</h3></form>'),
1938 type == 'Branch' && renderRepoLatest(req, repo, rev),
1939 pull.once('</section><section class="collapse">' +
1940 '<h3>' + req._t('Files') + pathLinks + '</h3>' +
1941 '<div>' + object.length + ' bytes' +
1942 '<span class="raw-link">' +
1943 link(rawFilePath, req._t('Raw')) + '</span>' +
1944 '</div></section>' +
1945 '<section>'),
1946 extension in imgMimes
1947 ? pull.once('<img src="' + encodeLink(rawFilePath) +
1948 '" alt="' + escapeHTML(filename) + '" />')
1949 : renderObjectData(object, filename, repo, rev, dirPath),
1950 pull.once('</section>')
1951 ])))
1952 })
1953 })
1954 }
1955
1956 function serveBlobNotFound(req, repoId, err) {
1957 return serveTemplate(req, req._t('error.BlobNotFound'), 404)(pull.once(
1958 '<h2>' + req._t('error.BlobNotFound') + '</h2>' +
1959 '<p>' + req._t('error.BlobNotFoundInRepo', {
1960 repo: link([repoId])
1961 }) + '</p>' +
1962 '<pre>' + escapeHTML(err.stack) + '</pre>'
1963 ))
1964 }
1965
1966 /* Raw blob */
1967
1968 function serveRepoRaw(req, repo, branch, path) {
1969 return readNext(function (cb) {
1970 repo.getFile(branch, path, function (err, object) {
1971 if (err) return cb(null,
1972 serveBuffer(404, req._t('error.BlobNotFound')))
1973 var extension = getExtension(path[path.length-1])
1974 var contentType = imgMimes[extension]
1975 cb(null, pull(object.read, serveRaw(object.length, contentType)))
1976 })
1977 })
1978 }
1979
1980 function serveRaw(length, contentType) {
1981 var headers = {
1982 'Content-Type': contentType || 'text/plain; charset=utf-8',
1983 'Cache-Control': 'max-age=31536000'
1984 }
1985 if (length != null)
1986 headers['Content-Length'] = length
1987 return function (read) {
1988 return cat([pull.once([200, headers]), read])
1989 }
1990 }
1991
1992 function getBlob(req, key, cb) {
1993 ssb.blobs.want(key, function (err, got) {
1994 if (err) cb(err)
1995 else if (!got) cb(new Error(req._t('error.MissingBlob', {key: key})))
1996 else cb(null, ssb.blobs.get(key))
1997 })
1998 }
1999
2000 function serveBlob(req, key) {
2001 return readNext(function (cb) {
2002 getBlob(req, key, function (err, read) {
2003 if (err) cb(null, serveError(req, err))
2004 else if (!read) cb(null, serve404(req))
2005 else cb(null, serveRaw()(read))
2006 })
2007 })
2008 }
2009
2010 /* Digs */
2011
2012 function serveRepoDigs(req, repo) {
2013 return readNext(function (cb) {
2014 getVotes(repo.id, function (err, votes) {
2015 cb(null, renderRepoPage(req, repo, null, null, cat([
2016 pull.once('<section><h3>' + req._t('Digs') + '</h3>' +
2017 '<div>' + req._t('Total') + ': ' + votes.upvotes + '</div>'),
2018 pull(
2019 pull.values(Object.keys(votes.upvoters)),
2020 paramap(function (feedId, cb) {
2021 about.getName(feedId, function (err, name) {
2022 if (err) return cb(err)
2023 cb(null, link([feedId], name))
2024 })
2025 }, 8),
2026 ul()
2027 ),
2028 pull.once('</section>')
2029 ])))
2030 })
2031 })
2032 }
2033
2034 /* Forks */
2035
2036 function getForks(repo, includeSelf) {
2037 return pull(
2038 cat([
2039 includeSelf && readOnce(function (cb) {
2040 getMsg(repo.id, function (err, value) {
2041 cb(err, value && {key: repo.id, value: value})
2042 })
2043 }),
2044 ssb.links({
2045 dest: repo.id,
2046 values: true,
2047 rel: 'upstream'
2048 })
2049 ]),
2050 pull.filter(function (msg) {
2051 return msg.value.content && msg.value.content.type == 'git-repo'
2052 }),
2053 paramap(function (msg, cb) {
2054 getRepoFullName(about, msg.value.author, msg.key,
2055 function (err, repoName, authorName) {
2056 if (err) return cb(err)
2057 cb(null, {
2058 key: msg.key,
2059 value: msg.value,
2060 repoName: repoName,
2061 authorName: authorName
2062 })
2063 })
2064 }, 8)
2065 )
2066 }
2067
2068 function serveRepoForks(req, repo) {
2069 var hasForks
2070 return renderRepoPage(req, repo, null, null, cat([
2071 pull.once('<h3>' + req._t('Forks') + '</h3>'),
2072 pull(
2073 getForks(repo),
2074 pull.map(function (msg) {
2075 hasForks = true
2076 return '<section class="collapse">' +
2077 link([msg.value.author], msg.authorName) + ' / ' +
2078 link([msg.key], msg.repoName) +
2079 '<span class="right-bar">' +
2080 timestamp(msg.value.timestamp, req) +
2081 '</span></section>'
2082 })
2083 ),
2084 readOnce(function (cb) {
2085 cb(null, hasForks ? '' : req._t('NoForks'))
2086 })
2087 ]))
2088 }
2089
2090 function serveRepoForkPrompt(req, repo) {
2091 return renderRepoPage(req, repo, null, null, pull.once(
2092 '<form action="" method="post" onreset="history.back()">' +
2093 '<h3>' + req._t('ForkRepoPrompt') + '</h3>' +
2094 '<p>' + hiddenInputs({ id: repo.id }) +
2095 '<button class="btn open" type="submit" name="action" value="fork">' +
2096 req._t('Fork') +
2097 '</button>' +
2098 ' <button class="btn" type="reset">' +
2099 req._t('Cancel') + '</button>' +
2100 '</p></form>'
2101 ))
2102 }
2103
2104 /* Issues */
2105
2106 function serveRepoIssues(req, repo, isPRs) {
2107 var count = 0
2108 var state = req._u.query.state || 'open'
2109 var newPath = isPRs ? [repo.id, 'compare'] : [repo.id, 'issues', 'new']
2110 return renderRepoPage(req, repo, isPRs ? 'pulls' : 'issues', null, cat([
2111 pull.once(
2112 (isPublic ? '' :
2113 '<form class="right-bar" method="get"' +
2114 ' action="' + encodeLink(newPath) + '">' +
2115 '<button class="btn">&plus; ' +
2116 req._t(isPRs ? 'pullRequest.New' : 'issue.New') +
2117 '</button>' +
2118 '</form>') +
2119 '<h3>' + req._t(isPRs ? 'PullRequests' : 'Issues') + '</h3>' +
2120 nav([
2121 ['?', req._t('issues.Open'), 'open'],
2122 ['?state=closed', req._t('issues.Closed'), 'closed'],
2123 ['?state=all', req._t('issues.All'), 'all']
2124 ], state)),
2125 pull(
2126 (isPRs ? pullReqs : issues).list({
2127 repo: repo.id,
2128 project: repo.id,
2129 open: {open: true, closed: false}[state]
2130 }),
2131 pull.map(function (issue) {
2132 count++
2133 var state = (issue.open ? 'open' : 'closed')
2134 var stateStr = req._t(issue.open ?
2135 'issue.state.Open' : 'issue.state.Closed')
2136 return '<section class="collapse">' +
2137 '<i class="issue-state issue-state-' + state + '"' +
2138 ' title="' + stateStr + '">โ—ผ</i> ' +
2139 '<a href="' + encodeLink(issue.id) + '">' +
2140 escapeHTML(issue.title) +
2141 '<span class="right-bar">' +
2142 new Date(issue.created_at).toLocaleString(req._locale) +
2143 '</span>' +
2144 '</a>' +
2145 '</section>'
2146 })
2147 ),
2148 readOnce(function (cb) {
2149 cb(null, count > 0 ? '' :
2150 '<p>' + req._t(isPRs ? 'NoPullRequests' : 'NoIssues') + '</p>')
2151 })
2152 ]))
2153 }
2154
2155 /* New Issue */
2156
2157 function serveRepoNewIssue(req, repo, issueId, path) {
2158 return renderRepoPage(req, repo, 'issues', null, pull.once(
2159 '<h3>' + req._t('issue.New') + '</h3>' +
2160 '<section><form action="" method="post">' +
2161 '<input type="hidden" name="action" value="new-issue">' +
2162 '<p><input class="wide-input" name="title" placeholder="' +
2163 req._t('issue.Title') + '" size="77" /></p>' +
2164 renderPostForm(req, repo, req._t('Description'), 8) +
2165 '<button type="submit" class="btn">' + req._t('Create') + '</button>' +
2166 '</form></section>'))
2167 }
2168
2169 /* Issue */
2170
2171 function serveRepoIssue(req, repo, issue, path, postId) {
2172 var isAuthor = (myId == issue.author) || (myId == repo.feed)
2173 var newestMsg = {key: issue.id, value: {timestamp: issue.created_at}}
2174 return renderRepoPage(req, repo, 'issues', null, cat([
2175 pull.once(
2176 renderNameForm(req, !isPublic, issue.id, issue.title, 'issue-title',
2177 null, req._t('issue.Rename'),
2178 '<h3>' + link([issue.id], issue.title) + '</h3>') +
2179 '<code>' + issue.id + '</code>' +
2180 '<section class="collapse">' +
2181 (issue.open
2182 ? '<strong class="issue-status open">' +
2183 req._t('issue.state.Open') + '</strong>'
2184 : '<strong class="issue-status closed">' +
2185 req._t('issue.state.Closed') + '</strong>')),
2186 readOnce(function (cb) {
2187 about.getName(issue.author, function (err, authorName) {
2188 if (err) return cb(err)
2189 var authorLink = link([issue.author], authorName)
2190 cb(null, req._t('issue.Opened',
2191 {name: authorLink, datetime: timestamp(issue.created_at, req)}))
2192 })
2193 }),
2194 pull.once('<hr/>' + markdown(issue.text, repo) + '</section>'),
2195 // render posts and edits
2196 pull(
2197 ssb.links({
2198 dest: issue.id,
2199 values: true
2200 }),
2201 pull.unique('key'),
2202 addAuthorName(about),
2203 sortMsgs(),
2204 pull.through(function (msg) {
2205 if (msg.value.timestamp > newestMsg.value.timestamp)
2206 newestMsg = msg
2207 }),
2208 pull.map(renderIssueActivityMsg.bind(null, req, repo, issue,
2209 req._t('issue.'), postId))
2210 ),
2211 isPublic ? pull.empty() : readOnce(function (cb) {
2212 cb(null, renderIssueCommentForm(req, issue, repo, newestMsg.key,
2213 isAuthor, req._t('issue.')))
2214 })
2215 ]))
2216 }
2217
2218 function renderIssueActivityMsg(req, repo, issue, type, postId, msg) {
2219 var authorLink = link([msg.value.author], msg.authorName)
2220 var msgHref = encodeLink(msg.key) + '#' + encodeURIComponent(msg.key)
2221 var msgTimeLink = '<a href="' + msgHref + '"' +
2222 ' name="' + escapeHTML(msg.key) + '">' +
2223 new Date(msg.value.timestamp).toLocaleString(req._locale) + '</a>'
2224 var c = msg.value.content
2225 switch (c.type) {
2226 case 'post':
2227 if (c.root == issue.id) {
2228 var changed = issues.isStatusChanged(msg, issue)
2229 return '<section class="collapse">' +
2230 (msg.key == postId ? '<div class="highlight">' : '') +
2231 '<tt class="right-bar item-id">' + msg.key + '</tt> ' +
2232 (changed == null ? authorLink : req._t(
2233 changed ? 'issue.Reopened' : 'issue.Closed',
2234 {name: authorLink, type: type})) +
2235 ' &middot; ' + msgTimeLink +
2236 (msg.key == postId ? '</div>' : '') +
2237 markdown(c.text, repo) +
2238 '</section>'
2239 } else {
2240 var text = c.text || (c.type + ' ' + msg.key)
2241 return '<section class="collapse mention-preview">' +
2242 req._t('issue.MentionedIn', {
2243 name: authorLink,
2244 type: type,
2245 post: '<a href="/' + msg.key + '#' + msg.key + '">' +
2246 String(text).substr(0, 140) + '</a>'
2247 }) + '</section>'
2248 }
2249 case 'issue':
2250 case 'pull-request':
2251 return '<section class="collapse mention-preview">' +
2252 req._t('issue.MentionedIn', {
2253 name: authorLink,
2254 type: type,
2255 post: link([msg.key], String(c.title || msg.key).substr(0, 140))
2256 }) + '</section>'
2257 case 'issue-edit':
2258 return '<section class="collapse">' +
2259 (msg.key == postId ? '<div class="highlight">' : '') +
2260 (c.title == null ? '' : req._t('issue.Renamed', {
2261 name: authorLink,
2262 type: type,
2263 name: '<q>' + escapeHTML(c.title) + '</q>'
2264 })) + ' &middot; ' + msgTimeLink +
2265 (msg.key == postId ? '</div>' : '') +
2266 '</section>'
2267 case 'git-update':
2268 var mention = issues.getMention(msg, issue)
2269 if (mention) {
2270 var commitLink = link([repo.id, 'commit', mention.object],
2271 mention.label || mention.object)
2272 return '<section class="collapse">' +
2273 req._t(mention.open ? 'issue.Reopened' : 'issue.Closed', {
2274 name: authorLink,
2275 type: type
2276 }) + ' &middot; ' + msgTimeLink + '<br/>' +
2277 commitLink +
2278 '</section>'
2279 } else if ((mention = getMention(msg, issue.id))) {
2280 var commitLink = link(mention.object ?
2281 [repo.id, 'commit', mention.object] : [msg.key],
2282 mention.label || mention.object || msg.key)
2283 return '<section class="collapse">' +
2284 req._t('issue.Mentioned', {
2285 name: authorLink,
2286 type: type
2287 }) + ' &middot; ' + msgTimeLink + '<br/>' +
2288 commitLink +
2289 '</section>'
2290 } else {
2291 // fallthrough
2292 }
2293
2294 default:
2295 return '<section class="collapse">' +
2296 authorLink +
2297 ' &middot; ' + msgTimeLink +
2298 json(c) +
2299 '</section>'
2300 }
2301 }
2302
2303 function renderIssueCommentForm(req, issue, repo, branch, isAuthor, type) {
2304 return '<section><form action="" method="post">' +
2305 '<input type="hidden" name="action" value="comment">' +
2306 '<input type="hidden" name="id" value="' + issue.id + '">' +
2307 '<input type="hidden" name="issue" value="' + issue.id + '">' +
2308 '<input type="hidden" name="repo" value="' + repo.id + '">' +
2309 '<input type="hidden" name="branch" value="' + branch + '">' +
2310 renderPostForm(req, repo) +
2311 '<input type="submit" class="btn open" value="' +
2312 req._t('issue.Comment') + '" />' +
2313 (isAuthor ?
2314 '<input type="submit" class="btn"' +
2315 ' name="' + (issue.open ? 'close' : 'open') + '"' +
2316 ' value="' + req._t(
2317 issue.open ? 'issue.Close' : 'issue.Reopen', {type: type}
2318 ) + '"/>' : '') +
2319 '</form></section>'
2320 }
2321
2322 /* Pull Request */
2323
2324 function serveRepoPullReq(req, repo, pr, path, postId) {
2325 var headRepo, authorLink
2326 var page = path[0] || 'activity'
2327 return renderRepoPage(req, repo, 'pulls', null, cat([
2328 pull.once('<div class="pull-request">' +
2329 renderNameForm(req, !isPublic, pr.id, pr.title, 'issue-title', null,
2330 req._t('pullRequest.Rename'),
2331 '<h3>' + link([pr.id], pr.title) + '</h3>') +
2332 '<code>' + pr.id + '</code>'),
2333 readOnce(function (cb) {
2334 var done = multicb({ pluck: 1, spread: true })
2335 var gotHeadRepo = done()
2336 about.getName(pr.author, done())
2337 var sameRepo = (pr.headRepo == pr.baseRepo)
2338 getRepo(pr.headRepo, function (err, headRepo) {
2339 if (err) return cb(err)
2340 getRepoName(about, headRepo.feed, headRepo.id, done())
2341 about.getName(headRepo.feed, done())
2342 gotHeadRepo(null, Repo(headRepo))
2343 })
2344
2345 done(function (err, _headRepo, issueAuthorName,
2346 headRepoName, headRepoAuthorName) {
2347 if (err) return cb(err)
2348 headRepo = _headRepo
2349 authorLink = link([pr.author], issueAuthorName)
2350 var repoLink = link([pr.headRepo], headRepoName)
2351 var headRepoAuthorLink = link([headRepo.feed], headRepoAuthorName)
2352 var headRepoLink = link([headRepo.id], headRepoName)
2353 var headBranchLink = link([headRepo.id, 'tree', pr.headBranch])
2354 var baseBranchLink = link([repo.id, 'tree', pr.baseBranch])
2355 cb(null, '<section class="collapse">' +
2356 '<strong class="issue-status ' +
2357 (pr.open ? 'open' : 'closed') + '">' +
2358 req._t(pr.open ? 'issue.state.Open' : 'issue.state.Closed') +
2359 '</strong> ' +
2360 req._t('pullRequest.WantToMerge', {
2361 name: authorLink,
2362 base: '<code>' + baseBranchLink + '</code>',
2363 head: (sameRepo ?
2364 '<code>' + headBranchLink + '</code>' :
2365 '<code class="bgslash">' +
2366 headRepoAuthorLink + ' / ' +
2367 headRepoLink + ' / ' +
2368 headBranchLink + '</code>')
2369 }) + '</section>')
2370 })
2371 }),
2372 pull.once(
2373 nav([
2374 [[pr.id], req._t('Discussion'), 'activity'],
2375 [[pr.id, 'commits'], req._t('Commits'), 'commits'],
2376 [[pr.id, 'files'], req._t('Files'), 'files']
2377 ], page)),
2378 readNext(function (cb) {
2379 if (page == 'commits')
2380 renderPullReqCommits(req, pr, repo, headRepo, cb)
2381 else if (page == 'files')
2382 renderPullReqFiles(req, pr, repo, headRepo, cb)
2383 else cb(null,
2384 renderPullReqActivity(req, pr, repo, headRepo, authorLink, postId))
2385 })
2386 ]))
2387 }
2388
2389 function renderPullReqCommits(req, pr, baseRepo, headRepo, cb) {
2390 pullReqs.getRevs(pr.id, function (err, revs) {
2391 if (err) return cb(null, renderError(err))
2392 cb(null, cat([
2393 pull.once('<section>'),
2394 renderCommitLog(req, baseRepo, revs.base, headRepo, revs.head),
2395 pull.once('</section>')
2396 ]))
2397 })
2398 }
2399
2400 function renderPullReqFiles(req, pr, baseRepo, headRepo, cb) {
2401 pullReqs.getRevs(pr.id, function (err, revs) {
2402 if (err) return cb(null, renderError(err))
2403 cb(null, cat([
2404 pull.once('<section>'),
2405 renderDiffStat(req,
2406 [baseRepo, headRepo], [revs.base, revs.head]),
2407 pull.once('</section>')
2408 ]))
2409 })
2410 }
2411
2412 function renderPullReqActivity(req, pr, repo, headRepo, authorLink, postId) {
2413 var msgTimeLink = link([pr.id],
2414 new Date(pr.created_at).toLocaleString(req._locale))
2415 var newestMsg = {key: pr.id, value: {timestamp: pr.created_at}}
2416 var isAuthor = (myId == pr.author) || (myId == repo.feed)
2417 return cat([
2418 readOnce(function (cb) {
2419 cb(null,
2420 '<section class="collapse">' +
2421 authorLink + ' &middot; ' + msgTimeLink +
2422 markdown(pr.text, repo) + '</section>')
2423 }),
2424 // render posts, edits, and updates
2425 pull(
2426 many([
2427 ssb.links({
2428 dest: pr.id,
2429 values: true
2430 }),
2431 readNext(function (cb) {
2432 cb(null, pull(
2433 ssb.links({
2434 dest: headRepo.id,
2435 source: headRepo.feed,
2436 rel: 'repo',
2437 values: true,
2438 reverse: true
2439 }),
2440 pull.take(function (link) {
2441 return link.value.timestamp > pr.created_at
2442 }),
2443 pull.filter(function (link) {
2444 return link.value.content.type == 'git-update'
2445 && ('refs/heads/' + pr.headBranch) in link.value.content.refs
2446 })
2447 ))
2448 })
2449 ]),
2450 addAuthorName(about),
2451 pull.unique('key'),
2452 pull.through(function (msg) {
2453 if (msg.value.timestamp > newestMsg.value.timestamp)
2454 newestMsg = msg
2455 }),
2456 sortMsgs(),
2457 pull.map(function (item) {
2458 if (item.value.content.type == 'git-update')
2459 return renderBranchUpdate(req, pr, item)
2460 return renderIssueActivityMsg(req, repo, pr,
2461 req._t('pull request'), postId, item)
2462 })
2463 ),
2464 !isPublic && isAuthor && pr.open && pull.once(
2465 '<section class="merge-instructions">' +
2466 '<input type="checkbox" class="toggle" id="merge-instructions"/>' +
2467 '<h4><label for="merge-instructions" class="toggle-link"><a>' +
2468 req._t('mergeInstructions.MergeViaCmdLine') +
2469 '</a></label></h4>' +
2470 '<div class="contents">' +
2471 '<p>' + req._t('mergeInstructions.CheckOut') + '</p>' +
2472 '<pre>' +
2473 'git fetch ssb://' + escapeHTML(pr.headRepo) + ' ' +
2474 escapeHTML(pr.headBranch) + '\n' +
2475 'git checkout -b ' + escapeHTML(pr.headBranch) + ' FETCH_HEAD' +
2476 '</pre>' +
2477 '<p>' + req._t('mergeInstructions.MergeAndPush') + '</p>' +
2478 '<pre>' +
2479 'git checkout ' + escapeHTML(pr.baseBranch) + '\n' +
2480 'git merge ' + escapeHTML(pr.headBranch) + '\n' +
2481 'git push ssb ' + escapeHTML(pr.baseBranch) +
2482 '</pre>' +
2483 '</div></section>'),
2484 !isPublic && readOnce(function (cb) {
2485 cb(null, renderIssueCommentForm(req, pr, repo, newestMsg.key, isAuthor,
2486 req._t('pull request')))
2487 })
2488 ])
2489 }
2490
2491 function renderBranchUpdate(req, pr, msg) {
2492 var authorLink = link([msg.value.author], msg.authorName)
2493 var msgLink = link([msg.key],
2494 new Date(msg.value.timestamp).toLocaleString(req._locale))
2495 var rev = msg.value.content.refs['refs/heads/' + pr.headBranch]
2496 if (!rev)
2497 return '<section class="collapse">' +
2498 req._t('NameDeletedBranch', {
2499 name: authorLink,
2500 branch: '<code>' + pr.headBranch + '</code>'
2501 }) + ' &middot; ' + msgLink +
2502 '</section>'
2503
2504 var revLink = link([pr.headRepo, 'commit', rev], rev.substr(0, 8))
2505 return '<section class="collapse">' +
2506 req._t('NameUpdatedBranch', {
2507 name: authorLink,
2508 rev: '<code>' + revLink + '</code>'
2509 }) + ' &middot; ' + msgLink +
2510 '</section>'
2511 }
2512
2513 /* Compare changes */
2514
2515 function serveRepoCompare(req, repo) {
2516 var query = req._u.query
2517 var base
2518 var count = 0
2519
2520 return renderRepoPage(req, repo, 'pulls', null, cat([
2521 pull.once('<h3>' + req._t('CompareChanges') + '</h3>' +
2522 '<form action="' + encodeLink(repo.id) + '/comparing" method="get">' +
2523 '<section>'),
2524 pull.once(req._t('BaseBranch') + ': '),
2525 readNext(function (cb) {
2526 if (query.base) gotBase(null, query.base)
2527 else repo.getSymRef('HEAD', true, gotBase)
2528 function gotBase(err, ref) {
2529 if (err) return cb(err)
2530 cb(null, branchMenu(repo, 'base', base = ref || 'HEAD'))
2531 }
2532 }),
2533 pull.once('<br/>' + req._t('ComparisonRepoBranch') + ':'),
2534 pull(
2535 getForks(repo, true),
2536 pull.asyncMap(function (msg, cb) {
2537 getRepo(msg.key, function (err, repo) {
2538 if (err) return cb(err)
2539 cb(null, {
2540 msg: msg,
2541 repo: repo
2542 })
2543 })
2544 }),
2545 pull.map(renderFork),
2546 pull.flatten()
2547 ),
2548 pull.once('</section>'),
2549 readOnce(function (cb) {
2550 cb(null, count == 0 ? req._t('NoBranches') :
2551 '<button type="submit" class="btn">' +
2552 req._t('Compare') + '</button>')
2553 }),
2554 pull.once('</form>')
2555 ]))
2556
2557 function renderFork(fork) {
2558 return pull(
2559 fork.repo.refs(),
2560 pull.map(function (ref) {
2561 var m = /^refs\/([^\/]*)\/(.*)$/.exec(ref.name) || [,ref.name]
2562 return {
2563 type: m[1],
2564 name: m[2],
2565 value: ref.value
2566 }
2567 }),
2568 pull.filter(function (ref) {
2569 return ref.type == 'heads'
2570 && !(ref.name == base && fork.msg.key == repo.id)
2571 }),
2572 pull.map(function (ref) {
2573 var branchLink = link([fork.msg.key, 'tree', ref.name], ref.name)
2574 var authorLink = link([fork.msg.value.author], fork.msg.authorName)
2575 var repoLink = link([fork.msg.key], fork.msg.repoName)
2576 var value = fork.msg.key + ':' + ref.name
2577 count++
2578 return '<div class="bgslash">' +
2579 '<input type="radio" name="head"' +
2580 ' value="' + escapeHTML(value) + '"' +
2581 (query.head == value ? ' checked="checked"' : '') + '> ' +
2582 authorLink + ' / ' + repoLink + ' / ' + branchLink + '</div>'
2583 })
2584 )
2585 }
2586 }
2587
2588 function serveRepoComparing(req, repo) {
2589 var query = req._u.query
2590 var baseBranch = query.base
2591 var s = (query.head || '').split(':')
2592
2593 if (!s || !baseBranch)
2594 return serveRedirect(req, encodeLink([repo.id, 'compare']))
2595
2596 var headRepoId = s[0]
2597 var headBranch = s[1]
2598 var baseLink = link([repo.id, 'tree', baseBranch])
2599 var headBranchLink = link([headRepoId, 'tree', headBranch])
2600 var backHref = encodeLink([repo.id, 'compare']) + req._u.search
2601
2602 return renderRepoPage(req, repo, 'pulls', null, cat([
2603 pull.once('<h3>' +
2604 req._t(query.expand ? 'OpenPullRequest': 'ComparingChanges') +
2605 '</h3>'),
2606 readNext(function (cb) {
2607 getRepo(headRepoId, function (err, headRepo) {
2608 if (err) return cb(err)
2609 getRepoFullName(about, headRepo.feed, headRepo.id,
2610 function (err, repoName, authorName) {
2611 if (err) return cb(err)
2612 cb(null, renderRepoInfo(Repo(headRepo), repoName, authorName))
2613 }
2614 )
2615 })
2616 })
2617 ]))
2618
2619 function renderRepoInfo(headRepo, headRepoName, headRepoAuthorName) {
2620 var authorLink = link([headRepo.feed], headRepoAuthorName)
2621 var repoLink = link([headRepoId], headRepoName)
2622 return cat([
2623 pull.once('<section>' +
2624 req._t('Base') + ': ' + baseLink + '<br/>' +
2625 req._t('Head') + ': ' +
2626 '<span class="bgslash">' + authorLink + ' / ' + repoLink +
2627 ' / ' + headBranchLink + '</span>' +
2628 '</section>' +
2629 (query.expand ? '<section><form method="post" action="">' +
2630 hiddenInputs({
2631 action: 'new-pull',
2632 branch: baseBranch,
2633 head_repo: headRepoId,
2634 head_branch: headBranch
2635 }) +
2636 '<input class="wide-input" name="title"' +
2637 ' placeholder="' + req._t('Title') + '" size="77"/>' +
2638 renderPostForm(req, repo, req._t('Description'), 8) +
2639 '<button type="submit" class="btn open">' +
2640 req._t('Create') + '</button>' +
2641 '</form></section>'
2642 : '<section><form method="get" action="">' +
2643 hiddenInputs({
2644 base: baseBranch,
2645 head: query.head
2646 }) +
2647 '<button class="btn open" type="submit" name="expand" value="1">' +
2648 '<i>โŽ‡</i> ' + req._t('CreatePullRequest') + '</button> ' +
2649 '<a href="' + backHref + '">' + req._t('Back') + '</a>' +
2650 '</form></section>') +
2651 '<div id="commits"></div>' +
2652 '<div class="tab-links">' +
2653 '<a href="#" id="files-link">' + req._t('FilesChanged') + '</a> ' +
2654 '<a href="#commits" id="commits-link">' +
2655 req._t('Commits') + '</a>' +
2656 '</div>' +
2657 '<section id="files-tab">'),
2658 renderDiffStat(req, [repo, headRepo], [baseBranch, headBranch]),
2659 pull.once('</section>' +
2660 '<section id="commits-tab">'),
2661 renderCommitLog(req, repo, baseBranch, headRepo, headBranch),
2662 pull.once('</section>')
2663 ])
2664 }
2665 }
2666
2667 function renderCommitLog(req, baseRepo, baseBranch, headRepo, headBranch) {
2668 return cat([
2669 pull.once('<table class="compare-commits">'),
2670 readNext(function (cb) {
2671 baseRepo.resolveRef(baseBranch, function (err, baseBranchRev) {
2672 if (err) return cb(err)
2673 var currentDay
2674 return cb(null, pull(
2675 headRepo.readLog(headBranch),
2676 pull.take(function (rev) { return rev != baseBranchRev }),
2677 pullReverse(),
2678 paramap(headRepo.getCommitParsed.bind(headRepo), 8),
2679 pull.map(function (commit) {
2680 var commitPath = [headRepo.id, 'commit', commit.id]
2681 var commitIdShort = '<tt>' + commit.id.substr(0, 8) + '</tt>'
2682 var day = Math.floor(commit.author.date / 86400000)
2683 var dateRow = day == currentDay ? '' :
2684 '<tr><th colspan=3 class="date-info">' +
2685 commit.author.date.toLocaleDateString(req._locale) +
2686 '</th><tr>'
2687 currentDay = day
2688 return dateRow + '<tr>' +
2689 '<td>' + escapeHTML(commit.author.name) + '</td>' +
2690 '<td>' + link(commitPath, commit.title) + '</td>' +
2691 '<td>' + link(commitPath, commitIdShort, true) + '</td>' +
2692 '</tr>'
2693 })
2694 ))
2695 })
2696 }),
2697 pull.once('</table>')
2698 ])
2699 }
2700}
2701

Built with git-ssb-web