git ssb

30+

cel / git-ssb-web



Tree: 28ee0655c00f29d4d71fe8f7ed7d0913771291ff

Files: 28ee0655c00f29d4d71fe8f7ed7d0913771291ff / index.js

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

Built with git-ssb-web