git ssb

30+

cel / git-ssb-web



Tree: 2ecf2d1d12af7be663c113640a4c6d6c8c1b7c55

Files: 2ecf2d1d12af7be663c113640a4c6d6c8c1b7c55 / index.js

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

Built with git-ssb-web