git ssb

30+

cel / git-ssb-web



Tree: 171bb8a2c96096c302ba35950277404010f2ddf5

Files: 171bb8a2c96096c302ba35950277404010f2ddf5 / index.js

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

Built with git-ssb-web