git ssb

30+

cel / git-ssb-web



Tree: d1e96510b7246fe9b0d685d132cfdfe322caa8f7

Files: d1e96510b7246fe9b0d685d132cfdfe322caa8f7 / index.js

57814 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 paramap = require('pull-paramap')
20var gitPack = require('pull-git-pack')
21var Mentions = require('ssb-mentions')
22var Highlight = require('highlight.js')
23var JsDiff = require('diff')
24
25var hlCssPath = path.resolve(require.resolve('highlight.js'), '../../styles')
26
27// render links to git objects and ssb objects
28var blockRenderer = new marked.Renderer()
29blockRenderer.urltransform = function (url) {
30 if (ref.isLink(url))
31 return encodeLink(url)
32 if (/^[0-9a-f]{40}$/.test(url) && this.options.repo)
33 return encodeLink([this.options.repo.id, 'commit', url])
34 return url
35}
36
37function getExtension(filename) {
38 return (/\.([^.]+)$/.exec(filename) || [,filename])[1]
39}
40
41function highlight(code, lang) {
42 try {
43 return lang
44 ? Highlight.highlight(lang, code).value
45 : Highlight.highlightAuto(code).value
46 } catch(e) {
47 if (/^Unknown language/.test(e.message))
48 return escapeHTML(code)
49 throw e
50 }
51}
52
53marked.setOptions({
54 gfm: true,
55 mentions: true,
56 tables: true,
57 breaks: true,
58 pedantic: false,
59 sanitize: true,
60 smartLists: true,
61 smartypants: false,
62 highlight: highlight,
63 renderer: blockRenderer
64})
65
66// hack to make git link mentions work
67var mdRules = new marked.InlineLexer(1, marked.defaults).rules
68mdRules.mention =
69 /^(\s)?([@%&][A-Za-z0-9\._\-+=\/]*[A-Za-z0-9_\-+=\/]|[0-9a-f]{40})/
70mdRules.text = /^[\s\S]+?(?=[\\<!\[_*`]| {2,}\n| [@%&]|[0-9a-f]{40}|$)/
71
72function markdown(text, repo, cb) {
73 if (!text) return ''
74 if (typeof text != 'string') text = String(text)
75 return marked(text, {repo: repo}, cb)
76}
77
78function parseAddr(str, def) {
79 if (!str) return def
80 var i = str.lastIndexOf(':')
81 if (~i) return {host: str.substr(0, i), port: str.substr(i+1)}
82 if (isNaN(str)) return {host: str, port: def.port}
83 return {host: def.host, port: str}
84}
85
86function isArray(arr) {
87 return Object.prototype.toString.call(arr) == '[object Array]'
88}
89
90function encodeLink(url) {
91 if (!isArray(url)) url = [url]
92 return '/' + url.map(encodeURIComponent).join('/')
93}
94
95function link(parts, text, raw, props) {
96 if (text == null) text = parts[parts.length-1]
97 if (!raw) text = escapeHTML(text)
98 return '<a href="' + encodeLink(parts) + '"' +
99 (props ? ' ' + props : '') +
100 '>' + text + '</a>'
101}
102
103function linkify(text) {
104 // regex is from ssb-ref
105 return text.replace(/(@|%|&)[A-Za-z0-9\/+]{43}=\.[\w\d]+/g, function (str) {
106 return '<a href="/' + encodeURIComponent(str) + '">' + str + '</a>'
107 })
108}
109
110function timestamp(time) {
111 time = Number(time)
112 var d = new Date(time)
113 return '<span title="' + time + '">' + d.toLocaleString() + '</span>'
114}
115
116function pre(text) {
117 return '<pre>' + escapeHTML(text) + '</pre>'
118}
119
120function json(obj) {
121 return linkify(pre(JSON.stringify(obj, null, 2)))
122}
123
124function escapeHTML(str) {
125 return String(str)
126 .replace(/&/g, '&amp;')
127 .replace(/</g, '&lt;')
128 .replace(/>/g, '&gt;')
129 .replace(/"/g, '&quot;')
130}
131
132function ucfirst(str) {
133 return str[0].toLocaleUpperCase() + str.slice(1)
134}
135
136function table(props) {
137 return function (read) {
138 return cat([
139 pull.once('<table' + (props ? ' ' + props : '') + '>'),
140 pull(
141 read,
142 pull.map(function (row) {
143 return row ? '<tr>' + row.map(function (cell) {
144 return '<td>' + cell + '</td>'
145 }).join('') + '</tr>' : ''
146 })
147 ),
148 pull.once('</table>')
149 ])
150 }
151}
152
153function ul(props) {
154 return function (read) {
155 return cat([
156 pull.once('<ul' + (props ? ' ' + props : '') + '>'),
157 pull(
158 read,
159 pull.map(function (li) {
160 return '<li>' + li + '</li>'
161 })
162 ),
163 pull.once('</ul>')
164 ])
165 }
166}
167
168function nav(links, page, after) {
169 return ['<nav>'].concat(
170 links.map(function (link) {
171 var href = typeof link[0] == 'string' ? link[0] : encodeLink(link[0])
172 var props = link[2] == page ? ' class="active"' : ''
173 return '<a href="' + href + '"' + props + '>' + link[1] + '</a>'
174 }), after || '', '</nav>').join('')
175}
176
177function renderNameForm(enabled, id, name, action, inputId, title, header) {
178 if (!inputId) inputId = action
179 return '<form class="petname" action="" method="post">' +
180 (enabled ?
181 '<input type="checkbox" class="name-checkbox" id="' + inputId + '" ' +
182 'onfocus="this.form.name.focus()" />' +
183 '<input name="name" class="name" value="' + escapeHTML(name) + '" ' +
184 'onkeyup="if (event.keyCode == 27) this.form.reset()" />' +
185 '<input type="hidden" name="action" value="' + action + '">' +
186 '<input type="hidden" name="id" value="' +
187 escapeHTML(id) + '">' +
188 '<label class="name-toggle" for="' + inputId + '" ' +
189 'title="' + title + '"><i>✍</i></label> ' +
190 '<input class="btn name-btn" type="submit" value="Rename">' +
191 header :
192 header + '<br clear="all"/>'
193 ) +
194 '</form>'
195}
196
197function renderPostForm(repo, placeholder, rows) {
198 return '<input type="radio" class="tab-radio" id="tab1" name="tab" checked="checked"/>' +
199 '<input type="radio" class="tab-radio" id="tab2" name="tab"/>' +
200 '<div id="tab-links" class="tab-links" style="display:none">' +
201 '<label for="tab1" id="write-tab-link" class="tab1-link">Write</label>' +
202 '<label for="tab2" id="preview-tab-link" class="tab2-link">Preview</label>' +
203 '</div>' +
204 '<input type="hidden" id="repo-id" value="' + repo.id + '"/>' +
205 '<div id="write-tab" class="tab1">' +
206 '<textarea id="post-text" name="text" class="wide-input"' +
207 ' rows="' + (rows||4) + '" cols="77"' +
208 (placeholder ? ' placeholder="' + placeholder + '"' : '') +
209 '></textarea>' +
210 '</div>' +
211 '<div class="preview-text tab2" id="preview-tab"></div>' +
212 '<script>' + issueCommentScript + '</script>'
213}
214
215function readNext(fn) {
216 var next
217 return function (end, cb) {
218 if (next) return next(end, cb)
219 fn(function (err, _next) {
220 if (err) return cb(err)
221 next = _next
222 next(null, cb)
223 })
224 }
225}
226
227function readOnce(fn) {
228 var ended
229 return function (end, cb) {
230 fn(function (err, data) {
231 if (err || ended) return cb(err || ended)
232 ended = true
233 cb(null, data)
234 })
235 }
236}
237
238function paginate(onFirst, through, onLast, onEmpty) {
239 var ended, last, first = true, queue = []
240 return function (read) {
241 var mappedRead = through(function (end, cb) {
242 if (ended = end) return read(ended, cb)
243 if (queue.length)
244 return cb(null, queue.shift())
245 read(null, function (end, data) {
246 if (end) return cb(end)
247 last = data
248 cb(null, data)
249 })
250 })
251 return function (end, cb) {
252 var tmp
253 if (ended) return cb(ended)
254 if (ended = end) return read(ended, cb)
255 if (first)
256 return read(null, function (end, data) {
257 if (ended = end) {
258 if (end === true && onEmpty)
259 return onEmpty(cb)
260 return cb(ended)
261 }
262 first = false
263 last = data
264 queue.push(data)
265 if (onFirst)
266 onFirst(data, cb)
267 else
268 mappedRead(null, cb)
269 })
270 mappedRead(null, function (end, data) {
271 if (ended = end) {
272 if (end === true && last)
273 return onLast(last, cb)
274 }
275 cb(end, data)
276 })
277 }
278 }
279}
280
281function readObjectString(obj, cb) {
282 pull(obj.read, pull.collect(function (err, bufs) {
283 if (err) return cb(err)
284 cb(null, Buffer.concat(bufs, obj.length).toString('utf8'))
285 }))
286}
287
288function getRepoObjectString(repo, id, cb) {
289 if (!id) return cb(null, '')
290 repo.getObjectFromAny(id, function (err, obj) {
291 if (err) return cb(err)
292 readObjectString(obj, cb)
293 })
294}
295
296function compareMsgs(a, b) {
297 return (a.value.timestamp - b.value.timestamp) || (a.key - b.key)
298}
299
300function pullSort(comparator) {
301 return function (read) {
302 return readNext(function (cb) {
303 pull(read, pull.collect(function (err, items) {
304 if (err) return cb(err)
305 items.sort(comparator)
306 cb(null, pull.values(items))
307 }))
308 })
309 }
310}
311
312function sortMsgs() {
313 return pullSort(compareMsgs)
314}
315
316function pullReverse() {
317 return function (read) {
318 return readNext(function (cb) {
319 pull(read, pull.collect(function (err, items) {
320 cb(err, items && pull.values(items.reverse()))
321 }))
322 })
323 }
324}
325
326function tryDecodeURIComponent(str) {
327 if (!str || (str[0] == '%' && ref.isBlobId(str)))
328 return str
329 try {
330 str = decodeURIComponent(str)
331 } finally {
332 return str
333 }
334}
335
336function getRepoName(about, ownerId, repoId, cb) {
337 about.getName({
338 owner: ownerId,
339 target: repoId,
340 toString: function () {
341 // hack to fit two parameters into asyncmemo
342 return ownerId + '/' + repoId
343 }
344 }, cb)
345}
346
347function addAuthorName(about) {
348 return paramap(function (msg, cb) {
349 about.getName(msg.value.author, function (err, authorName) {
350 msg.authorName = authorName
351 cb(err, msg)
352 })
353 }, 8)
354}
355
356function getMention(msg, id) {
357 if (msg.key == id) return msg
358 var mentions = msg.value.content.mentions
359 if (mentions) for (var i = 0; i < mentions.length; i++) {
360 var mention = mentions[i]
361 if (mention.link == id)
362 return mention
363 }
364 return null
365}
366
367var hasOwnProp = Object.prototype.hasOwnProperty
368
369function getContentType(filename) {
370 var ext = filename.split('.').pop()
371 return hasOwnProp.call(contentTypes, ext)
372 ? contentTypes[ext]
373 : 'text/plain; charset=utf-8'
374}
375
376var contentTypes = {
377 css: 'text/css'
378}
379
380function readReqJSON(req, cb) {
381 pull(
382 toPull(req),
383 pull.collect(function (err, bufs) {
384 if (err) return cb(err)
385 var data
386 try {
387 data = qs.parse(Buffer.concat(bufs).toString('ascii'))
388 } catch(e) {
389 return cb(e)
390 }
391 cb(null, data)
392 })
393 )
394}
395
396var issueCommentScript = '(' + function () {
397 var $ = document.getElementById.bind(document)
398 $('tab-links').style.display = 'block'
399 $('preview-tab-link').onclick = function (e) {
400 with (new XMLHttpRequest()) {
401 open('POST', '', true)
402 onload = function() {
403 $('preview-tab').innerHTML = responseText
404 }
405 send('action=markdown' +
406 '&repo=' + encodeURIComponent($('repo-id').value) +
407 '&text=' + encodeURIComponent($('post-text').value))
408 }
409 }
410}.toString() + ')()'
411
412var hashHighlightScript = '<script>(' + function () {
413 var activeEl
414 function onHashChange() {
415 var el = document.getElementById(location.hash.substr(1))
416 if (activeEl)
417 activeEl.classList.remove('active-hash')
418 if (el)
419 el.classList.add('active-hash')
420 activeEl = el
421 }
422 onHashChange()
423 window.addEventListener('hashchange', onHashChange, false)
424}.toString() + ')()</script>'
425
426var msgTypes = {
427 'git-repo': true,
428 'git-update': true,
429 'issue': true
430}
431
432var imgMimes = {
433 png: 'image/png',
434 jpeg: 'image/jpeg',
435 jpg: 'image/jpeg',
436 gif: 'image/gif',
437 tif: 'image/tiff',
438 svg: 'image/svg+xml',
439 bmp: 'image/bmp'
440}
441
442module.exports = function (opts, cb) {
443 var ssb, reconnect, myId, getRepo, getVotes, getMsg, issues
444 var about = function (id, cb) { cb(null, {name: id}) }
445 var reqQueue = []
446 var isPublic = opts.public
447 var ssbAppname = opts.appname || 'ssb'
448
449 var addr = parseAddr(opts.listenAddr, {host: 'localhost', port: 7718})
450 http.createServer(onRequest).listen(addr.port, addr.host, onListening)
451
452 var server = {
453 setSSB: function (_ssb, _reconnect) {
454 _ssb.whoami(function (err, feed) {
455 if (err) throw err
456 ssb = _ssb
457 reconnect = _reconnect
458 myId = feed.id
459 about = ssbAbout(ssb, myId)
460 while (reqQueue.length)
461 onRequest.apply(this, reqQueue.shift())
462 getRepo = asyncMemo(function (id, cb) {
463 getMsg(id, function (err, msg) {
464 if (err) return cb(err)
465 ssbGit.getRepo(ssb, {key: id, value: msg}, {live: true}, cb)
466 })
467 })
468 getVotes = ssbVotes(ssb)
469 getMsg = asyncMemo(ssb.get)
470 issues = Issues.init(ssb)
471 })
472 }
473 }
474
475 function onListening() {
476 var host = ~addr.host.indexOf(':') ? '[' + addr.host + ']' : addr.host
477 console.log('Listening on http://' + host + ':' + addr.port + '/')
478 cb(null, server)
479 }
480
481 /* Serving a request */
482
483 function onRequest(req, res) {
484 console.log(req.method, req.url)
485 if (!ssb) return reqQueue.push(arguments)
486 pull(
487 handleRequest(req),
488 pull.filter(function (data) {
489 if (Array.isArray(data)) {
490 res.writeHead.apply(res, data)
491 return false
492 }
493 return true
494 }),
495 toPull(res)
496 )
497 }
498
499 function handleRequest(req) {
500 var u = req._u = url.parse(req.url, true)
501 var path = u.pathname.slice(1)
502 var dirs = ref.isLink(path) ? [path] :
503 path.split(/\/+/).map(tryDecodeURIComponent)
504 var dir = dirs[0]
505
506 if (req.method == 'POST') {
507 if (isPublic)
508 return servePlainError(405, 'POST not allowed on public site')
509 return readNext(function (cb) {
510 readReqJSON(req, function (err, data) {
511 if (err) return cb(null, serveError(err, 400))
512 if (!data) return cb(null, serveError(new Error('No data'), 400))
513
514 switch (data.action) {
515 case 'vote':
516 var voteValue = +data.vote || 0
517 if (!data.id)
518 return cb(null, serveError(new Error('Missing vote id'), 400))
519 var msg = schemas.vote(data.id, voteValue)
520 return ssb.publish(msg, function (err) {
521 if (err) return cb(null, serveError(err))
522 cb(null, serveRedirect(req.url))
523 })
524 return
525
526 case 'repo-name':
527 if (!data.name)
528 return cb(null, serveError(new Error('Missing name'), 400))
529 if (!data.id)
530 return cb(null, serveError(new Error('Missing id'), 400))
531 var msg = schemas.name(data.id, data.name)
532 return ssb.publish(msg, function (err) {
533 if (err) return cb(null, serveError(err))
534 cb(null, serveRedirect(req.url))
535 })
536
537 case 'issue-title':
538 if (!data.name)
539 return cb(null, serveError(new Error('Missing name'), 400))
540 if (!data.id)
541 return cb(null, serveError(new Error('Missing id'), 400))
542 var msg = Issues.schemas.edit(data.id, {title: data.name})
543 return ssb.publish(msg, function (err) {
544 if (err) return cb(null, serveError(err))
545 cb(null, serveRedirect(req.url))
546 })
547
548 case 'comment':
549 if (!data.id)
550 return cb(null, serveError(new Error('Missing id'), 400))
551
552 var msg = schemas.post(data.text, data.id, data.branch || data.id)
553 msg.issue = data.issue
554 msg.repo = data.repo
555 if (data.open != null)
556 Issues.schemas.opens(msg, data.id)
557 if (data.close != null)
558 Issues.schemas.closes(msg, data.id)
559 var mentions = Mentions(data.text)
560 if (mentions.length)
561 msg.mentions = mentions
562 return ssb.publish(msg, function (err) {
563 if (err) return cb(null, serveError(err))
564 cb(null, serveRedirect(req.url))
565 })
566
567 case 'new-issue':
568 var msg = Issues.schemas.new(dir, data.title, data.text)
569 var mentions = Mentions(data.text)
570 if (mentions.length)
571 msg.mentions = mentions
572 return ssb.publish(msg, function (err, msg) {
573 if (err) return cb(null, serveError(err))
574 cb(null, serveRedirect(encodeLink(msg.key)))
575 })
576
577 case 'markdown':
578 return cb(null, serveMarkdown(data.text, {id: data.repo}))
579
580 default:
581 cb(null, servePlainError(400, 'What are you trying to do?'))
582 }
583 })
584 })
585 }
586
587 if (dir == '')
588 return serveIndex(req)
589 else if (ref.isBlobId(dir))
590 return serveBlob(req, dir)
591 else if (ref.isMsgId(dir))
592 return serveMessage(req, dir, dirs.slice(1))
593 else if (ref.isFeedId(dir))
594 return serveUserPage(req, dir, dirs.slice(1))
595 else if (dir == 'static')
596 return serveFile(req, dirs)
597 else if (dir == 'highlight')
598 return serveFile(req, [hlCssPath].concat(dirs.slice(1)), true)
599 else
600 return serve404(req)
601 }
602
603 function serveFile(req, dirs, outside) {
604 var filename = path.resolve.apply(path, [__dirname].concat(dirs))
605 // prevent escaping base dir
606 if (!outside && filename.indexOf('../') === 0)
607 return servePlainError(403, '403 Forbidden')
608
609 return readNext(function (cb) {
610 fs.stat(filename, function (err, stats) {
611 cb(null, err ?
612 err.code == 'ENOENT' ? serve404(req)
613 : servePlainError(500, err.message)
614 : 'if-modified-since' in req.headers &&
615 new Date(req.headers['if-modified-since']) >= stats.mtime ?
616 pull.once([304])
617 : stats.isDirectory() ?
618 servePlainError(403, 'Directory not listable')
619 : cat([
620 pull.once([200, {
621 'Content-Type': getContentType(filename),
622 'Content-Length': stats.size,
623 'Last-Modified': stats.mtime.toGMTString()
624 }]),
625 toPull(fs.createReadStream(filename))
626 ]))
627 })
628 })
629 }
630
631 function servePlainError(code, msg) {
632 return pull.values([
633 [code, {
634 'Content-Length': Buffer.byteLength(msg),
635 'Content-Type': 'text/plain; charset=utf-8'
636 }],
637 msg
638 ])
639 }
640
641 function serve404(req) {
642 return servePlainError(404, '404 Not Found')
643 }
644
645 function serveRedirect(path) {
646 var msg = '<!doctype><html><head><meta charset=utf-8>' +
647 '<title>Redirect</title></head>' +
648 '<body><p><a href="' + path + '">Continue</a></p></body></html>'
649 return pull.values([
650 [302, {
651 'Content-Length': Buffer.byteLength(msg),
652 'Content-Type': 'text/html',
653 Location: path
654 }],
655 msg
656 ])
657 }
658
659 function serveMarkdown(text, repo) {
660 var html = markdown(text, repo)
661 return pull.values([
662 [200, {
663 'Content-Length': Buffer.byteLength(html),
664 'Content-Type': 'text/html; charset=utf-8'
665 }],
666 html
667 ])
668 }
669
670 function renderTry(read) {
671 var ended
672 return function (end, cb) {
673 if (ended) return cb(ended)
674 read(end, function (err, data) {
675 if (err === true)
676 cb(true)
677 else if (err) {
678 ended = true
679 cb(null,
680 '<h3>' + err.name + '</h3>' +
681 '<pre>' + escapeHTML(err.stack) + '</pre>')
682 } else
683 cb(null, data)
684 })
685 }
686 }
687
688 function serveTemplate(title, code, read) {
689 if (read === undefined) return serveTemplate.bind(this, title, code)
690 return cat([
691 pull.values([
692 [code || 200, {
693 'Content-Type': 'text/html'
694 }],
695 '<!doctype html><html><head><meta charset=utf-8>',
696 '<title>' + escapeHTML(title || 'git ssb') + '</title>',
697 '<link rel=stylesheet href="/static/styles.css"/>',
698 '<link rel=stylesheet href="/highlight/github.css"/>',
699 '</head>\n',
700 '<body>',
701 '<header>',
702 '<h1><a href="/">git ssb' +
703 (ssbAppname != 'ssb' ? ' <sub>' + ssbAppname + '</sub>' : '') +
704 '</a></h1>',
705 '</header>',
706 '<article>']),
707 renderTry(read),
708 pull.once('<hr/></article></body></html>')
709 ])
710 }
711
712 function serveError(err, status) {
713 if (err.message == 'stream is closed')
714 reconnect()
715 return pull(
716 pull.once(
717 '<h2>' + err.name + '</h3>' +
718 '<pre>' + escapeHTML(err.stack) + '</pre>'),
719 serveTemplate(err.name, status || 500)
720 )
721 }
722
723 function renderObjectData(obj, filename, repo) {
724 var ext = getExtension(filename)
725 return readOnce(function (cb) {
726 readObjectString(obj, function (err, buf) {
727 buf = buf.toString('utf8')
728 if (err) return cb(err)
729 cb(null, (ext == 'md' || ext == 'markdown')
730 ? markdown(buf, repo)
731 : renderCodeTable(buf, ext) + hashHighlightScript)
732 })
733 })
734 }
735
736 function renderCodeTable(buf, ext) {
737 return '<pre><table class="code">' +
738 highlight(buf, ext).split('\n').map(function (line, i) {
739 i++
740 return '<tr id="L' + i + '">' +
741 '<td class="code-linenum">' + '<a href="#L' + i + '">' + i + '</td>' +
742 '<td class="code-text">' + line + '</td></tr>'
743 }).join('') +
744 '</table></pre>'
745 }
746
747 /* Feed */
748
749 function renderFeed(req, feedId) {
750 var query = req._u.query
751 var opts = {
752 reverse: !query.forwards,
753 lt: query.lt && +query.lt || Date.now(),
754 gt: query.gt && +query.gt,
755 id: feedId
756 }
757 return pull(
758 feedId ? ssb.createUserStream(opts) : ssb.createFeedStream(opts),
759 pull.filter(function (msg) {
760 return msg.value.content.type in msgTypes
761 }),
762 pull.take(20),
763 addAuthorName(about),
764 query.forwards && pullReverse(),
765 paginate(
766 function (first, cb) {
767 if (!query.lt && !query.gt) return cb(null, '')
768 var gt = feedId ? first.value.sequence : first.value.timestamp + 1
769 var q = qs.stringify({
770 gt: gt,
771 forwards: 1
772 })
773 cb(null, '<a href="?' + q + '">Newer</a>')
774 },
775 paramap(renderFeedItem, 8),
776 function (last, cb) {
777 cb(null, '<a href="?' + qs.stringify({
778 lt: feedId ? last.value.sequence : last.value.timestamp - 1
779 }) + '">Older</a>')
780 },
781 function (cb) {
782 cb(null, query.forwards ?
783 '<a href="?lt=' + (opts.gt + 1) + '">Older</a>' :
784 '<a href="?gt=' + (opts.lt - 1) + '&amp;forwards=1">Newer</a>')
785 }
786 )
787 )
788 }
789
790 function renderFeedItem(msg, cb) {
791 var c = msg.value.content
792 var msgLink = link([msg.key],
793 new Date(msg.value.timestamp).toLocaleString())
794 var author = msg.value.author
795 var authorLink = link([msg.value.author], msg.authorName)
796 switch (c.type) {
797 case 'git-repo':
798 return getRepoName(about, author, msg.key, function (err, repoName) {
799 if (err) return cb(err)
800 var repoLink = link([msg.key], repoName)
801 cb(null, '<section class="collapse">' + msgLink + '<br>' +
802 authorLink + ' created repo ' + repoLink + '</section>')
803 })
804 case 'git-update':
805 return getRepoName(about, author, c.repo, function (err, repoName) {
806 if (err) return cb(err)
807 var repoLink = link([c.repo], repoName)
808 cb(null, '<section class="collapse">' + msgLink + '<br>' +
809 authorLink + ' pushed to ' + repoLink + '</section>')
810 })
811 case 'issue':
812 var issueLink = link([msg.key], c.title)
813 return getRepoName(about, author, c.project, function (err, repoName) {
814 if (err) return cb(err)
815 var repoLink = link([c.project], repoName)
816 cb(null, '<section class="collapse">' + msgLink + '<br>' +
817 authorLink + ' opened issue ' + issueLink +
818 ' on ' + repoLink + '</section>')
819 })
820 }
821 }
822
823 /* Index */
824
825 function serveIndex(req) {
826 return serveTemplate('git ssb')(renderFeed(req))
827 }
828
829 function serveUserPage(req, feedId, dirs) {
830 switch (dirs[0]) {
831 case undefined:
832 case '':
833 case 'activity':
834 return serveUserActivity(req, feedId)
835 case 'repos':
836 return serveUserRepos(feedId)
837 }
838 }
839
840 function renderUserPage(feedId, page, body) {
841 return serveTemplate(feedId)(cat([
842 readOnce(function (cb) {
843 about.getName(feedId, function (err, name) {
844 cb(null, '<h2>' + link([feedId], name) +
845 '<code class="user-id">' + feedId + '</code></h2>' +
846 nav([
847 [[feedId], 'Activity', 'activity'],
848 [[feedId, 'repos'], 'Repos', 'repos']
849 ], page))
850 })
851 }),
852 body,
853 ]))
854 }
855
856 function serveUserActivity(req, feedId) {
857 return renderUserPage(feedId, 'activity', renderFeed(req, feedId))
858 }
859
860 function serveUserRepos(feedId) {
861 return renderUserPage(feedId, 'repos', pull(
862 ssb.messagesByType({
863 type: 'git-repo',
864 reverse: true
865 }),
866 pull.filter(function (msg) {
867 return msg.value.author == feedId
868 }),
869 pull.take(20),
870 paramap(function (msg, cb) {
871 getRepoName(about, feedId, msg.key, function (err, repoName) {
872 if (err) return cb(err)
873 cb(null, '<section class="collapse">' +
874 link([msg.key], repoName) +
875 '</section>')
876 })
877 }, 8)
878 ))
879 }
880
881 /* Message */
882
883 function serveMessage(req, id, path) {
884 return readNext(function (cb) {
885 ssb.get(id, function (err, msg) {
886 if (err) return cb(null, serveError(err))
887 var c = msg.content || {}
888 switch (c.type) {
889 case 'git-repo':
890 return getRepo(id, function (err, repo) {
891 if (err) return cb(null, serveError(err))
892 cb(null, serveRepoPage(req, Repo(repo), path))
893 })
894 case 'git-update':
895 return getRepo(c.repo, function (err, repo) {
896 if (err) return cb(null, serveRepoNotFound(c.repo, err))
897 cb(null, serveRepoUpdate(req, Repo(repo), id, msg, path))
898 })
899 case 'issue':
900 return getRepo(c.project, function (err, repo) {
901 if (err) return cb(null, serveRepoNotFound(c.project, err))
902 issues.get(id, function (err, issue) {
903 if (err) return cb(null, serveError(err))
904 cb(null, serveRepoIssue(req, Repo(repo), issue, path))
905 })
906 })
907 case 'post':
908 if (ref.isMsgId(c.issue) && ref.isMsgId(c.repo)) {
909 var done = multicb({ pluck: 1, spread: true })
910 getRepo(c.repo, done())
911 issues.get(c.issue, done())
912 return done(function (err, repo, issue) {
913 if (err) {
914 if (!repo) return cb(null, serveRepoNotFound(c.repo, err))
915 return cb(null, serveError(err))
916 }
917 cb(null, serveRepoIssue(req, Repo(repo), issue, path, id))
918 })
919 }
920 // fallthrough
921 default:
922 if (ref.isMsgId(c.repo))
923 return getRepo(c.repo, function (err, repo) {
924 if (err) return cb(null, serveRepoNotFound(c.repo, err))
925 cb(null, serveRepoSomething(req, Repo(repo), id, msg, path))
926 })
927 else
928 return cb(null, serveGenericMessage(req, id, msg, path))
929 }
930 })
931 })
932 }
933
934 function serveGenericMessage(req, id, msg, path) {
935 return serveTemplate(id)(pull.once(
936 '<section><h2>' + link([id]) + '</h2>' +
937 json(msg) +
938 '</section>'))
939 }
940
941 /* Repo */
942
943 function serveRepoPage(req, repo, path) {
944 var defaultBranch = 'master'
945 var query = req._u.query
946
947 if (query.rev != null) {
948 // Allow navigating revs using GET query param.
949 // Replace the branch in the path with the rev query value
950 path[0] = path[0] || 'tree'
951 path[1] = query.rev
952 req._u.pathname = encodeLink([repo.id].concat(path))
953 delete req._u.query.rev
954 delete req._u.search
955 return serveRedirect(url.format(req._u))
956 }
957
958 // get branch
959 return path[1] ?
960 serveRepoPage2(req, repo, path) :
961 readNext(function (cb) {
962 // TODO: handle this in pull-git-repo or ssb-git-repo
963 repo.getSymRef('HEAD', true, function (err, ref) {
964 if (err) return cb(err)
965 repo.resolveRef(ref, function (err, rev) {
966 path[1] = rev ? ref : null
967 cb(null, serveRepoPage2(req, repo, path))
968 })
969 })
970 })
971 }
972
973 function serveRepoPage2(req, repo, path) {
974 var branch = path[1]
975 var filePath = path.slice(2)
976 switch (path[0]) {
977 case undefined:
978 case '':
979 return serveRepoTree(repo, branch, [])
980 case 'activity':
981 return serveRepoActivity(repo, branch)
982 case 'commits':
983 return serveRepoCommits(req, repo, branch)
984 case 'commit':
985 return serveRepoCommit(repo, path[1])
986 case 'tree':
987 return serveRepoTree(repo, branch, filePath)
988 case 'blob':
989 return serveRepoBlob(repo, branch, filePath)
990 case 'raw':
991 return serveRepoRaw(repo, branch, filePath)
992 case 'digs':
993 return serveRepoDigs(repo)
994 case 'issues':
995 switch (path[1]) {
996 case 'new':
997 if (filePath.length == 0)
998 return serveRepoNewIssue(repo)
999 break
1000 default:
1001 return serveRepoIssues(req, repo, branch, filePath)
1002 }
1003 default:
1004 return serve404(req)
1005 }
1006 }
1007
1008 function serveRepoNotFound(id, err) {
1009 return serveTemplate('Repo not found', 404, pull.values([
1010 '<h2>Repo not found</h2>',
1011 '<p>Repo ' + id + ' was not found</p>',
1012 '<pre>' + escapeHTML(err.stack) + '</pre>',
1013 ]))
1014 }
1015
1016 function renderRepoPage(repo, page, branch, body) {
1017 var gitUrl = 'ssb://' + repo.id
1018 var gitLink = '<input class="clone-url" readonly="readonly" ' +
1019 'value="' + gitUrl + '" size="61" ' +
1020 'onclick="this.select()"/>'
1021 var digsPath = [repo.id, 'digs']
1022
1023 var done = multicb({ pluck: 1, spread: true })
1024 getRepoName(about, repo.feed, repo.id, done())
1025 about.getName(repo.feed, done())
1026 getVotes(repo.id, done())
1027
1028 return readNext(function (cb) {
1029 done(function (err, repoName, authorName, votes) {
1030 if (err) return cb(null, serveError(err))
1031 var upvoted = votes.upvoters[myId] > 0
1032 cb(null, serveTemplate(repo.id)(cat([
1033 pull.once(
1034 '<div class="repo-title">' +
1035 '<form class="right-bar" action="" method="post">' +
1036 '<button class="btn" ' +
1037 (isPublic ? 'disabled="disabled"' : ' type="submit"') + '>' +
1038 '<i>✌</i> ' + (!isPublic && upvoted ? 'Undig' : 'Dig') +
1039 '</button>' +
1040 (isPublic ? '' : '<input type="hidden" name="vote" value="' +
1041 (upvoted ? '0' : '1') + '">' +
1042 '<input type="hidden" name="action" value="vote">' +
1043 '<input type="hidden" name="id" value="' +
1044 escapeHTML(repo.id) + '">') + ' ' +
1045 '<strong>' + link(digsPath, votes.upvotes) + '</strong>' +
1046 '</form>' +
1047 renderNameForm(!isPublic, repo.id, repoName, 'repo-name', null,
1048 'Rename the repo',
1049 '<h2>' + link([repo.feed], authorName) + ' / ' +
1050 link([repo.id], repoName) + '</h2>') +
1051 '</div>' +
1052 nav([
1053 [[repo.id], 'Code', 'code'],
1054 [[repo.id, 'activity'], 'Activity', 'activity'],
1055 [[repo.id, 'commits', branch || ''], 'Commits', 'commits'],
1056 [[repo.id, 'issues'], 'Issues', 'issues']
1057 ], page, gitLink)),
1058 body
1059 ])))
1060 })
1061 })
1062 }
1063
1064 function serveEmptyRepo(repo) {
1065 if (repo.feed != myId)
1066 return renderRepoPage(repo, 'code', null, pull.once(
1067 '<section>' +
1068 '<h3>Empty repository</h3>' +
1069 '</section>'))
1070
1071 var gitUrl = 'ssb://' + repo.id
1072 return renderRepoPage(repo, 'code', null, pull.once(
1073 '<section>' +
1074 '<h3>Getting started</h3>' +
1075 '<h4>Create a new repository</h4><pre>' +
1076 'touch README.md\n' +
1077 'git init\n' +
1078 'git add README.md\n' +
1079 'git commit -m "Initial commit"\n' +
1080 'git remote add origin ' + gitUrl + '\n' +
1081 'git push -u origin master</pre>\n' +
1082 '<h4>Push an existing repository</h4>\n' +
1083 '<pre>git remote add origin ' + gitUrl + '\n' +
1084 'git push -u origin master</pre>' +
1085 '</section>'))
1086 }
1087
1088 function serveRepoTree(repo, rev, path) {
1089 if (!rev) return serveEmptyRepo(repo)
1090 var type = repo.isCommitHash(rev) ? 'Tree' : 'Branch'
1091 return renderRepoPage(repo, 'code', rev, cat([
1092 pull.once('<section><form action="" method="get">' +
1093 '<h3>' + type + ': ' + rev + ' '),
1094 revMenu(repo, rev),
1095 pull.once('</h3></form>'),
1096 type == 'Branch' && renderRepoLatest(repo, rev),
1097 pull.once('</section><section>'),
1098 renderRepoTree(repo, rev, path),
1099 pull.once('</section>'),
1100 renderRepoReadme(repo, rev, path)
1101 ]))
1102 }
1103
1104 /* Repo activity */
1105
1106 function serveRepoActivity(repo, branch) {
1107 return renderRepoPage(repo, 'activity', branch, cat([
1108 pull.once('<h3>Activity</h3>'),
1109 pull(
1110 ssb.links({
1111 type: 'git-update',
1112 dest: repo.id,
1113 source: repo.feed,
1114 rel: 'repo',
1115 values: true,
1116 reverse: true
1117 }),
1118 pull.map(renderRepoUpdate.bind(this, repo))
1119 )
1120 ]))
1121 }
1122
1123 function renderRepoUpdate(repo, msg, full) {
1124 var c = msg.value.content
1125
1126 var refs = c.refs ? Object.keys(c.refs).map(function (ref) {
1127 return {name: ref, value: c.refs[ref]}
1128 }) : []
1129 var numObjects = c.objects ? Object.keys(c.objects).length : 0
1130
1131 return '<section class="collapse">' +
1132 link([msg.key], new Date(msg.value.timestamp).toLocaleString()) +
1133 '<br>' +
1134 (numObjects ? 'Pushed ' + numObjects + ' objects<br>' : '') +
1135 refs.map(function (update) {
1136 var name = escapeHTML(update.name)
1137 if (!update.value) {
1138 return 'Deleted ' + name
1139 } else {
1140 var commitLink = link([repo.id, 'commit', update.value])
1141 return name + ' &rarr; ' + commitLink
1142 }
1143 }).join('<br>') +
1144 '</section>'
1145 }
1146
1147 /* Repo commits */
1148
1149 function serveRepoCommits(req, repo, branch) {
1150 var query = req._u.query
1151 return renderRepoPage(repo, 'commits', branch, cat([
1152 pull.once('<h3>Commits</h3>'),
1153 pull(
1154 repo.readLog(query.start || branch),
1155 pull.take(20),
1156 paginate(
1157 !query.start ? '' : function (first, cb) {
1158 cb(null, '&hellip;')
1159 },
1160 pull(
1161 paramap(repo.getCommitParsed.bind(repo), 8),
1162 pull.map(renderCommit.bind(this, repo))
1163 ),
1164 function (last, cb) {
1165 cb(null, '<a href="?start=' + last + '">Older</a>')
1166 }
1167 )
1168 )
1169 ]))
1170 }
1171
1172 function renderCommit(repo, commit) {
1173 var commitPath = [repo.id, 'commit', commit.id]
1174 var treePath = [repo.id, 'tree', commit.id]
1175 return '<section class="collapse">' +
1176 '<strong>' + link(commitPath, commit.title) + '</strong><br>' +
1177 '<tt>' + commit.id + '</tt> ' +
1178 link(treePath, 'Tree') + '<br>' +
1179 escapeHTML(commit.author.name) + ' &middot; ' + commit.author.date.toLocaleString() +
1180 (commit.separateAuthor ? '<br>' + escapeHTML(commit.committer.name) + ' committed on ' + commit.committer.date.toLocaleString() : "") +
1181 '</section>'
1182}
1183
1184 /* Repo tree */
1185
1186 function revMenu(repo, currentName) {
1187 return readOnce(function (cb) {
1188 repo.getRefNames(true, function (err, refs) {
1189 if (err) return cb(err)
1190 cb(null, '<select name="rev" onchange="this.form.submit()">' +
1191 Object.keys(refs).map(function (group) {
1192 return '<optgroup label="' + group + '">' +
1193 refs[group].map(function (name) {
1194 var htmlName = escapeHTML(name)
1195 return '<option value="' + htmlName + '"' +
1196 (name == currentName ? ' selected="selected"' : '') +
1197 '>' + htmlName + '</option>'
1198 }).join('') + '</optgroup>'
1199 }).join('') +
1200 '</select><noscript> <input type="submit" value="Go"/></noscript>')
1201 })
1202 })
1203 }
1204
1205 function renderRepoLatest(repo, rev) {
1206 return readOnce(function (cb) {
1207 repo.getCommitParsed(rev, function (err, commit) {
1208 if (err) return cb(err)
1209 var commitPath = [repo.id, 'commit', commit.id]
1210 cb(null,
1211 'Latest: <strong>' + link(commitPath, commit.title) +
1212 '</strong><br>' +
1213 '<tt>' + commit.id + '</tt><br> ' +
1214 escapeHTML(commit.committer.name) + ' committed on ' +
1215 commit.committer.date.toLocaleString() +
1216 (commit.separateAuthor ? '<br>' +
1217 escapeHTML(commit.author.name) + ' authored on ' +
1218 commit.author.date.toLocaleString() : ''))
1219 })
1220 })
1221 }
1222
1223 // breadcrumbs
1224 function linkPath(basePath, path) {
1225 path = path.slice()
1226 var last = path.pop()
1227 return path.map(function (dir, i) {
1228 return link(basePath.concat(path.slice(0, i+1)), dir)
1229 }).concat(last).join(' / ')
1230 }
1231
1232 function renderRepoTree(repo, rev, path) {
1233 var pathLinks = path.length === 0 ? '' :
1234 ': ' + linkPath([repo.id, 'tree'], [rev].concat(path))
1235 return cat([
1236 pull.once('<h3>Files' + pathLinks + '</h3>'),
1237 pull(
1238 repo.readDir(rev, path),
1239 pull.map(function (file) {
1240 var type = (file.mode === 040000) ? 'tree' :
1241 (file.mode === 0160000) ? 'commit' : 'blob'
1242 if (type == 'commit')
1243 return ['<span title="git commit link">🖈</span>', '<span title="' + escapeHTML(file.id) + '">' + escapeHTML(file.name) + '</span>']
1244 var filePath = [repo.id, type, rev].concat(path, file.name)
1245 return ['<i>' + (type == 'tree' ? '📁' : '📄') + '</i>',
1246 link(filePath, file.name)]
1247 }),
1248 table('class="files"')
1249 )
1250 ])
1251 }
1252
1253 /* Repo readme */
1254
1255 function renderRepoReadme(repo, branch, path) {
1256 return readNext(function (cb) {
1257 pull(
1258 repo.readDir(branch, path),
1259 pull.filter(function (file) {
1260 return /readme(\.|$)/i.test(file.name)
1261 }),
1262 pull.take(1),
1263 pull.collect(function (err, files) {
1264 if (err) return cb(null, pull.empty())
1265 var file = files[0]
1266 if (!file)
1267 return cb(null, pull.once(path.length ? '' : '<p>No readme</p>'))
1268 repo.getObjectFromAny(file.id, function (err, obj) {
1269 if (err) return cb(err)
1270 cb(null, cat([
1271 pull.once('<section><h4><a name="readme">' +
1272 escapeHTML(file.name) + '</a></h4><hr/>'),
1273 renderObjectData(obj, file.name, repo),
1274 pull.once('</section>')
1275 ]))
1276 })
1277 })
1278 )
1279 })
1280 }
1281
1282 /* Repo commit */
1283
1284 function serveRepoCommit(repo, rev) {
1285 return renderRepoPage(repo, null, rev, cat([
1286 readNext(function (cb) {
1287 repo.getCommitParsed(rev, function (err, commit) {
1288 if (err) return cb(err)
1289 var commitPath = [repo.id, 'commit', commit.id]
1290 var treePath = [repo.id, 'tree', commit.id]
1291 cb(null, cat([pull.once(
1292 '<h3>' + link(commitPath, 'Commit ' + rev) + '</h3>' +
1293 '<section class="collapse">' +
1294 '<div class="right-bar">' +
1295 link(treePath, 'Browse Files') +
1296 '</div>' +
1297 '<h4>' + escapeHTML(commit.title) + '</h4>' +
1298 (commit.body ? pre(commit.body) : '') +
1299 (commit.separateAuthor ? escapeHTML(commit.author.name) +
1300 ' authored on ' + commit.author.date.toLocaleString() + '<br>'
1301 : '') +
1302 escapeHTML(commit.committer.name) + ' committed on ' +
1303 commit.committer.date.toLocaleString() + '<br/>' +
1304 commit.parents.map(function (id) {
1305 return 'Parent: ' + link([repo.id, 'commit', id], id)
1306 }).join('<br>') +
1307 '</section>'),
1308 renderDiffStat(repo, commit.id, commit.parents)
1309 ]))
1310 })
1311 })
1312 ]))
1313 }
1314
1315 /* Diff stat */
1316
1317 function renderDiffStat(repo, id, parentIds) {
1318 if (parentIds.length == 0) parentIds = [null]
1319 var lastI = parentIds.length
1320 var oldTree = parentIds[0]
1321 var changedFiles = []
1322 return cat([
1323 pull.once('<section><h3>Files changed</h3>'),
1324 pull(
1325 repo.diffTrees(parentIds.concat(id), true),
1326 pull.map(function (item) {
1327 var filename = item.filename = escapeHTML(item.path.join('/'))
1328 var oldId = item.id && item.id[0]
1329 var newId = item.id && item.id[lastI]
1330 var oldMode = item.mode && item.mode[0]
1331 var newMode = item.mode && item.mode[lastI]
1332 var action =
1333 !oldId && newId ? 'added' :
1334 oldId && !newId ? 'deleted' :
1335 oldMode != newMode ?
1336 'changed mode from ' + oldMode.toString(8) +
1337 ' to ' + newMode.toString(8) :
1338 'changed'
1339 if (item.id)
1340 changedFiles.push(item)
1341 var fileHref = item.id ?
1342 '#' + encodeURIComponent(item.path.join('/')) :
1343 encodeLink([repo.id, 'blob', id].concat(item.path))
1344 return ['<a href="' + fileHref + '">' + filename + '</a>', action]
1345 }),
1346 table()
1347 ),
1348 pull(
1349 pull.values(changedFiles),
1350 paramap(function (item, cb) {
1351 var done = multicb({ pluck: 1, spread: true })
1352 getRepoObjectString(repo, item.id[0], done())
1353 getRepoObjectString(repo, item.id[lastI], done())
1354 done(function (err, strOld, strNew) {
1355 if (err) return cb(err)
1356 var commitId = item.id[lastI] ? id : parentIds.filter(Boolean)[0]
1357 cb(null, htmlLineDiff(item.filename, item.filename,
1358 strOld, strNew,
1359 encodeLink([repo.id, 'blob', commitId].concat(item.path))))
1360 })
1361 }, 4)
1362 ),
1363 pull.once('</section>' + hashHighlightScript),
1364 ])
1365 }
1366
1367 function htmlLineDiff(filename, anchor, oldStr, newStr, blobHref, rawHref) {
1368 var diff = JsDiff.structuredPatch('', '', oldStr, newStr)
1369 var groups = diff.hunks.map(function (hunk) {
1370 var oldLine = hunk.oldStart
1371 var newLine = hunk.newStart
1372 var header = '<tr class="diff-hunk-header"><td colspan=2></td><td>' +
1373 '@@ -' + oldLine + ',' + hunk.oldLines + ' ' +
1374 '+' + newLine + ',' + hunk.newLines + ' @@' +
1375 '</td></tr>'
1376 return [header].concat(hunk.lines.map(function (line) {
1377 var s = line[0]
1378 if (s == '\\') return
1379 var html = highlight(line, getExtension(filename))
1380 var trClass = s == '+' ? 'diff-new' : s == '-' ? 'diff-old' : ''
1381 var lineNums = [s == '+' ? '' : oldLine++, s == '-' ? '' : newLine++]
1382 var id = [filename].concat(lineNums).join('-')
1383 return '<tr id="' + escapeHTML(id) + '" class="' + trClass + '">' +
1384 lineNums.map(function (num) {
1385 return '<td class="code-linenum">' +
1386 (num ? '<a href="#' + encodeURIComponent(id) + '">' +
1387 num + '</a>' : '') + '</td>'
1388 }).join('') +
1389 '<td class="code-text">' + html + '</td></tr>'
1390 }))
1391 })
1392 return '<pre><table class="code">' +
1393 '<tr><th colspan=3><a name="' + anchor + '">' + filename + '</a>' +
1394 '<span class="right-bar">' +
1395 '<a href="' + blobHref + '">View</a> ' +
1396 '</span></th></tr>' +
1397 [].concat.apply([], groups).join('') +
1398 '</table></pre>'
1399 }
1400
1401 /* An unknown message linking to a repo */
1402
1403 function serveRepoSomething(req, repo, id, msg, path) {
1404 return renderRepoPage(repo, null, null,
1405 pull.once('<section><h3>' + link([id]) + '</h3>' +
1406 json(msg) + '</section>'))
1407 }
1408
1409 /* Repo update */
1410
1411 function objsArr(objs) {
1412 return Array.isArray(objs) ? objs :
1413 Object.keys(objs).map(function (sha1) {
1414 var obj = Object.create(objs[sha1])
1415 obj.sha1 = sha1
1416 return obj
1417 })
1418 }
1419
1420 function serveRepoUpdate(req, repo, id, msg, path) {
1421 var raw = req._u.query.raw != null
1422
1423 if (raw)
1424 return renderRepoPage(repo, 'activity', null, pull.once(
1425 '<a href="?" class="raw-link header-align">Info</a>' +
1426 '<h3>Update</h3>' +
1427 '<section class="collapse">' + json({key: id, value: msg}) + '</section>'))
1428
1429 // convert packs to old single-object style
1430 if (msg.content.indexes) {
1431 for (var i = 0; i < msg.content.indexes.length; i++) {
1432 msg.content.packs[i] = {
1433 pack: {link: msg.content.packs[i].link},
1434 idx: msg.content.indexes[i]
1435 }
1436 }
1437 }
1438
1439 return renderRepoPage(repo, 'activity', null, cat([
1440 pull.once(
1441 '<a href="?raw" class="raw-link header-align">Data</a>' +
1442 '<h3>Update</h3>' +
1443 renderRepoUpdate(repo, {key: id, value: msg}, true) +
1444 (msg.content.objects ? '<h3>Objects</h3>' +
1445 objsArr(msg.content.objects).map(renderObject).join('\n') : '') +
1446 (msg.content.packs ? '<h3>Packs</h3>' +
1447 msg.content.packs.map(renderPack).join('\n') : '')),
1448 cat(!msg.content.packs ? [] : [
1449 pull.once('<h3>Commits</h3>'),
1450 pull(
1451 pull.values(msg.content.packs),
1452 paramap(function (pack, cb) {
1453 var key = pack.pack.link
1454 ssb.blobs.want(key, function (err, got) {
1455 if (err) cb(err)
1456 else if (!got) cb(null, pull.once('Missing blob ' + key))
1457 else cb(null, ssb.blobs.get(key))
1458 })
1459 }, 8),
1460 pull.map(function (readPack, cb) {
1461 return gitPack.decode({}, repo, cb, readPack)
1462 }),
1463 pull.flatten(),
1464 paramap(function (obj, cb) {
1465 if (obj.type == 'commit')
1466 Repo.getCommitParsed(obj, cb)
1467 else
1468 pull(obj.read, pull.drain(null, cb))
1469 }, 8),
1470 pull.filter(),
1471 pull.map(function (commit) {
1472 return renderCommit(repo, commit)
1473 })
1474 )
1475 ])
1476 ]))
1477 }
1478
1479 function renderObject(obj) {
1480 return '<section class="collapse">' +
1481 obj.type + ' ' + link([obj.link], obj.sha1) + '<br>' +
1482 obj.length + ' bytes' +
1483 '</section>'
1484 }
1485
1486 function renderPack(info) {
1487 return '<section class="collapse">' +
1488 (info.pack ? 'Pack: ' + link([info.pack.link]) + '<br>' : '') +
1489 (info.idx ? 'Index: ' + link([info.idx.link]) : '') + '</section>'
1490 }
1491
1492 /* Blob */
1493
1494 function serveRepoBlob(repo, rev, path) {
1495 return readNext(function (cb) {
1496 repo.getFile(rev, path, function (err, object) {
1497 if (err) return cb(null, serveBlobNotFound(repo.id, err))
1498 var type = repo.isCommitHash(rev) ? 'Tree' : 'Branch'
1499 var pathLinks = path.length === 0 ? '' :
1500 ': ' + linkPath([repo.id, 'tree'], [rev].concat(path))
1501 var rawFilePath = [repo.id, 'raw', rev].concat(path)
1502 var filename = path[path.length-1]
1503 var extension = filename.split('.').pop()
1504 cb(null, renderRepoPage(repo, 'code', rev, cat([
1505 pull.once('<section><form action="" method="get">' +
1506 '<h3>' + type + ': ' + rev + ' '),
1507 revMenu(repo, rev),
1508 pull.once('</h3></form>'),
1509 type == 'Branch' && renderRepoLatest(repo, rev),
1510 pull.once('</section><section class="collapse">' +
1511 '<h3>Files' + pathLinks + '</h3>' +
1512 '<div>' + object.length + ' bytes' +
1513 '<span class="raw-link">' + link(rawFilePath, 'Raw') + '</span>' +
1514 '</div></section>' +
1515 '<section>'),
1516 extension in imgMimes
1517 ? pull.once('<img src="' + encodeLink(rawFilePath) +
1518 '" alt="' + escapeHTML(filename) + '" />')
1519 : renderObjectData(object, filename, repo),
1520 pull.once('</section>')
1521 ])))
1522 })
1523 })
1524 }
1525
1526 function serveBlobNotFound(repoId, err) {
1527 return serveTemplate('Blob not found', 404, pull.values([
1528 '<h2>Blob not found</h2>',
1529 '<p>Blob in repo ' + link([repoId]) + ' was not found</p>',
1530 '<pre>' + escapeHTML(err.stack) + '</pre>'
1531 ]))
1532 }
1533
1534 /* Raw blob */
1535
1536 function serveRepoRaw(repo, branch, path) {
1537 return readNext(function (cb) {
1538 repo.getFile(branch, path, function (err, object) {
1539 if (err) return cb(null, servePlainError(404, 'Blob not found'))
1540 var extension = path[path.length-1].split('.').pop()
1541 var contentType = imgMimes[extension]
1542 cb(null, pull(object.read, serveRaw(object.length, contentType)))
1543 })
1544 })
1545 }
1546
1547 function serveRaw(length, contentType) {
1548 var inBody
1549 var headers = {
1550 'Content-Type': contentType || 'text/plain; charset=utf-8',
1551 'Cache-Control': 'max-age=31536000'
1552 }
1553 if (length != null)
1554 headers['Content-Length'] = length
1555 return function (read) {
1556 return function (end, cb) {
1557 if (inBody) return read(end, cb)
1558 if (end) return cb(true)
1559 cb(null, [200, headers])
1560 inBody = true
1561 }
1562 }
1563 }
1564
1565 function serveBlob(req, key) {
1566 return readNext(function (cb) {
1567 ssb.blobs.want(key, function (err, got) {
1568 if (err) cb(null, serveError(err))
1569 else if (!got) cb(null, serve404(req))
1570 else cb(null, serveRaw()(ssb.blobs.get(key)))
1571 })
1572 })
1573 }
1574
1575 /* Digs */
1576
1577 function serveRepoDigs(repo) {
1578 return readNext(function (cb) {
1579 getVotes(repo.id, function (err, votes) {
1580 cb(null, renderRepoPage(repo, null, null, cat([
1581 pull.once('<section><h3>Digs</h3>' +
1582 '<div>Total: ' + votes.upvotes + '</div>'),
1583 pull(
1584 pull.values(Object.keys(votes.upvoters)),
1585 paramap(function (feedId, cb) {
1586 about.getName(feedId, function (err, name) {
1587 if (err) return cb(err)
1588 cb(null, link([feedId], name))
1589 })
1590 }, 8),
1591 ul()
1592 ),
1593 pull.once('</section>')
1594 ])))
1595 })
1596 })
1597 }
1598
1599 /* Issues */
1600
1601 function serveRepoIssues(req, repo, issueId, path) {
1602 var numIssues = 0
1603 var state = req._u.query.state || 'open'
1604 return renderRepoPage(repo, 'issues', null, cat([
1605 pull.once(
1606 (isPublic ? '' :
1607 '<div class="right-bar">' + link([repo.id, 'issues', 'new'],
1608 '<button class="btn">&plus; New Issue</button>', true) +
1609 '</div>') +
1610 '<h3>Issues</h3>' +
1611 nav([
1612 ['?state=open', 'Open', 'open'],
1613 ['?state=closed', 'Closed', 'closed'],
1614 ['?state=all', 'All', 'all']
1615 ], state)),
1616 pull(
1617 issues.createFeedStream({ project: repo.id }),
1618 pull.filter(function (issue) {
1619 return state == 'all' ? true : (state == 'closed') == !issue.open
1620 }),
1621 pull.map(function (issue) {
1622 numIssues++
1623 var state = (issue.open ? 'open' : 'closed')
1624 return '<section class="collapse">' +
1625 '<i class="issue-state issue-state-' + state + '"' +
1626 ' title="' + ucfirst(state) + '">◾</i> ' +
1627 '<a href="' + encodeLink(issue.id) + '">' +
1628 escapeHTML(issue.title) +
1629 '<span class="right-bar">' +
1630 new Date(issue.created_at).toLocaleString() +
1631 '</span>' +
1632 '</a>' +
1633 '</section>'
1634 })
1635 ),
1636 readOnce(function (cb) {
1637 cb(null, numIssues > 0 ? '' : '<p>No issues</p>')
1638 })
1639 ]))
1640 }
1641
1642 /* New Issue */
1643
1644 function serveRepoNewIssue(repo, issueId, path) {
1645 return renderRepoPage(repo, 'issues', null, pull.once(
1646 '<h3>New Issue</h3>' +
1647 '<section><form action="" method="post">' +
1648 '<input type="hidden" name="action" value="new-issue">' +
1649 '<p><input class="wide-input" name="title" placeholder="Issue Title" size="77" /></p>' +
1650 renderPostForm(repo, 'Description', 8) +
1651 '<button type="submit" class="btn">Create</button>' +
1652 '</form></section>'))
1653 }
1654
1655 /* Issue */
1656
1657 function serveRepoIssue(req, repo, issue, path, postId) {
1658 var isAuthor = (myId == issue.author) || (myId == repo.feed)
1659 var newestMsg = {key: issue.id, value: {timestamp: issue.created_at}}
1660 return renderRepoPage(repo, 'issues', null, cat([
1661 pull.once(
1662 renderNameForm(!isPublic, issue.id, issue.title, 'issue-title', null,
1663 'Rename the issue',
1664 '<h3>' + link([issue.id], issue.title) + '</h3>') +
1665 '<code>' + issue.id + '</code>' +
1666 '<section class="collapse">' +
1667 (issue.open
1668 ? '<strong class="issue-status open">Open</strong>'
1669 : '<strong class="issue-status closed">Closed</strong>')),
1670 readOnce(function (cb) {
1671 about.getName(issue.author, function (err, authorName) {
1672 if (err) return cb(err)
1673 var authorLink = link([issue.author], authorName)
1674 cb(null,
1675 authorLink + ' opened this issue on ' + timestamp(issue.created_at) +
1676 '<hr/>' +
1677 markdown(issue.text, repo) +
1678 '</section>')
1679 })
1680 }),
1681 // render posts and edits
1682 pull(
1683 ssb.links({
1684 dest: issue.id,
1685 values: true
1686 }),
1687 pull.unique('key'),
1688 addAuthorName(about),
1689 sortMsgs(),
1690 pull.map(function (msg) {
1691 var authorLink = link([msg.value.author], msg.authorName)
1692 var msgTimeLink = link([msg.key],
1693 new Date(msg.value.timestamp).toLocaleString(), false,
1694 'name="' + escapeHTML(msg.key) + '"')
1695 var c = msg.value.content
1696 if (msg.value.timestamp > newestMsg.value.timestamp)
1697 newestMsg = msg
1698 switch (c.type) {
1699 case 'post':
1700 if (c.root == issue.id) {
1701 var changed = issues.isStatusChanged(msg, issue)
1702 return '<section class="collapse">' +
1703 (msg.key == postId ? '<div class="highlight">' : '') +
1704 authorLink +
1705 (changed == null ? '' : ' ' + (
1706 changed ? 'reopened this issue' : 'closed this issue')) +
1707 ' &middot; ' + msgTimeLink +
1708 (msg.key == postId ? '</div>' : '') +
1709 markdown(c.text, repo) +
1710 '</section>'
1711 } else {
1712 var text = c.text || (c.type + ' ' + msg.key)
1713 return '<section class="collapse mention-preview">' +
1714 authorLink + ' mentioned this issue in ' +
1715 '<a href="/' + msg.key + '#' + msg.key + '">' +
1716 String(text).substr(0, 140) + '</a>' +
1717 '</section>'
1718 }
1719 case 'issue':
1720 return '<section class="collapse mention-preview">' +
1721 authorLink + ' mentioned this issue in ' +
1722 link([msg.key], String(c.title || msg.key).substr(0, 140)) +
1723 '</section>'
1724 case 'issue-edit':
1725 return '<section class="collapse">' +
1726 (c.title == null ? '' :
1727 authorLink + ' renamed this issue to <q>' +
1728 escapeHTML(c.title) + '</q>') +
1729 ' &middot; ' + msgTimeLink +
1730 '</section>'
1731 case 'git-update':
1732 var mention = issues.getMention(msg, issue)
1733 if (mention) {
1734 var commitLink = link([repo.id, 'commit', mention.object],
1735 mention.label || mention.object)
1736 return '<section class="collapse">' +
1737 authorLink + ' ' +
1738 (mention.open ? 'reopened this issue' :
1739 'closed this issue') +
1740 ' &middot; ' + msgTimeLink + '<br/>' +
1741 commitLink +
1742 '</section>'
1743 } else if ((mention = getMention(msg, issue.id))) {
1744 var commitLink = link(mention.object ?
1745 [repo.id, 'commit', mention.object] : [msg.key],
1746 mention.label || mention.object || msg.key)
1747 return '<section class="collapse">' +
1748 authorLink + ' mentioned this issue' +
1749 ' &middot; ' + msgTimeLink + '<br/>' +
1750 commitLink +
1751 '</section>'
1752 } else {
1753 // fallthrough
1754 }
1755
1756 default:
1757 return '<section class="collapse">' +
1758 authorLink +
1759 ' &middot; ' + msgTimeLink +
1760 json(c) +
1761 '</section>'
1762 }
1763 })
1764 ),
1765 isPublic ? pull.empty() : readOnce(renderCommentForm)
1766 ]))
1767
1768 function renderCommentForm(cb) {
1769 cb(null, '<section><form action="" method="post">' +
1770 '<input type="hidden" name="action" value="comment">' +
1771 '<input type="hidden" name="id" value="' + issue.id + '">' +
1772 '<input type="hidden" name="issue" value="' + issue.id + '">' +
1773 '<input type="hidden" name="repo" value="' + repo.id + '">' +
1774 '<input type="hidden" name="branch" value="' + newestMsg.key + '">' +
1775 renderPostForm(repo) +
1776 '<input type="submit" class="btn open" value="Comment" />' +
1777 (isAuthor ?
1778 '<input type="submit" class="btn"' +
1779 ' name="' + (issue.open ? 'close' : 'open') + '"' +
1780 ' value="' + (issue.open ? 'Close issue' : 'Reopen issue') + '"' +
1781 '/>' : '') +
1782 '</form></section>')
1783 }
1784 }
1785
1786}
1787

Built with git-ssb-web