git ssb

30+

cel / git-ssb-web



Tree: 1ba0ea701968920d00242b73ce3b017bbcffec28

Files: 1ba0ea701968920d00242b73ce3b017bbcffec28 / index.js

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

Built with git-ssb-web