git ssb

30+

cel / git-ssb-web



Tree: 7456f0fd0b2c4db62b3d7b8f4aa506c5c62badf2

Files: 7456f0fd0b2c4db62b3d7b8f4aa506c5c62badf2 / index.js

30112 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')
18
19marked.setOptions({
20 gfm: true,
21 mentions: true,
22 tables: true,
23 breaks: true,
24 pedantic: false,
25 sanitize: true,
26 smartLists: true,
27 smartypants: false
28})
29
30function parseAddr(str, def) {
31 if (!str) return def
32 var i = str.lastIndexOf(':')
33 if (~i) return {host: str.substr(0, i), port: str.substr(i+1)}
34 if (isNaN(str)) return {host: str, port: def.port}
35 return {host: def.host, port: str}
36}
37
38function link(parts, html) {
39 var href = '/' + parts.map(encodeURIComponent).join('/')
40 var innerHTML = html == null ? escapeHTML(parts[parts.length-1]) : html
41 return '<a href="' + escapeHTML(href) + '">' + innerHTML + '</a>'
42}
43
44function timestamp(time) {
45 time = Number(time)
46 var d = new Date(time)
47 return '<span title="' + time + '">' + d.toLocaleString() + '</span>'
48}
49
50function pre(text) {
51 return '<pre>' + escapeHTML(text) + '</pre>'
52}
53
54function json(obj) {
55 return pre(JSON.stringify(obj, null, 2))
56}
57
58function escapeHTML(str) {
59 return String(str)
60 .replace(/&/g, '&amp;')
61 .replace(/</g, '&lt;')
62 .replace(/>/g, '&gt;')
63 .replace(/"/g, '&quot;')
64}
65
66function escapeHTMLStream() {
67 return pull.map(function (buf) {
68 return escapeHTML(buf.toString('utf8'))
69 })
70}
71
72function table(props) {
73 return function (read) {
74 return cat([
75 pull.once('<table' + (props ? ' ' + props : '') + '>'),
76 pull(
77 read,
78 pull.map(function (row) {
79 return row ? '<tr>' + row.map(function (cell) {
80 return '<td>' + cell + '</td>'
81 }).join('') + '</tr>' : ''
82 })
83 ),
84 pull.once('</table>')
85 ])
86 }
87}
88
89function ul(props) {
90 return function (read) {
91 return cat([
92 pull.once('<ul' + (props ? ' ' + props : '') + '>'),
93 pull(
94 read,
95 pull.map(function (li) {
96 return '<li>' + li + '</li>'
97 })
98 ),
99 pull.once('</ul>')
100 ])
101 }
102}
103
104function readNext(fn) {
105 var next
106 return function (end, cb) {
107 if (next) return next(end, cb)
108 fn(function (err, _next) {
109 if (err) return cb(err)
110 next = _next
111 next(null, cb)
112 })
113 }
114}
115
116function readOnce(fn) {
117 var ended
118 return function (end, cb) {
119 fn(function (err, data) {
120 if (err || ended) return cb(err || ended)
121 ended = true
122 cb(null, data)
123 })
124 }
125}
126
127function tryDecodeURIComponent(str) {
128 if (!str || (str[0] == '%' && ref.isBlobId(str)))
129 return str
130 try {
131 str = decodeURIComponent(str)
132 } finally {
133 return str
134 }
135}
136
137function getRepoName(about, ownerId, repoId, cb) {
138 about.getName({
139 owner: ownerId,
140 target: repoId,
141 toString: function () {
142 // hack to fit two parameters into asyncmemo
143 return ownerId + '/' + repoId
144 }
145 }, cb)
146}
147
148var hasOwnProp = Object.prototype.hasOwnProperty
149
150function getContentType(filename) {
151 var ext = filename.split('.').pop()
152 return hasOwnProp.call(contentTypes, ext)
153 ? contentTypes[ext]
154 : 'text/plain; charset=utf-8'
155}
156
157var contentTypes = {
158 css: 'text/css'
159}
160
161var staticBase = path.join(__dirname, 'static')
162
163function readReqJSON(req, cb) {
164 pull(
165 toPull(req),
166 pull.collect(function (err, bufs) {
167 if (err) return cb(err)
168 var data
169 try {
170 data = qs.parse(Buffer.concat(bufs).toString('ascii'))
171 } catch(e) {
172 return cb(e)
173 }
174 cb(null, data)
175 })
176 )
177}
178
179var msgTypes = {
180 'git-repo': true,
181 'git-update': true
182}
183
184var refLabels = {
185 heads: 'Branches'
186}
187
188module.exports = function (opts, cb) {
189 var ssb, reconnect, myId, getRepo, getVotes, getMsg
190 var about = function (id, cb) { cb(null, {name: id}) }
191 var reqQueue = []
192 var isPublic = opts.public
193 var ssbAppname = opts.appname || 'ssb'
194
195 var addr = parseAddr(opts.listenAddr, {host: 'localhost', port: 7718})
196 http.createServer(onRequest).listen(addr.port, addr.host, onListening)
197
198 var server = {
199 setSSB: function (_ssb, _reconnect) {
200 _ssb.whoami(function (err, feed) {
201 if (err) throw err
202 ssb = _ssb
203 reconnect = _reconnect
204 myId = feed.id
205 about = ssbAbout(ssb, myId)
206 while (reqQueue.length)
207 onRequest.apply(this, reqQueue.shift())
208 getRepo = asyncMemo(function (id, cb) {
209 getMsg(id, function (err, msg) {
210 if (err) return cb(err)
211 ssbGit.getRepo(ssb, {key: id, value: msg}, {live: true}, cb)
212 })
213 })
214 getVotes = ssbVotes(ssb)
215 getMsg = asyncMemo(ssb.get)
216 })
217 }
218 }
219
220 function onListening() {
221 var host = ~addr.host.indexOf(':') ? '[' + addr.host + ']' : addr.host
222 console.log('Listening on http://' + host + ':' + addr.port + '/')
223 cb(null, server)
224 }
225
226 /* Serving a request */
227
228 function onRequest(req, res) {
229 console.log(req.method, req.url)
230 if (!ssb) return reqQueue.push(arguments)
231 pull(
232 handleRequest(req),
233 pull.filter(function (data) {
234 if (Array.isArray(data)) {
235 res.writeHead.apply(res, data)
236 return false
237 }
238 return true
239 }),
240 toPull(res)
241 )
242 }
243
244 function handleRequest(req) {
245 var u = req._u = url.parse(req.url)
246 var dirs = u.pathname.slice(1).split(/\/+/).map(tryDecodeURIComponent)
247 var dir = dirs[0]
248 if (dir == '')
249 return serveIndex(req)
250 else if (ref.isBlobId(dir))
251 return serveBlob(req, dir)
252 else if (ref.isMsgId(dir))
253 return serveMessage(req, dir, dirs.slice(1))
254 else if (ref.isFeedId(dir))
255 return serveUserPage(dir)
256 else
257 return serveFile(req, dirs)
258 }
259
260 function serveFile(req, dirs) {
261 var filename = path.join.apply(path, [staticBase].concat(dirs))
262 // prevent escaping base dir
263 if (filename.indexOf(staticBase) !== 0)
264 return servePlainError(403, '403 Forbidden')
265
266 return readNext(function (cb) {
267 fs.stat(filename, function (err, stats) {
268 cb(null, err ?
269 err.code == 'ENOENT' ? serve404(req)
270 : servePlainError(500, err.message)
271 : 'if-modified-since' in req.headers &&
272 new Date(req.headers['if-modified-since']) >= stats.mtime ?
273 pull.once([304])
274 : stats.isDirectory() ?
275 servePlainError(403, 'Directory not listable')
276 : cat([
277 pull.once([200, {
278 'Content-Type': getContentType(filename),
279 'Content-Length': stats.size,
280 'Last-Modified': stats.mtime.toGMTString()
281 }]),
282 toPull(fs.createReadStream(filename))
283 ]))
284 })
285 })
286 }
287
288 function servePlainError(code, msg) {
289 return pull.values([
290 [code, {
291 'Content-Length': Buffer.byteLength(msg),
292 'Content-Type': 'text/plain; charset=utf-8'
293 }],
294 msg
295 ])
296 }
297
298 function serve404(req) {
299 return servePlainError(404, '404 Not Found')
300 }
301
302 function serveRedirect(path) {
303 var msg = '<!doctype><html><head><meta charset=utf-8>' +
304 '<title>Redirect</title></head>' +
305 '<body><p><a href="' + path + '">Continue</a></p></body></html>'
306 return pull.values([
307 [302, {
308 'Content-Length': Buffer.byteLength(msg),
309 'Content-Type': 'text/html',
310 Location: path
311 }],
312 msg
313 ])
314 }
315
316 function renderTry(read) {
317 var ended
318 return function (end, cb) {
319 if (ended) return cb(ended)
320 read(end, function (err, data) {
321 if (err === true)
322 cb(true)
323 else if (err) {
324 ended = true
325 cb(null,
326 '<h3>' + err.name + '</h3>' +
327 '<pre>' + escapeHTML(err.stack) + '</pre>')
328 } else
329 cb(null, data)
330 })
331 }
332 }
333
334 function serveTemplate(title, code, read) {
335 if (read === undefined) return serveTemplate.bind(this, title, code)
336 return cat([
337 pull.values([
338 [code || 200, {
339 'Content-Type': 'text/html'
340 }],
341 '<!doctype html><html><head><meta charset=utf-8>',
342 '<title>' + escapeHTML(title || 'git ssb') + '</title>',
343 '<link rel=stylesheet href="/styles.css"/>',
344 '</head>\n',
345 '<body>',
346 '<header>',
347 '<h1><a href="/">git ssb' +
348 (ssbAppname != 'ssb' ? ' <sub>' + ssbAppname + '</sub>' : '') +
349 '</a></h1>',
350 '</header>',
351 '<article>']),
352 renderTry(read),
353 pull.once('<hr/></article></body></html>')
354 ])
355 }
356
357 function serveError(err, status) {
358 if (err.message == 'stream is closed')
359 reconnect()
360 return pull(
361 pull.once(
362 '<h2>' + err.name + '</h3>' +
363 '<pre>' + escapeHTML(err.stack) + '</pre>'),
364 serveTemplate(err.name, status || 500)
365 )
366 }
367
368 /* Feed */
369
370 function renderFeed(feedId) {
371 var opts = {
372 reverse: true,
373 id: feedId
374 }
375 return pull(
376 feedId ? ssb.createUserStream(opts) : ssb.createFeedStream(opts),
377 pull.filter(function (msg) {
378 return msg.value.content.type in msgTypes &&
379 msg.value.timestamp < Date.now()
380 }),
381 pull.take(20),
382 pull.asyncMap(function (msg, cb) {
383 about.getName(msg.value.author, function (err, name) {
384 if (err) return cb(err)
385 switch (msg.value.content.type) {
386 case 'git-repo': return renderRepoCreated(msg, name, cb)
387 case 'git-update': return renderUpdate(msg, name, cb)
388 }
389 })
390 })
391 )
392 }
393
394 function renderRepoCreated(msg, authorName, cb) {
395 var msgLink = link([msg.key],
396 new Date(msg.value.timestamp).toLocaleString())
397 var authorLink = link([msg.value.author], authorName)
398 var author = msg.value.author
399 getRepoName(about, author, msg.key, function (err, repoName) {
400 if (err) return cb(err)
401 var repoLink = link([msg.key], repoName)
402 cb(null, '<section class="collapse">' + msgLink + '<br>' +
403 authorLink + ' created repo ' + repoLink + '</section>')
404 })
405 }
406
407 function renderUpdate(msg, authorName, cb) {
408 var msgLink = link([msg.key],
409 new Date(msg.value.timestamp).toLocaleString())
410 var authorLink = link([msg.value.author], authorName)
411 var repoId = msg.value.content.repo
412 var author = msg.value.author
413 getRepoName(about, author, repoId, function (err, repoName) {
414 if (err) return cb(err)
415 var repoLink = link([msg.value.content.repo], repoName)
416 cb(null, '<section class="collapse">' + msgLink + '<br>' +
417 authorLink + ' pushed to ' + repoLink + '</section>')
418 })
419 }
420
421 /* Index */
422
423 function serveIndex() {
424 return serveTemplate('git ssb')(renderFeed())
425 }
426
427 function serveUserPage(feedId) {
428 return serveTemplate(feedId)(cat([
429 readOnce(function (cb) {
430 about.getName(feedId, function (err, name) {
431 cb(null, '<h2>' + link([feedId], name) +
432 '<code class="user-id">' + feedId + '</code></h2>')
433 })
434 }),
435 renderFeed(feedId),
436 ]))
437 }
438
439 /* Message */
440
441 function serveMessage(req, id, path) {
442 return readNext(function (cb) {
443 ssb.get(id, function (err, msg) {
444 if (err) return cb(null, serveError(err))
445 var c = msg.content || {}
446 switch (c.type) {
447 case 'git-repo':
448 return getRepo(id, function (err, repo) {
449 if (err) return cb(null, serveError(err))
450 cb(null, serveRepoPage(req, Repo(repo), path))
451 })
452 case 'git-update':
453 return getRepo(c.repo, function (err, repo) {
454 if (err) return cb(null, serveRepoNotFound(repo.id, err))
455 cb(null, serveRepoUpdate(req, Repo(repo), id, msg, path))
456 })
457 default:
458 if (ref.isMsgId(c.repo))
459 return getRepo(c.repo, function (err, repo) {
460 if (err) return cb(null, serveRepoNotFound(repo.id, err))
461 cb(null, serveRepoSomething(req, Repo(repo), id, msg, path))
462 })
463 else
464 return cb(null, serveGenericMessage(req, id, msg, path))
465 }
466 })
467 })
468 }
469
470 function serveGenericMessage(req, id, msg, path) {
471 return serveTemplate(id)(pull.once(
472 '<section><h2>' + link([id]) + '</h2>' +
473 json(msg) +
474 '</section>'))
475 }
476
477 /* Repo */
478
479 function serveRepoPage(req, repo, path) {
480 var defaultBranch = 'master'
481
482 if (req.method == 'POST') {
483 if (isPublic)
484 return servePlainError(405, 'POST not allowed on public site')
485 return readNext(function (cb) {
486 readReqJSON(req, function (err, data) {
487 if (err) return cb(null, serveError(err, 400))
488 if (!data) return cb(null, serveError(new Error('No data'), 400))
489 if (data.vote != null) {
490 var voteValue = +data.vote || 0
491 ssb.publish(schemas.vote(repo.id, voteValue), function (err) {
492 if (err) return cb(null, serveError(err))
493 cb(null, serveRedirect(req.url))
494 })
495 } else if ('repo-name' in data) {
496 var name = data['repo-name']
497 if (!name) return cb(null, serveRedirect(req.url))
498 var msg = schemas.name(repo.id, name)
499 ssb.publish(msg, function (err) {
500 if (err) return cb(null, serveError(err))
501 cb(null, serveRedirect(req.url))
502 })
503 } else {
504 cb(null, servePlainError(400, 'What are you trying to do?'))
505 }
506 })
507 })
508 }
509
510 var branch = path[1] || defaultBranch
511 var filePath = path.slice(2)
512 switch (path[0]) {
513 case undefined:
514 return serveRepoTree(repo, branch, [])
515 case 'activity':
516 return serveRepoActivity(repo, branch)
517 case 'commits':
518 return serveRepoCommits(repo, branch)
519 case 'commit':
520 return serveRepoCommit(repo, path[1])
521 case 'tree':
522 return serveRepoTree(repo, branch, filePath)
523 case 'blob':
524 return serveRepoBlob(repo, branch, filePath)
525 case 'raw':
526 return serveRepoRaw(repo, branch, filePath)
527 case 'digs':
528 return serveRepoDigs(repo)
529 default:
530 return serve404(req)
531 }
532 }
533
534 function serveRepoNotFound(id, err) {
535 return serveTemplate('Repo not found', 404, pull.values([
536 '<h2>Repo not found</h2>',
537 '<p>Repo ' + id + ' was not found</p>',
538 '<pre>' + escapeHTML(err.stack) + '</pre>',
539 ]))
540 }
541
542 function renderRepoPage(repo, branch, body) {
543 var gitUrl = 'ssb://' + repo.id
544 var gitLink = '<input class="clone-url" readonly="readonly" ' +
545 'value="' + gitUrl + '" size="' + (2 + gitUrl.length) + '" ' +
546 'onclick="this.select()"/>'
547 var digsPath = [repo.id, 'digs']
548
549 var done = multicb({ pluck: 1, spread: true })
550 getRepoName(about, repo.feed, repo.id, done())
551 about.getName(repo.feed, done())
552 getVotes(repo.id, done())
553
554 return readNext(function (cb) {
555 done(function (err, repoName, authorName, votes) {
556 if (err) return cb(null, serveError(err))
557 var upvoted = votes.upvoters[myId] > 0
558 cb(null, serveTemplate(repo.id)(cat([
559 pull.once(
560 '<div class="repo-title">' +
561 '<form class="upvotes" action="" method="post">' +
562 (isPublic
563 ? '<button disabled="disabled"><i>โœŒ</i> Dig</button> '
564 : '<input type="hidden" name="vote" value="' +
565 (upvoted ? '0' : '1') + '">' +
566 '<button type="submit"><i>โœŒ</i> ' +
567 (upvoted ? 'Undig' : 'Dig') +
568 '</button>') + ' ' +
569 '<strong>' + link(digsPath, votes.upvotes) + '</strong>' +
570 '</form>' +
571 '<form class="petname" action="" method="post">' +
572 (isPublic ? '' :
573 '<input name="repo-name" id="repo-name" value="' +
574 escapeHTML(repoName) + '" />' +
575 '<label class="repo-name-toggle" for="repo-name" ' +
576 'title="Rename the repo"><i>โœ</i></label>' +
577 '<input class="repo-name-btn" type="submit" value="Rename">') +
578 '<h2 class="left">' + link([repo.feed], authorName) + ' / ' +
579 link([repo.id], repoName) + '</h2>' +
580 '</form>' +
581 '<br clear="all" \>' +
582 '</div><div class="repo-nav">' + link([repo.id], 'Code') +
583 link([repo.id, 'activity'], 'Activity') +
584 link([repo.id, 'commits', branch || ''], 'Commits') +
585 gitLink +
586 '</div>'),
587 body
588 ])))
589 })
590 })
591 }
592
593 function serveRepoTree(repo, rev, path) {
594 var type = repo.isCommitHash(rev) ? 'Tree' : 'Branch'
595 return renderRepoPage(repo, rev, cat([
596 pull.once('<section><h3>' + type + ': ' + rev + ' '),
597 revMenu(repo, rev),
598 pull.once('</h3>'),
599 type == 'Branch' && renderRepoLatest(repo, rev),
600 pull.once('</section><section>'),
601 renderRepoTree(repo, rev, path),
602 pull.once('</section>'),
603 renderRepoReadme(repo, rev, path)
604 ]))
605 }
606
607 /* Repo activity */
608
609 function serveRepoActivity(repo, branch) {
610 return renderRepoPage(repo, branch, cat([
611 pull.once('<h3>Activity</h3>'),
612 pull(
613 ssb.links({
614 type: 'git-update',
615 dest: repo.id,
616 source: repo.feed,
617 rel: 'repo',
618 values: true,
619 reverse: true,
620 limit: 8
621 }),
622 pull.map(renderRepoUpdate.bind(this, repo))
623 )
624 ]))
625 }
626
627 function renderRepoUpdate(repo, msg, full) {
628 var c = msg.value.content
629
630 var refs = c.refs ? Object.keys(c.refs).map(function (ref) {
631 return {name: ref, value: c.refs[ref]}
632 }) : []
633 var numObjects = c.objects ? Object.keys(c.objects).length : 0
634
635 return '<section class="collapse">' +
636 link([msg.key], new Date(msg.value.timestamp).toLocaleString()) +
637 '<br>' +
638 (numObjects ? 'Pushed ' + numObjects + ' objects<br>' : '') +
639 refs.map(function (update) {
640 var name = escapeHTML(update.name)
641 if (!update.value) {
642 return 'Deleted ' + name
643 } else {
644 var commitLink = link([repo.id, 'commit', update.value])
645 return name + ' &rarr; ' + commitLink
646 }
647 }).join('<br>') +
648 '</section>'
649 }
650
651 /* Repo commits */
652
653 function serveRepoCommits(repo, branch) {
654 return renderRepoPage(repo, branch, cat([
655 pull.once('<h3>Commits</h3>'),
656 pull(
657 repo.readLog(branch),
658 pull.asyncMap(function (hash, cb) {
659 repo.getCommitParsed(hash, function (err, commit) {
660 if (err) return cb(err)
661 var commitPath = [repo.id, 'commit', commit.id]
662 var treePath = [repo.id, 'tree', commit.id]
663 cb(null, '<section class="collapse">' +
664 '<strong>' + link(commitPath, escapeHTML(commit.title)) + '</strong><br>' +
665 '<code>' + commit.id + '</code> ' +
666 link(treePath, 'Tree') + '<br>' +
667 (commit.separateAuthor ? escapeHTML(commit.author.name) + ' authored on ' + commit.author.date.toLocaleString() + '<br>' : '') +
668 escapeHTML(commit.committer.name) + ' committed on ' + commit.committer.date.toLocaleString() +
669 '</section>')
670 })
671 })
672 )
673 ]))
674 }
675
676 /* Repo tree */
677
678 function revMenu(repo, currentName) {
679 var baseHref = '/' + encodeURIComponent(repo.id) + '/tree/'
680 var onchange = 'location.href="' + baseHref + '" + this.value'
681 var currentGroup
682 return cat([
683 pull.once('<select onchange="' + escapeHTML(onchange) + '">'),
684 pull(
685 repo.refs(),
686 pull.map(function (ref) {
687 var m = ref.name.match(/^refs\/([^\/]*)\/(.*)$/) || [,, ref.name]
688 var group = m[1]
689 var name = m[2]
690
691 var optgroup = (group === currentGroup) ? '' :
692 (currentGroup ? '</optgroup>' : '') +
693 '<optgroup label="' + (refLabels[group] || group) + '">'
694 currentGroup = group
695 var selected = (name == currentName) ? ' selected="selected"' : ''
696 var htmlName = escapeHTML(name)
697 return optgroup +
698 '<option value="' + htmlName + '"' + selected + '>' +
699 htmlName + '</option>'
700 })
701 ),
702 readOnce(function (cb) {
703 cb(null, currentGroup ? '</optgroup>' : '')
704 }),
705 pull.once('</select>')
706 ])
707 }
708
709 function renderRepoLatest(repo, rev) {
710 return readOnce(function (cb) {
711 repo.getCommitParsed(rev, function (err, commit) {
712 if (err) return cb(err)
713 var commitPath = [repo.id, 'commit', commit.id]
714 cb(null,
715 'Latest: <strong>' + link(commitPath, escapeHTML(commit.title)) +
716 '</strong><br>' +
717 '<code>' + commit.id + '</code><br> ' +
718 escapeHTML(commit.committer.name) + ' committed on ' +
719 commit.committer.date.toLocaleString() +
720 (commit.separateAuthor ? '<br>' +
721 escapeHTML(commit.author.name) + ' authored on ' +
722 commit.author.date.toLocaleString() : ''))
723 })
724 })
725 }
726
727 // breadcrumbs
728 function linkPath(basePath, path) {
729 path = path.slice()
730 var last = path.pop()
731 return path.map(function (dir, i) {
732 return link(basePath.concat(path.slice(0, i+1)), dir)
733 }).concat(last).join(' / ')
734 }
735
736 function renderRepoTree(repo, rev, path) {
737 var pathLinks = path.length === 0 ? '' :
738 ': ' + linkPath([repo.id, 'tree'], [rev].concat(path))
739 return cat([
740 pull.once('<h3>Files' + pathLinks + '</h3>'),
741 pull(
742 repo.readDir(rev, path),
743 pull.map(function (file) {
744 var type = (file.mode === 040000) ? 'tree' :
745 (file.mode === 0160000) ? 'commit' : 'blob'
746 if (type == 'commit')
747 return ['<span title="git commit link">๐Ÿ–ˆ</span>', '<span title="' + escapeHTML(file.id) + '">' + escapeHTML(file.name) + '</span>']
748 var filePath = [repo.id, type, rev].concat(path, file.name)
749 return ['<i>' + (type == 'tree' ? '๐Ÿ“' : '๐Ÿ“„') + '</i>',
750 link(filePath, file.name)]
751 }),
752 table('class="files"')
753 )
754 ])
755 }
756
757 /* Repo readme */
758
759 function renderRepoReadme(repo, branch, path) {
760 return readNext(function (cb) {
761 pull(
762 repo.readDir(branch, path),
763 pull.filter(function (file) {
764 return /readme(\.|$)/i.test(file.name)
765 }),
766 pull.take(1),
767 pull.collect(function (err, files) {
768 if (err) return cb(null, pull.empty())
769 var file = files[0]
770 if (!file)
771 return cb(null, pull.once(path.length ? '' : '<p>No readme</p>'))
772 repo.getObjectFromAny(file.id, function (err, obj) {
773 if (err) return cb(err)
774 cb(null, cat([
775 pull.once('<section><h4>' + escapeHTML(file.name) + '</h4><hr/>'),
776 /\.md|\/.markdown/i.test(file.name) ?
777 readOnce(function (cb) {
778 pull(obj.read, pull.collect(function (err, bufs) {
779 if (err) return cb(err)
780 var buf = Buffer.concat(bufs, obj.length)
781 cb(null, marked(buf.toString()))
782 }))
783 })
784 : cat([
785 pull.once('<pre>'),
786 pull(obj.read, escapeHTMLStream()),
787 pull.once('</pre>')
788 ]),
789 pull.once('</section>')
790 ]))
791 })
792 })
793 )
794 })
795 }
796
797 /* Repo commit */
798
799 function serveRepoCommit(repo, rev) {
800 return renderRepoPage(repo, rev, cat([
801 pull.once('<h3>Commit ' + rev + '</h3>'),
802 readOnce(function (cb) {
803 repo.getCommitParsed(rev, function (err, commit) {
804 if (err) return cb(err)
805 var commitPath = [repo.id, 'commit', commit.id]
806 var treePath = [repo.id, 'tree', commit.tree]
807 cb(null,
808 '<p><strong>' + link(commitPath, escapeHTML(commit.title)) +
809 '</strong></p>' +
810 pre(commit.body) +
811 '<p>' +
812 (commit.separateAuthor ? escapeHTML(commit.author.name) +
813 ' authored on ' + commit.author.date.toLocaleString() + '<br>'
814 : '') +
815 escapeHTML(commit.committer.name) + ' committed on ' +
816 commit.committer.date.toLocaleString() + '</p>' +
817 '<p>' + commit.parents.map(function (id) {
818 return 'Parent: ' + link([repo.id, 'commit', id], id)
819 }).join('<br>') + '</p>' +
820 '<p>' +
821 (commit.tree ? 'Tree: ' + link(treePath) : 'No tree') +
822 '</p>')
823 })
824 })
825 ]))
826 }
827
828 /* An unknown message linking to a repo */
829
830 function serveRepoSomething(req, repo, id, msg, path) {
831 return renderRepoPage(repo, null,
832 pull.once('<section><h3>' + link([id]) + '</h3>' +
833 json(msg) + '</section>'))
834 }
835
836 /* Repo update */
837
838 function objsArr(objs) {
839 return Array.isArray(objs) ? objs :
840 Object.keys(objs).map(function (sha1) {
841 var obj = Object.create(objs[sha1])
842 obj.sha1 = sha1
843 return obj
844 })
845 }
846
847 function serveRepoUpdate(req, repo, id, msg, path) {
848 var raw = String(req._u.query).split('&').indexOf('raw') > -1
849
850 // convert packs to old single-object style
851 if (msg.content.indexes) {
852 for (var i = 0; i < msg.content.indexes.length; i++) {
853 msg.content.packs[i] = {
854 pack: {link: msg.content.packs[i].link},
855 idx: msg.content.indexes[i]
856 }
857 }
858 }
859
860 return renderRepoPage(repo, null, pull.once(
861 (raw ? '<a href="?" class="raw-link header-align">Info</a>' :
862 '<a href="?raw" class="raw-link header-align">Data</a>') +
863 '<h3>Update</h3>' +
864 (raw ? '<section class="collapse">' + json(msg) + '</section>' :
865 renderRepoUpdate(repo, {key: id, value: msg}, true) +
866 (msg.content.objects ? '<h3>Objects</h3>' +
867 objsArr(msg.content.objects).map(renderObject).join('\n') : '') +
868 (msg.content.packs ? '<h3>Packs</h3>' +
869 msg.content.packs.map(renderPack).join('\n') : ''))))
870 }
871
872 function renderObject(obj) {
873 return '<section class="collapse">' +
874 obj.type + ' ' + link([obj.link], escapeHTML(obj.sha1)) + '<br>' +
875 obj.length + ' bytes' +
876 '</section>'
877 }
878
879 function renderPack(info) {
880 return '<section class="collapse">' +
881 (info.pack ? 'Pack: ' + link([info.pack.link]) + '<br>' : '') +
882 (info.idx ? 'Index: ' + link([info.idx.link]) : '') + '</section>'
883 }
884
885 /* Blob */
886
887 function serveRepoBlob(repo, rev, path) {
888 return readNext(function (cb) {
889 repo.getFile(rev, path, function (err, object) {
890 if (err) return cb(null, serveBlobNotFound(repoId, err))
891 var type = repo.isCommitHash(rev) ? 'Tree' : 'Branch'
892 var pathLinks = path.length === 0 ? '' :
893 ': ' + linkPath([repo.id, 'tree'], [rev].concat(path))
894 var rawFilePath = [repo.id, 'raw', rev].concat(path)
895 cb(null, renderRepoPage(repo, rev, cat([
896 pull.once('<section><h3>' + type + ': ' + rev + ' '),
897 revMenu(repo, rev),
898 pull.once('</h3>'),
899 type == 'Branch' && renderRepoLatest(repo, rev),
900 pull.once('</section><section class="collapse">' +
901 '<h3>Files' + pathLinks + '</h3>' +
902 '<div>' + object.length + ' bytes' +
903 '<span class="raw-link">' + link(rawFilePath, 'Raw') + '</span>' +
904 '</div></section>' +
905 '<section><pre>'),
906 pull(object.read, escapeHTMLStream()),
907 pull.once('</pre></section>')
908 ])))
909 })
910 })
911 }
912
913 function serveBlobNotFound(repoId, err) {
914 return serveTemplate(400, 'Blob not found', pull.values([
915 '<h2>Blob not found</h2>',
916 '<p>Blob in repo ' + link([repoId]) + ' was not found</p>',
917 '<pre>' + escapeHTML(err.stack) + '</pre>'
918 ]))
919 }
920
921 /* Raw blob */
922
923 function serveRepoRaw(repo, branch, path) {
924 return readNext(function (cb) {
925 repo.getFile(branch, path, function (err, object) {
926 if (err) return cb(null, servePlainError(404, 'Blob not found'))
927 cb(null, serveObjectRaw(object))
928 })
929 })
930 }
931
932 function serveRaw(length) {
933 var inBody
934 var headers = {
935 'Content-Type': 'text/plain; charset=utf-8',
936 'Cache-Control': 'max-age=31536000'
937 }
938 if (length != null)
939 headers['Content-Length'] = length
940 return function (read) {
941 return function (end, cb) {
942 if (inBody) return read(end, cb)
943 if (end) return cb(true)
944 cb(null, [200, headers])
945 inBody = true
946 }
947 }
948 }
949
950 function serveObjectRaw(object) {
951 return pull(object.read, serveRaw(object.length))
952 }
953
954 function serveBlob(req, key) {
955 return readNext(function (cb) {
956 ssb.blobs.want(key, function (err, got) {
957 if (err) cb(null, serveError(err))
958 else if (!got) cb(null, serve404(req))
959 else cb(null, serveRaw()(ssb.blobs.get(key)))
960 })
961 })
962 }
963
964 /* Digs */
965
966 function serveRepoDigs(repo) {
967 return readNext(function (cb) {
968 getVotes(repo.id, function (err, votes) {
969 cb(null, renderRepoPage(repo, '', cat([
970 pull.once('<section><h3>Digs</h3>' +
971 '<div>Total: ' + votes.upvotes + '</div>'),
972 pull(
973 pull.values(Object.keys(votes.upvoters)),
974 pull.asyncMap(function (feedId, cb) {
975 about.getName(feedId, function (err, name) {
976 if (err) return cb(err)
977 cb(null, link([feedId], name))
978 })
979 }),
980 ul()
981 ),
982 pull.once('</section>')
983 ])))
984 })
985 })
986 }
987}
988

Built with git-ssb-web