git ssb

30+

cel / git-ssb-web



Tree: 7c57d165e125d8054b97df8d5339b45d3e62d8c3

Files: 7c57d165e125d8054b97df8d5339b45d3e62d8c3 / index.js

29956 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 return readNext(function (cb) {
484 readReqJSON(req, function (err, data) {
485 if (err) return cb(null, serveError(err, 400))
486 if (!data) return cb(null, serveError(new Error('No data'), 400))
487 if (data.vote != null) {
488 var voteValue = +data.vote || 0
489 ssb.publish(schemas.vote(repo.id, voteValue), function (err) {
490 if (err) return cb(null, serveError(err))
491 cb(null, serveRedirect(req.url))
492 })
493 } else if ('repo-name' in data) {
494 var name = data['repo-name']
495 if (!name) return cb(null, serveRedirect(req.url))
496 var msg = schemas.name(repo.id, name)
497 ssb.publish(msg, function (err) {
498 if (err) return cb(null, serveError(err))
499 cb(null, serveRedirect(req.url))
500 })
501 } else {
502 cb(null, servePlainError(400, 'What are you trying to do?'))
503 }
504 })
505 })
506 }
507
508 var branch = path[1] || defaultBranch
509 var filePath = path.slice(2)
510 switch (path[0]) {
511 case undefined:
512 return serveRepoTree(repo, branch, [])
513 case 'activity':
514 return serveRepoActivity(repo, branch)
515 case 'commits':
516 return serveRepoCommits(repo, branch)
517 case 'commit':
518 return serveRepoCommit(repo, path[1])
519 case 'tree':
520 return serveRepoTree(repo, branch, filePath)
521 case 'blob':
522 return serveRepoBlob(repo, branch, filePath)
523 case 'raw':
524 return serveRepoRaw(repo, branch, filePath)
525 case 'digs':
526 return serveRepoDigs(repo)
527 default:
528 return serve404(req)
529 }
530 }
531
532 function serveRepoNotFound(id, err) {
533 return serveTemplate('Repo not found', 404, pull.values([
534 '<h2>Repo not found</h2>',
535 '<p>Repo ' + id + ' was not found</p>',
536 '<pre>' + escapeHTML(err.stack) + '</pre>',
537 ]))
538 }
539
540 function renderRepoPage(repo, branch, body) {
541 var gitUrl = 'ssb://' + repo.id
542 var gitLink = '<input class="clone-url" readonly="readonly" ' +
543 'value="' + gitUrl + '" size="' + (2 + gitUrl.length) + '" ' +
544 'onclick="this.select()"/>'
545 var digsPath = [repo.id, 'digs']
546
547 var done = multicb({ pluck: 1, spread: true })
548 about.getName(repo.id, done())
549 about.getName(repo.feed, done())
550 getVotes(repo.id, done())
551
552 return readNext(function (cb) {
553 done(function (err, repoName, authorName, votes) {
554 if (err) return cb(null, serveError(err))
555 var upvoted = votes.upvoters[myId] > 0
556 cb(null, serveTemplate(repo.id)(cat([
557 pull.once(
558 '<div class="repo-title">' +
559 '<form class="upvotes" action="" method="post">' +
560 (isPublic
561 ? '<button disabled="disabled">โœŒ Dig</button> '
562 : '<input type="hidden" name="vote" value="' +
563 (upvoted ? '0' : '1') + '">' +
564 '<button type="submit"><i>โœŒ</i> ' +
565 (upvoted ? 'Undig' : 'Dig') +
566 '</button>') + ' ' +
567 '<strong>' + link(digsPath, votes.upvotes) + '</strong>' +
568 '</form>' +
569 '<form class="petname" action="" method="post">' +
570 '<input name="repo-name" id="repo-name" value="' +
571 escapeHTML(repoName) + '" />' +
572 '<label class="repo-name-toggle" for="repo-name" ' +
573 'title="Rename the repo"><i>โœ</i></label>' +
574 '<input class="repo-name-btn" type="submit" value="Rename">' +
575 '<h2 class="left">' + link([repo.feed], authorName) + ' / ' +
576 link([repo.id], repoName) + '</h2>' +
577 '</form>' +
578 '<br clear="all" \>' +
579 '</div><div class="repo-nav">' + link([repo.id], 'Code') +
580 link([repo.id, 'activity'], 'Activity') +
581 link([repo.id, 'commits', branch || ''], 'Commits') +
582 gitLink +
583 '</div>'),
584 body
585 ])))
586 })
587 })
588 }
589
590 function serveRepoTree(repo, rev, path) {
591 var type = repo.isCommitHash(rev) ? 'Tree' : 'Branch'
592 return renderRepoPage(repo, rev, cat([
593 pull.once('<section><h3>' + type + ': ' + rev + ' '),
594 revMenu(repo, rev),
595 pull.once('</h3>'),
596 type == 'Branch' && renderRepoLatest(repo, rev),
597 pull.once('</section><section>'),
598 renderRepoTree(repo, rev, path),
599 pull.once('</section>'),
600 renderRepoReadme(repo, rev, path)
601 ]))
602 }
603
604 /* Repo activity */
605
606 function serveRepoActivity(repo, branch) {
607 return renderRepoPage(repo, branch, cat([
608 pull.once('<h3>Activity</h3>'),
609 pull(
610 ssb.links({
611 type: 'git-update',
612 dest: repo.id,
613 source: repo.feed,
614 rel: 'repo',
615 values: true,
616 reverse: true,
617 limit: 8
618 }),
619 pull.map(renderRepoUpdate.bind(this, repo))
620 )
621 ]))
622 }
623
624 function renderRepoUpdate(repo, msg, full) {
625 var c = msg.value.content
626
627 var refs = c.refs ? Object.keys(c.refs).map(function (ref) {
628 return {name: ref, value: c.refs[ref]}
629 }) : []
630 var numObjects = c.objects ? Object.keys(c.objects).length : 0
631
632 return '<section class="collapse">' +
633 link([msg.key], new Date(msg.value.timestamp).toLocaleString()) +
634 '<br>' +
635 (numObjects ? 'Pushed ' + numObjects + ' objects<br>' : '') +
636 refs.map(function (update) {
637 var name = escapeHTML(update.name)
638 if (!update.value) {
639 return 'Deleted ' + name
640 } else {
641 var commitLink = link([repo.id, 'commit', update.value])
642 return name + ' &rarr; ' + commitLink
643 }
644 }).join('<br>') +
645 '</section>'
646 }
647
648 /* Repo commits */
649
650 function serveRepoCommits(repo, branch) {
651 return renderRepoPage(repo, branch, cat([
652 pull.once('<h3>Commits</h3>'),
653 pull(
654 repo.readLog(branch),
655 pull.asyncMap(function (hash, cb) {
656 repo.getCommitParsed(hash, function (err, commit) {
657 if (err) return cb(err)
658 var commitPath = [repo.id, 'commit', commit.id]
659 var treePath = [repo.id, 'tree', commit.id]
660 cb(null, '<section class="collapse">' +
661 '<strong>' + link(commitPath, escapeHTML(commit.title)) + '</strong><br>' +
662 '<code>' + commit.id + '</code> ' +
663 link(treePath, 'Tree') + '<br>' +
664 (commit.separateAuthor ? escapeHTML(commit.author.name) + ' authored on ' + commit.author.date.toLocaleString() + '<br>' : '') +
665 escapeHTML(commit.committer.name) + ' committed on ' + commit.committer.date.toLocaleString() +
666 '</section>')
667 })
668 })
669 )
670 ]))
671 }
672
673 /* Repo tree */
674
675 function revMenu(repo, currentName) {
676 var baseHref = '/' + encodeURIComponent(repo.id) + '/tree/'
677 var onchange = 'location.href="' + baseHref + '" + this.value'
678 var currentGroup
679 return cat([
680 pull.once('<select onchange="' + escapeHTML(onchange) + '">'),
681 pull(
682 repo.refs(),
683 pull.map(function (ref) {
684 var m = ref.name.match(/^refs\/([^\/]*)\/(.*)$/) || [,, ref.name]
685 var group = m[1]
686 var name = m[2]
687
688 var optgroup = (group === currentGroup) ? '' :
689 (currentGroup ? '</optgroup>' : '') +
690 '<optgroup label="' + (refLabels[group] || group) + '">'
691 currentGroup = group
692 var selected = (name == currentName) ? ' selected="selected"' : ''
693 var htmlName = escapeHTML(name)
694 return optgroup +
695 '<option value="' + htmlName + '"' + selected + '>' +
696 htmlName + '</option>'
697 })
698 ),
699 readOnce(function (cb) {
700 cb(null, currentGroup ? '</optgroup>' : '')
701 }),
702 pull.once('</select>')
703 ])
704 }
705
706 function renderRepoLatest(repo, rev) {
707 return readOnce(function (cb) {
708 repo.getCommitParsed(rev, function (err, commit) {
709 if (err) return cb(err)
710 var commitPath = [repo.id, 'commit', commit.id]
711 cb(null,
712 'Latest: <strong>' + link(commitPath, escapeHTML(commit.title)) +
713 '</strong><br>' +
714 '<code>' + commit.id + '</code><br> ' +
715 escapeHTML(commit.committer.name) + ' committed on ' +
716 commit.committer.date.toLocaleString() +
717 (commit.separateAuthor ? '<br>' +
718 escapeHTML(commit.author.name) + ' authored on ' +
719 commit.author.date.toLocaleString() : ''))
720 })
721 })
722 }
723
724 // breadcrumbs
725 function linkPath(basePath, path) {
726 path = path.slice()
727 var last = path.pop()
728 return path.map(function (dir, i) {
729 return link(basePath.concat(path.slice(0, i+1)), dir)
730 }).concat(last).join(' / ')
731 }
732
733 function renderRepoTree(repo, rev, path) {
734 var pathLinks = path.length === 0 ? '' :
735 ': ' + linkPath([repo.id, 'tree'], [rev].concat(path))
736 return cat([
737 pull.once('<h3>Files' + pathLinks + '</h3>'),
738 pull(
739 repo.readDir(rev, path),
740 pull.map(function (file) {
741 var type = (file.mode === 040000) ? 'tree' :
742 (file.mode === 0160000) ? 'commit' : 'blob'
743 if (type == 'commit')
744 return ['<span title="git commit link">๐Ÿ–ˆ</span>', '<span title="' + escapeHTML(file.id) + '">' + escapeHTML(file.name) + '</span>']
745 var filePath = [repo.id, type, rev].concat(path, file.name)
746 return ['<i>' + (type == 'tree' ? '๐Ÿ“' : '๐Ÿ“„') + '</i>',
747 link(filePath, file.name)]
748 }),
749 table('class="files"')
750 )
751 ])
752 }
753
754 /* Repo readme */
755
756 function renderRepoReadme(repo, branch, path) {
757 return readNext(function (cb) {
758 pull(
759 repo.readDir(branch, path),
760 pull.filter(function (file) {
761 return /readme(\.|$)/i.test(file.name)
762 }),
763 pull.take(1),
764 pull.collect(function (err, files) {
765 if (err) return cb(null, pull.empty())
766 var file = files[0]
767 if (!file)
768 return cb(null, pull.once(path.length ? '' : '<p>No readme</p>'))
769 repo.getObjectFromAny(file.id, function (err, obj) {
770 if (err) return cb(err)
771 cb(null, cat([
772 pull.once('<section><h4>' + escapeHTML(file.name) + '</h4><hr/>'),
773 /\.md|\/.markdown/i.test(file.name) ?
774 readOnce(function (cb) {
775 pull(obj.read, pull.collect(function (err, bufs) {
776 if (err) return cb(err)
777 var buf = Buffer.concat(bufs, obj.length)
778 cb(null, marked(buf.toString()))
779 }))
780 })
781 : cat([
782 pull.once('<pre>'),
783 pull(obj.read, escapeHTMLStream()),
784 pull.once('</pre>')
785 ]),
786 pull.once('</section>')
787 ]))
788 })
789 })
790 )
791 })
792 }
793
794 /* Repo commit */
795
796 function serveRepoCommit(repo, rev) {
797 return renderRepoPage(repo, rev, cat([
798 pull.once('<h3>Commit ' + rev + '</h3>'),
799 readOnce(function (cb) {
800 repo.getCommitParsed(rev, function (err, commit) {
801 if (err) return cb(err)
802 var commitPath = [repo.id, 'commit', commit.id]
803 var treePath = [repo.id, 'tree', commit.tree]
804 cb(null,
805 '<p><strong>' + link(commitPath, escapeHTML(commit.title)) +
806 '</strong></p>' +
807 pre(commit.body) +
808 '<p>' +
809 (commit.separateAuthor ? escapeHTML(commit.author.name) +
810 ' authored on ' + commit.author.date.toLocaleString() + '<br>'
811 : '') +
812 escapeHTML(commit.committer.name) + ' committed on ' +
813 commit.committer.date.toLocaleString() + '</p>' +
814 '<p>' + commit.parents.map(function (id) {
815 return 'Parent: ' + link([repo.id, 'commit', id], id)
816 }).join('<br>') + '</p>' +
817 '<p>' +
818 (commit.tree ? 'Tree: ' + link(treePath) : 'No tree') +
819 '</p>')
820 })
821 })
822 ]))
823 }
824
825 /* An unknown message linking to a repo */
826
827 function serveRepoSomething(req, repo, id, msg, path) {
828 return renderRepoPage(repo, null,
829 pull.once('<section><h3>' + link([id]) + '</h3>' +
830 json(msg) + '</section>'))
831 }
832
833 /* Repo update */
834
835 function objsArr(objs) {
836 return Array.isArray(objs) ? objs :
837 Object.keys(objs).map(function (sha1) {
838 var obj = Object.create(objs[sha1])
839 obj.sha1 = sha1
840 return obj
841 })
842 }
843
844 function serveRepoUpdate(req, repo, id, msg, path) {
845 var raw = String(req._u.query).split('&').indexOf('raw') > -1
846
847 // convert packs to old single-object style
848 if (msg.content.indexes) {
849 for (var i = 0; i < msg.content.indexes.length; i++) {
850 msg.content.packs[i] = {
851 pack: {link: msg.content.packs[i].link},
852 idx: msg.content.indexes[i]
853 }
854 }
855 }
856
857 return renderRepoPage(repo, null, pull.once(
858 (raw ? '<a href="?" class="raw-link header-align">Info</a>' :
859 '<a href="?raw" class="raw-link header-align">Data</a>') +
860 '<h3>Update</h3>' +
861 (raw ? '<section class="collapse">' + json(msg) + '</section>' :
862 renderRepoUpdate(repo, {key: id, value: msg}, true) +
863 (msg.content.objects ? '<h3>Objects</h3>' +
864 objsArr(msg.content.objects).map(renderObject).join('\n') : '') +
865 (msg.content.packs ? '<h3>Packs</h3>' +
866 msg.content.packs.map(renderPack).join('\n') : ''))))
867 }
868
869 function renderObject(obj) {
870 return '<section class="collapse">' +
871 obj.type + ' ' + link([obj.link], escapeHTML(obj.sha1)) + '<br>' +
872 obj.length + ' bytes' +
873 '</section>'
874 }
875
876 function renderPack(info) {
877 return '<section class="collapse">' +
878 (info.pack ? 'Pack: ' + link([info.pack.link]) + '<br>' : '') +
879 (info.idx ? 'Index: ' + link([info.idx.link]) : '') + '</section>'
880 }
881
882 /* Blob */
883
884 function serveRepoBlob(repo, rev, path) {
885 return readNext(function (cb) {
886 repo.getFile(rev, path, function (err, object) {
887 if (err) return cb(null, serveBlobNotFound(repoId, err))
888 var type = repo.isCommitHash(rev) ? 'Tree' : 'Branch'
889 var pathLinks = path.length === 0 ? '' :
890 ': ' + linkPath([repo.id, 'tree'], [rev].concat(path))
891 var rawFilePath = [repo.id, 'raw', rev].concat(path)
892 cb(null, renderRepoPage(repo, rev, cat([
893 pull.once('<section><h3>' + type + ': ' + rev + ' '),
894 revMenu(repo, rev),
895 pull.once('</h3>'),
896 type == 'Branch' && renderRepoLatest(repo, rev),
897 pull.once('</section><section class="collapse">' +
898 '<h3>Files' + pathLinks + '</h3>' +
899 '<div>' + object.length + ' bytes' +
900 '<span class="raw-link">' + link(rawFilePath, 'Raw') + '</span>' +
901 '</div></section>' +
902 '<section><pre>'),
903 pull(object.read, escapeHTMLStream()),
904 pull.once('</pre></section>')
905 ])))
906 })
907 })
908 }
909
910 function serveBlobNotFound(repoId, err) {
911 return serveTemplate(400, 'Blob not found', pull.values([
912 '<h2>Blob not found</h2>',
913 '<p>Blob in repo ' + link([repoId]) + ' was not found</p>',
914 '<pre>' + escapeHTML(err.stack) + '</pre>'
915 ]))
916 }
917
918 /* Raw blob */
919
920 function serveRepoRaw(repo, branch, path) {
921 return readNext(function (cb) {
922 repo.getFile(branch, path, function (err, object) {
923 if (err) return cb(null, servePlainError(404, 'Blob not found'))
924 cb(null, serveObjectRaw(object))
925 })
926 })
927 }
928
929 function serveRaw(length) {
930 var inBody
931 var headers = {
932 'Content-Type': 'text/plain; charset=utf-8',
933 'Cache-Control': 'max-age=31536000'
934 }
935 if (length != null)
936 headers['Content-Length'] = length
937 return function (read) {
938 return function (end, cb) {
939 if (inBody) return read(end, cb)
940 if (end) return cb(true)
941 cb(null, [200, headers])
942 inBody = true
943 }
944 }
945 }
946
947 function serveObjectRaw(object) {
948 return pull(object.read, serveRaw(object.length))
949 }
950
951 function serveBlob(req, key) {
952 return readNext(function (cb) {
953 ssb.blobs.want(key, function (err, got) {
954 if (err) cb(null, serveError(err))
955 else if (!got) cb(null, serve404(req))
956 else cb(null, serveRaw()(ssb.blobs.get(key)))
957 })
958 })
959 }
960
961 /* Digs */
962
963 function serveRepoDigs(repo) {
964 return readNext(function (cb) {
965 getVotes(repo.id, function (err, votes) {
966 cb(null, renderRepoPage(repo, '', cat([
967 pull.once('<section><h3>Digs</h3>' +
968 '<div>Total: ' + votes.upvotes + '</div>'),
969 pull(
970 pull.values(Object.keys(votes.upvoters)),
971 pull.asyncMap(function (feedId, cb) {
972 about.getName(feedId, function (err, name) {
973 if (err) return cb(err)
974 cb(null, link([feedId], name))
975 })
976 }),
977 ul()
978 ),
979 pull.once('</section>')
980 ])))
981 })
982 })
983 }
984}
985

Built with git-ssb-web