git ssb

30+

cel / git-ssb-web



Tree: c64198c96150f70ccd8f8b3afeed330813769eaa

Files: c64198c96150f70ccd8f8b3afeed330813769eaa / index.js

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

Built with git-ssb-web