git ssb

30+

cel / git-ssb-web



Tree: 0280bd8f7e0ed8a70224173c93b930022fb02225

Files: 0280bd8f7e0ed8a70224173c93b930022fb02225 / index.js

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

Built with git-ssb-web