git ssb

30+

cel / git-ssb-web



Tree: 2515885506eef29d56ab8b1c94a9ebc9e15483aa

Files: 2515885506eef29d56ab8b1c94a9ebc9e15483aa / index.js

17411 bytesRaw
1var http = require('http')
2var url = require('url')
3var ref = require('ssb-ref')
4var pull = require('pull-stream')
5var ssbGit = require('ssb-git-repo')
6var toPull = require('stream-to-pull-stream')
7var cat = require('pull-cat')
8var Repo = require('pull-git-repo')
9var ssbAbout = require('./about')
10
11function parseAddr(str, def) {
12 if (!str) return def
13 var i = str.lastIndexOf(':')
14 if (~i) return {host: str.substr(0, i), port: str.substr(i+1)}
15 if (isNaN(str)) return {host: str, port: def.port}
16 return {host: def.host, port: str}
17}
18
19function link(parts, html) {
20 var href = '/' + parts.map(encodeURIComponent).join('/')
21 var innerHTML = html || escapeHTML(parts[parts.length-1])
22 return '<a href="' + href + '">' + innerHTML + '</a>'
23}
24
25function timestamp(time) {
26 time = Number(time)
27 var d = new Date(time)
28 return '<span title="' + time + '">' + d.toLocaleString() + '</span>'
29}
30
31function pre(text) {
32 return '<pre>' + escapeHTML(text) + '</pre>'
33}
34
35function json(obj) {
36 return pre(JSON.stringify(obj, null, 2))
37}
38
39function escapeHTML(str) {
40 return String(str)
41 .replace(/&/g, '&amp;')
42 .replace(/</g, '&lt;')
43 .replace(/>/g, '&gt;')
44 .replace(/"/g, '&quot;')
45}
46
47function escapeHTMLStream() {
48 return pull.map(function (buf) {
49 return escapeHTML(buf.toString('utf8'))
50 })
51}
52
53function readNext(fn) {
54 var next
55 return function (end, cb) {
56 if (next) return next(end, cb)
57 fn(function (err, _next) {
58 if (err) return cb(err)
59 next = _next
60 next(null, cb)
61 })
62 }
63}
64
65function readOnce(fn) {
66 var ended
67 return function (end, cb) {
68 fn(function (err, data) {
69 if (err || ended) return cb(err || ended)
70 ended = true
71 cb(null, data)
72 })
73 }
74}
75
76function tryDecodeURIComponent(str) {
77 if (!str || (str[0] == '%' && ref.isBlobId(str)))
78 return str
79 try {
80 str = decodeURIComponent(str)
81 } finally {
82 return str
83 }
84}
85
86var msgTypes = {
87 'git-repo': true,
88 'git-update': true
89}
90
91var refLabels = {
92 heads: 'Branches'
93}
94
95module.exports = function (listenAddr, cb) {
96 var ssb, reconnect, myId
97 var about = function (id, cb) { cb(null, {name: id}) }
98
99 var addr = parseAddr(listenAddr, {host: 'localhost', port: 7718})
100 http.createServer(onRequest).listen(addr.port, addr.host, onListening)
101
102 var server = {
103 setSSB: function (_ssb, _reconnect) {
104 ssb = _ssb
105 reconnect = _reconnect
106 ssb.whoami(function (err, feed) {
107 myId = feed.id
108 about = ssbAbout(ssb, ssb.id)
109 })
110 }
111 }
112
113 function onListening() {
114 var host = ~addr.host.indexOf(':') ? '[' + addr.host + ']' : addr.host
115 console.error('Listening on http://' + host + ':' + addr.port + '/')
116 cb(null, server)
117 }
118
119 /* Serving a request */
120
121 function onRequest(req, res) {
122 console.log(req.method, req.url)
123 pull(
124 handleRequest(req),
125 pull.filter(function (data) {
126 if (Array.isArray(data)) {
127 res.writeHead.apply(res, data)
128 return false
129 }
130 return true
131 }),
132 toPull(res)
133 )
134 }
135
136 function handleRequest(req) {
137 var u = url.parse(req.url)
138 var dirs = u.pathname.slice(1).split(/\/+/).map(tryDecodeURIComponent)
139 switch (dirs[0]) {
140 case '':
141 return serveIndex(req)
142 default:
143 if (ref.isMsgId(dirs[0]))
144 return serveRepoPage(dirs[0], dirs.slice(1))
145 else if (ref.isFeedId(dirs[0]))
146 return serveUserPage(dirs[0])
147 else
148 return serve404(req)
149 }
150 }
151
152 function serve404(req) {
153 var body = '404 Not Found'
154 return pull.values([
155 [404, {
156 'Content-Length': body.length,
157 'Content-Type': 'text/plain'
158 }],
159 body
160 ])
161 }
162
163 function renderTry(read) {
164 var ended
165 return function (end, cb) {
166 if (ended) return cb(ended)
167 read(end, function (err, data) {
168 if (err === true)
169 cb(true)
170 else if (err) {
171 ended = true
172 cb(null,
173 '<h3>' + err.name + '</h3>' +
174 '<pre>' + escapeHTML(err.stack) + '</pre>')
175 } else
176 cb(null, data)
177 })
178 }
179 }
180
181 function serveError(id, err) {
182 if (err.message == 'stream is closed')
183 reconnect()
184 return pull.values([
185 [500, {
186 'Content-Type': 'text/html'
187 }],
188 '<!doctype html><html><head><meta charset=utf-8>',
189 '<title>' + err.name + '</title></head><body>',
190 '<h1><a href="/">git ssb</a></h1>',
191 '<h2>' + err.name + '</h3>' +
192 '<pre>' + escapeHTML(err.stack) + '</pre>' +
193 '</body></html>'
194 ])
195 }
196
197 /* Feed */
198
199 function renderFeed(feedId) {
200 var opts = {
201 reverse: true,
202 id: feedId
203 }
204 return pull(
205 feedId ? ssb.createUserStream(opts) : ssb.createLogStream(opts),
206 pull.filter(function (msg) {
207 return msg.value.content.type in msgTypes
208 }),
209 pull.take(20),
210 pull.asyncMap(function (msg, cb) {
211 switch (msg.value.content.type) {
212 case 'git-repo': return renderRepoCreated(msg, cb)
213 case 'git-update': return renderUpdate(msg, cb)
214 }
215 })
216 )
217 }
218
219 function renderRepoCreated(msg, cb) {
220 var repoLink = link([msg.key])
221 var authorLink = link([msg.value.author])
222 cb(null, '<p>' + timestamp(msg.value.timestamp) + '<br>' +
223 authorLink + ' created repo ' + repoLink + '</p>')
224 }
225
226 function renderUpdate(msg, cb) {
227 about.getName(msg.value.author, function (err, name) {
228 if (err) return cb(err)
229 var repoLink = link([msg.value.content.repo])
230 var authorLink = link([msg.value.author], name)
231 cb(null, '<p>' + timestamp(msg.value.timestamp) + '<br>' +
232 authorLink + ' pushed to ' + repoLink + '</p>')
233 })
234 }
235
236 /* Index */
237
238 function serveIndex() {
239 return cat([
240 pull.values([
241 [200, {
242 'Content-Type': 'text/html'
243 }],
244 '<!doctype html><html><head><meta charset=utf-8>',
245 '<title>git ssb</title></head><body>',
246 '<h1><a href="/">git ssb</a></h1>'
247 ]),
248 renderTry(renderFeed()),
249 pull.once('</body></html>')
250 ])
251 }
252
253 function serveUserPage(feedId) {
254 return cat([
255 pull.values([
256 [200, {
257 'Content-Type': 'text/html'
258 }],
259 '<!doctype html><html><head><meta charset=utf-8>',
260 '<title>git ssb</title></head><body>',
261 '<h1><a href="/">git ssb</a></h1>',
262 ]),
263 readOnce(function (cb) {
264 about.getName(feedId, function (err, name) {
265 cb(null, '<h2>' + link([feedId], name) + '</h2>' +
266 '<p><small><code>' + feedId + '</code></small></p>')
267 })
268 }),
269 renderFeed(feedId),
270 pull.once('</body></html>')
271 ])
272 }
273
274 /* Repo */
275
276 function serveRepoPage(id, path) {
277 var defaultBranch = 'master'
278 return readNext(function (cb) {
279 ssbGit.getRepo(ssb, id, function (err, repo) {
280 if (err) {
281 if (0)
282 cb(null, serveRepoNotFound(id, err))
283 else
284 cb(null, serveError(id, err))
285 return
286 }
287 repo = Repo(repo)
288 cb(null, (function () {
289 var branch = path[1] || defaultBranch
290 var filePath = path.slice(2)
291 switch (path[0]) {
292 case undefined:
293 return serveRepoTree(repo, branch, [])
294 case 'activity':
295 return serveRepoActivity(repo, branch)
296 case 'commits':
297 return serveRepoCommits(repo, branch)
298 case 'commit':
299 return serveRepoCommit(repo, path[1])
300 case 'tree':
301 return serveRepoTree(repo, branch, filePath)
302 case 'blob':
303 return serveBlob(repo, branch, filePath)
304 default:
305 return serve404(req)
306 }
307 })())
308 })
309 })
310 }
311
312 function serveRepoNotFound(id, err) {
313 return pull.values([
314 [404, {
315 'Content-Type': 'text/html'
316 }],
317 '<!doctype html><html><head><meta charset=utf-8>',
318 '<title>Repo not found</title></head><body>',
319 '<h1><a href="/">git ssb</a></h1>',
320 '<h2>Repo not found</h2>',
321 '<p>Repo ' + id + ' was not found</p>',
322 '<pre>' + escapeHTML(err.stack) + '</pre>',
323 '</body></html>'
324 ])
325 }
326
327 function renderRepoPage(repo, branch, body) {
328 var gitUrl = 'ssb://' + repo.id
329 var gitLink = '<code>' + gitUrl + '</code>'
330
331 return cat([
332 pull.values([
333 [200, {
334 'Content-Type': 'text/html'
335 }],
336 '<!doctype html><html><head><meta charset=utf-8>' +
337 '<title>git ssb</title></head><body>' +
338 '<h1><a href="/">git ssb</a></h1>' +
339 '<h2>' + link([repo.id]) + '</h2>' +
340 '<p>git URL: ' + gitLink + '</p>']),
341 readOnce(function (cb) {
342 about.getName(repo.feed, function (err, name) {
343 cb(null, '<p>Author: ' + link([repo.feed], name) + '</p>')
344 })
345 }),
346 pull.once(
347 '<p>' + link([repo.id], 'Code') + ' ' +
348 link([repo.id, 'activity'], 'Activity') + ' ' +
349 link([repo.id, 'commits', branch], 'Commits') + '</p>' +
350 '<hr/>'
351 ),
352 renderTry(body),
353 pull.once('<hr/></body></html>')
354 ])
355 }
356
357 function serveRepoTree(repo, rev, path) {
358 var type = repo.isCommitHash(rev) ? 'Tree' : 'Branch'
359 return renderRepoPage(repo, rev, cat([
360 pull.once('<h3>' + type + ': ' + rev + ' '),
361 revMenu(repo, rev),
362 pull.once('</h3>'),
363 type == 'Branch' && renderRepoLatest(repo, rev),
364 renderRepoTree(repo, rev, path),
365 renderRepoReadme(repo, rev, path),
366 ]))
367 }
368
369 /* Repo activity */
370
371 function serveRepoActivity(repo, branch) {
372 return renderRepoPage(repo, branch, cat([
373 pull.once('<h3>Activity</h3>'),
374 pull(
375 ssb.links({
376 type: 'git-update',
377 dest: repo.id,
378 source: repo.feed,
379 rel: 'repo',
380 values: true,
381 reverse: true,
382 limit: 8
383 }),
384 pull.map(renderRepoUpdate)
385 )
386 ]))
387
388 function renderRepoUpdate(msg) {
389 var c = msg.value.content
390
391 var refs = c.refs ? Object.keys(c.refs).map(function (ref) {
392 return {name: ref, value: c.refs[ref]}
393 }) : []
394 var numObjects = c.objects ? Object.keys(c.objects).length : 0
395
396 return '<p>' + timestamp(msg.value.timestamp) + '<br>' +
397 (numObjects ? 'Pushed ' + numObjects + ' objects<br>' : '') +
398 refs.map(function (update) {
399 var name = escapeHTML(update.name)
400 if (!update.value) {
401 return 'Deleted ' + name
402 } else {
403 var commitLink = link([repo.id, 'commit', update.value])
404 return name + ' &rarr; ' + commitLink
405 }
406 }).join('<br>') +
407 '</p>'
408 }
409 }
410
411 /* Repo commits */
412
413 function serveRepoCommits(repo, branch) {
414 return renderRepoPage(repo, branch, cat([
415 pull.once('<h3>Commits</h3><ul>'),
416 pull(
417 repo.readLog(branch),
418 pull.asyncMap(function (hash, cb) {
419 repo.getCommitParsed(hash, function (err, commit) {
420 if (err) return cb(err)
421 var commitPath = [repo.id, 'commit', commit.id]
422 var treePath = [repo.id, 'tree', commit.id]
423 cb(null, '<li>' +
424 '<strong>' + link(commitPath, escapeHTML(commit.title)) + '</strong><br>' +
425 '<code>' + commit.id + '</code> ' +
426 link(treePath, 'Tree') + '<br>' +
427 (commit.separateAuthor ? escapeHTML(commit.author.name) + ' authored on ' + commit.author.date.toLocaleString() + '<br>' : '') +
428 escapeHTML(commit.committer.name) + ' committed on ' + commit.committer.date.toLocaleString() +
429 '</li>')
430 })
431 })
432 ),
433 pull.once('</ul>')
434 ]))
435 }
436
437 /* Repo tree */
438
439 function revMenu(repo, currentName) {
440 var baseHref = '/' + encodeURIComponent(repo.id) + '/tree/'
441 var onchange = 'location.href="' + baseHref + '" + this.value'
442 var currentGroup
443 return cat([
444 pull.once('<select onchange="' + escapeHTML(onchange) + '">'),
445 pull(
446 repo.refs(),
447 pull.map(function (ref) {
448 var m = ref.name.match(/^refs\/([^\/]*)\/(.*)$/) || [,, ref.name]
449 var group = m[1]
450 var name = m[2]
451
452 var optgroup = (group === currentGroup) ? '' :
453 (currentGroup ? '</optgroup>' : '') +
454 '<optgroup label="' + (refLabels[group] || group) + '">'
455 currentGroup = group
456 var selected = (name == currentName) ? ' selected="selected"' : ''
457 var htmlName = escapeHTML(name)
458 return optgroup +
459 '<option value="' + htmlName + '"' + selected + '>' +
460 htmlName + '</option>'
461 })
462 ),
463 readOnce(function (cb) {
464 cb(null, currentGroup ? '</optgroup>' : '')
465 }),
466 pull.once('</select>')
467 ])
468 }
469
470 function renderRepoLatest(repo, rev) {
471 return readOnce(function (cb) {
472 repo.getCommitParsed(rev, function (err, commit) {
473 if (err) return cb(err)
474 var commitPath = [repo.id, 'commit', commit.id]
475 cb(null, '<p>' +
476 'Latest: <strong>' + link(commitPath, escapeHTML(commit.title)) +
477 '</strong><br>' +
478 '<code>' + commit.id + '</code><br> ' +
479 escapeHTML(commit.committer.name) + ' committed on ' +
480 commit.committer.date.toLocaleString() +
481 (commit.separateAuthor ? '<br>' +
482 escapeHTML(commit.author.name) + ' authored on ' +
483 commit.author.date.toLocaleString() : '') +
484 '</p>')
485 })
486 })
487 }
488
489 // breadcrumbs
490 function linkPath(basePath, path) {
491 path = path.slice()
492 var last = path.pop()
493 return path.map(function (dir, i) {
494 return link(basePath.concat(path.slice(0, i+1)), dir)
495 }).concat(last).join(' / ')
496 }
497
498 function renderRepoTree(repo, rev, path) {
499 var pathLinks = linkPath([repo.id, 'tree'], [rev].concat(path))
500 return cat([
501 pull.once('<h3>Files: ' + pathLinks + '</h3><ul>'),
502 pull(
503 repo.readDir(rev, path),
504 pull.map(function (file) {
505 var type = (file.mode === 040000) ? 'tree' : 'blob'
506 var filePath = [repo.id, type, rev].concat(path, file.name)
507 return '<li>' + link(filePath) + '</li>'
508 })
509 ),
510 pull.once('</ul>')
511 ])
512 }
513
514 /* Repo readme */
515
516 function renderRepoReadme(repo, branch, path) {
517 return readNext(function (cb) {
518 pull(
519 repo.readDir(branch, path),
520 pull.filter(function (file) {
521 return /readme(\.|$)/i.test(file.name)
522 }),
523 pull.take(1),
524 pull.collect(function (err, files) {
525 if (err) return cb(null, pull.empty())
526 var file = files[0]
527 if (!file)
528 return cb(null, pull.once(path.length ? '' : '<p>No readme</p>'))
529 repo.getObject(file.id, function (err, obj) {
530 if (err) return cb(null, pull.empty())
531 cb(null, cat([
532 pull.once('<h4>' + escapeHTML(file.name) + '</h4>' +
533 '<blockquote><pre>'),
534 pull(
535 obj.read,
536 escapeHTMLStream()
537 ),
538 pull.once('</pre></blockquote>')
539 ]))
540 })
541 })
542 )
543 })
544 }
545
546 /* Repo commit */
547
548 function serveRepoCommit(repo, rev) {
549 return renderRepoPage(repo, rev, cat([
550 pull.once('<h3>Commit ' + rev + '</h3>'),
551 readOnce(function (cb) {
552 repo.getCommitParsed(rev, function (err, commit) {
553 if (err) return cb(err)
554 var commitPath = [repo.id, 'commit', commit.id]
555 var treePath = [repo.id, 'tree', commit.tree]
556 cb(null,
557 '<p><strong>' + link(commitPath, escapeHTML(commit.title)) +
558 '</strong></p>' +
559 pre(commit.body) +
560 '<p>' +
561 (commit.separateAuthor ? escapeHTML(commit.author.name) +
562 ' authored on ' + commit.author.date.toLocaleString() + '<br>'
563 : '') +
564 escapeHTML(commit.committer.name) + ' committed on ' +
565 commit.committer.date.toLocaleString() + '</p>' +
566 '<p>' + commit.parents.map(function (id) {
567 return 'Parent: ' + link([repo.id, 'commit', id], id)
568 }).join('<br>') + '</p>' +
569 '<p>' +
570 (commit.tree ? 'Tree: ' + link(treePath) : 'No tree') +
571 '</p>')
572 })
573 })
574 ]))
575 }
576
577 /* Blob */
578
579 function serveBlob(repo, branch, path) {
580 return readNext(function (cb) {
581 repo.getFile(branch, path, function (err, object) {
582 if (err) return cb(null, serveBlobNotFound(repoId, err))
583 cb(null, serveObjectRaw(object))
584 })
585 })
586 }
587
588 function serveBlobNotFound(repoId, err) {
589 return pull.values([
590 [404, {
591 'Content-Type': 'text/html'
592 }],
593 '<!doctype html><html><head><meta charset=utf-8>',
594 '<title>Blob not found</title></head><body>',
595 '<h1><a href="/">git ssb</a></h1>',
596 '<h2>Blob not found</h2>',
597 '<p>Blob in repo ' + link([repoId]) + ' was not found</p>',
598 '<pre>' + escapeHTML(err.stack) + '</pre>',
599 '</body></html>'
600 ])
601 }
602
603 function serveObjectRaw(object) {
604 return cat([
605 pull.once([200, {
606 'Content-Length': object.length,
607 'Content-Type': 'text/plain'
608 }]),
609 object.read
610 ])
611 }
612
613}
614

Built with git-ssb-web