git ssb

30+

cel / git-ssb-web



Tree: d7ec56cfafdb55e447fea811aad69317944ece35

Files: d7ec56cfafdb55e447fea811aad69317944ece35 / index.js

27782 bytesRaw
1var fs = require('fs')
2var http = require('http')
3var path = require('path')
4var url = require('url')
5var qs = require('querystring')
6var util = require('util')
7var ref = require('ssb-ref')
8var pull = require('pull-stream')
9var ssbGit = require('ssb-git-repo')
10var toPull = require('stream-to-pull-stream')
11var cat = require('pull-cat')
12var GitRepo = require('pull-git-repo')
13var u = require('./lib/util')
14var markdown = require('./lib/markdown')
15var paginate = require('./lib/paginate')
16var asyncMemo = require('asyncmemo')
17var multicb = require('multicb')
18var schemas = require('ssb-msg-schemas')
19var Issues = require('ssb-issues')
20var PullRequests = require('ssb-pull-requests')
21var paramap = require('pull-paramap')
22var Mentions = require('ssb-mentions')
23var many = require('pull-many')
24
25var hlCssPath = path.resolve(require.resolve('highlight.js'), '../../styles')
26
27function ParamError(msg) {
28 var err = Error.call(this, msg)
29 err.name = ParamError.name
30 return err
31}
32util.inherits(ParamError, Error)
33
34function parseAddr(str, def) {
35 if (!str) return def
36 var i = str.lastIndexOf(':')
37 if (~i) return {host: str.substr(0, i), port: str.substr(i+1)}
38 if (isNaN(str)) return {host: str, port: def.port}
39 return {host: def.host, port: str}
40}
41
42function tryDecodeURIComponent(str) {
43 if (!str || (str[0] == '%' && ref.isBlobId(str)))
44 return str
45 try {
46 str = decodeURIComponent(str)
47 } finally {
48 return str
49 }
50}
51
52function getContentType(filename) {
53 var ext = u.getExtension(filename)
54 return contentTypes[ext] || u.imgMimes[ext] || 'text/plain; charset=utf-8'
55}
56
57var contentTypes = {
58 css: 'text/css'
59}
60
61function readReqForm(req, cb) {
62 pull(
63 toPull(req),
64 pull.collect(function (err, bufs) {
65 if (err) return cb(err)
66 var data
67 try {
68 data = qs.parse(Buffer.concat(bufs).toString('ascii'))
69 } catch(e) {
70 return cb(e)
71 }
72 cb(null, data)
73 })
74 )
75}
76
77var msgTypes = {
78 'git-repo': true,
79 'git-update': true,
80 'issue': true,
81 'pull-request': true
82}
83
84var _httpServer
85
86module.exports = {
87 name: 'git-ssb-web',
88 version: require('./package').version,
89 manifest: {},
90 init: function (ssb, config, reconnect) {
91 // close existing server. when scuttlebot plugins get a deinit method, we
92 // will close it in that instead it
93 if (_httpServer)
94 _httpServer.close()
95
96 var web = new GitSSBWeb(ssb, config, reconnect)
97 _httpSserver = web.httpServer
98
99 return {}
100 }
101}
102
103function GitSSBWeb(ssb, config, reconnect) {
104 this.ssb = ssb
105 this.config = config
106 this.reconnect = reconnect
107
108 this.ssbAppname = config.appname || 'ssb'
109 this.isPublic = config.public
110 this.getVotes = require('./lib/votes')(ssb)
111 this.getMsg = asyncMemo(ssb.get)
112 this.issues = Issues.init(ssb)
113 this.pullReqs = PullRequests.init(ssb)
114 this.getRepo = asyncMemo(function (id, cb) {
115 this.getMsg(id, function (err, msg) {
116 if (err) return cb(err)
117 ssbGit.getRepo(ssb, {key: id, value: msg}, {live: true}, cb)
118 })
119 })
120
121 this.about = function (id, cb) { cb(null, {name: id}) }
122 ssb.whoami(function (err, feed) {
123 this.myId = feed.id
124 this.about = require('./lib/about')(ssb, this.myId)
125 }.bind(this))
126
127 this.i18n = require('./lib/i18n')(path.join(__dirname, 'locale'), 'en')
128 this.users = require('./lib/users')(this)
129 this.repos = require('./lib/repos')(this)
130
131 var webConfig = config['git-ssb-web'] || {}
132 var addr = parseAddr(config.listenAddr, {
133 host: webConfig.host || 'localhost',
134 port: webConfig.port || 7718
135 })
136 this.listen(addr.host, addr.port)
137}
138
139var G = GitSSBWeb.prototype
140
141G.listen = function (host, port) {
142 this.httpServer = http.createServer(G_onRequest.bind(this))
143 this.httpServer.listen(port, host, function () {
144 var hostName = ~host.indexOf(':') ? '[' + host + ']' : host
145 console.log('Listening on http://' + hostName + ':' + port + '/')
146 })
147}
148
149G.getRepoName = function (ownerId, repoId, cb) {
150 this.about.getName({
151 owner: ownerId,
152 target: repoId,
153 toString: function () {
154 // hack to fit two parameters into asyncmemo
155 return ownerId + '/' + repoId
156 }
157 }, cb)
158}
159
160G.getRepoFullName = function (author, repoId, cb) {
161 var done = multicb({ pluck: 1, spread: true })
162 this.getRepoName(author, repoId, done())
163 this.about.getName(author, done())
164 done(cb)
165}
166
167G.addAuthorName = function () {
168 var about = this.about
169 return paramap(function (msg, cb) {
170 var author = msg && msg.value && msg.value.author
171 if (!author) return cb(null, msg)
172 about.getName(author, function (err, authorName) {
173 msg.authorName = authorName
174 cb(err, msg)
175 })
176 }, 8)
177}
178
179/* Serving a request */
180
181function serve(req, res) {
182 return pull(
183 pull.filter(function (data) {
184 if (Array.isArray(data)) {
185 res.writeHead.apply(res, data)
186 return false
187 }
188 return true
189 }),
190 toPull(res)
191 )
192}
193
194function G_onRequest(req, res) {
195 console.log(req.method, req.url)
196 req._u = url.parse(req.url, true)
197 var locale = req._u.query.locale ||
198 (/locale=([^;]*)/.exec(req.headers.cookie) || [])[1]
199 var reqLocales = req.headers['accept-language']
200 this.i18n.pickCatalog(reqLocales, locale, function (err, t) {
201 if (err) return pull(this.serveError(req, err, 500), serve(req, res))
202 req._t = t
203 req._locale = t.locale
204 pull(this.handleRequest(req), serve(req, res))
205 }.bind(this))
206}
207
208G.handleRequest = function (req) {
209 var path = req._u.pathname.slice(1)
210 var dirs = ref.isLink(path) ? [path] :
211 path.split(/\/+/).map(tryDecodeURIComponent)
212 var dir = dirs[0]
213
214 if (req.method == 'POST')
215 return this.handlePOST(req, dir)
216
217 if (dir == '')
218 return this.serveIndex(req)
219 else if (dir == 'search')
220 return this.serveSearch(req)
221 else if (ref.isBlobId(dir))
222 return this.serveBlob(req, dir)
223 else if (ref.isMsgId(dir))
224 return this.serveMessage(req, dir, dirs.slice(1))
225 else if (ref.isFeedId(dir))
226 return this.users.serveUserPage(req, dir, dirs.slice(1))
227 else if (dir == 'static')
228 return this.serveFile(req, dirs)
229 else if (dir == 'highlight')
230 return this.serveFile(req, [hlCssPath].concat(dirs.slice(1)), true)
231 else
232 return this.serve404(req)
233}
234
235G.handlePOST = function (req, dir) {
236 var self = this
237 if (self.isPublic)
238 return self.serveBuffer(405, req._t('error.POSTNotAllowed'))
239 return u.readNext(function (cb) {
240 readReqForm(req, function (err, data) {
241 if (err) return cb(null, self.serveError(req, err, 400))
242 if (!data) return cb(null, self.serveError(req,
243 new ParamError(req._t('error.MissingData')), 400))
244
245 switch (data.action) {
246 case 'fork-prompt':
247 return cb(null, self.serveRedirect(req,
248 u.encodeLink([data.id, 'fork'])))
249
250 case 'fork':
251 if (!data.id)
252 return cb(null, self.serveError(req,
253 new ParamError(req._t('error.MissingId')), 400))
254 return ssbGit.createRepo(self.ssb, {upstream: data.id},
255 function (err, repo) {
256 if (err) return cb(null, self.serveError(req, err))
257 cb(null, self.serveRedirect(req, u.encodeLink(repo.id)))
258 })
259
260 case 'vote':
261 var voteValue = +data.value || 0
262 if (!data.id)
263 return cb(null, self.serveError(req,
264 new ParamError(req._t('error.MissingId')), 400))
265 var msg = schemas.vote(data.id, voteValue)
266 return self.ssb.publish(msg, function (err) {
267 if (err) return cb(null, self.serveError(req, err))
268 cb(null, self.serveRedirect(req, req.url))
269 })
270
271 case 'repo-name':
272 if (!data.id)
273 return cb(null, self.serveError(req,
274 new ParamError(req._t('error.MissingId')), 400))
275 if (!data.name)
276 return cb(null, self.serveError(req,
277 new ParamError(req._t('error.MissingName')), 400))
278 var msg = schemas.name(data.id, data.name)
279 return self.ssb.publish(msg, function (err) {
280 if (err) return cb(null, self.serveError(req, err))
281 cb(null, self.serveRedirect(req, req.url))
282 })
283
284 case 'issue-title':
285 if (!data.id)
286 return cb(null, self.serveError(req,
287 new ParamError(req._t('error.MissingId')), 400))
288 if (!data.name)
289 return cb(null, self.serveError(req,
290 new ParamError(req._t('error.MissingName')), 400))
291 var msg = Issues.schemas.edit(data.id, {title: data.name})
292 return self.ssb.publish(msg, function (err) {
293 if (err) return cb(null, self.serveError(req, err))
294 cb(null, self.serveRedirect(req, req.url))
295 })
296
297 case 'comment':
298 if (!data.id)
299 return cb(null, self.serveError(req,
300 new ParamError(req._t('error.MissingId')), 400))
301 var msg = schemas.post(data.text, data.id, data.branch || data.id)
302 msg.issue = data.issue
303 msg.repo = data.repo
304 if (data.open != null)
305 Issues.schemas.reopens(msg, data.id)
306 if (data.close != null)
307 Issues.schemas.closes(msg, data.id)
308 var mentions = Mentions(data.text)
309 if (mentions.length)
310 msg.mentions = mentions
311 return self.ssb.publish(msg, function (err) {
312 if (err) return cb(null, self.serveError(req, err))
313 cb(null, self.serveRedirect(req, req.url))
314 })
315
316 case 'new-issue':
317 var msg = Issues.schemas.new(dir, data.title, data.text)
318 var mentions = Mentions(data.text)
319 if (mentions.length)
320 msg.mentions = mentions
321 return self.ssb.publish(msg, function (err, msg) {
322 if (err) return cb(null, self.serveError(req, err))
323 cb(null, self.serveRedirect(req, u.encodeLink(msg.key)))
324 })
325
326 case 'new-pull':
327 var msg = PullRequests.schemas.new(dir, data.branch,
328 data.head_repo, data.head_branch, data.title, data.text)
329 var mentions = Mentions(data.text)
330 if (mentions.length)
331 msg.mentions = mentions
332 return self.ssb.publish(msg, function (err, msg) {
333 if (err) return cb(null, self.serveError(req, err))
334 cb(null, self.serveRedirect(req, u.encodeLink(msg.key)))
335 })
336
337 case 'markdown':
338 return cb(null, self.serveMarkdown(data.text, {id: data.repo}))
339
340 default:
341 cb(null, self.serveBuffer(400, req._t('error.UnknownAction', data)))
342 }
343 })
344 })
345}
346
347G.serveFile = function (req, dirs, outside) {
348 var filename = path.resolve.apply(path, [__dirname].concat(dirs))
349 // prevent escaping base dir
350 if (!outside && filename.indexOf('../') === 0)
351 return this.serveBuffer(403, req._t("error.403Forbidden"))
352
353 return u.readNext(function (cb) {
354 fs.stat(filename, function (err, stats) {
355 cb(null, err ?
356 err.code == 'ENOENT' ? this.serve404(req)
357 : this.serveBuffer(500, err.message)
358 : 'if-modified-since' in req.headers &&
359 new Date(req.headers['if-modified-since']) >= stats.mtime ?
360 pull.once([304])
361 : stats.isDirectory() ?
362 this.serveBuffer(403, req._t('error.DirectoryNotListable'))
363 : cat([
364 pull.once([200, {
365 'Content-Type': getContentType(filename),
366 'Content-Length': stats.size,
367 'Last-Modified': stats.mtime.toGMTString()
368 }]),
369 toPull(fs.createReadStream(filename))
370 ]))
371 }.bind(this))
372 }.bind(this))
373}
374
375G.serveBuffer = function (code, buf, contentType, headers) {
376 headers = headers || {}
377 headers['Content-Type'] = contentType || 'text/plain; charset=utf-8'
378 headers['Content-Length'] = Buffer.byteLength(buf)
379 return pull.values([
380 [code, headers],
381 buf
382 ])
383}
384
385G.serve404 = function (req) {
386 return this.serveBuffer(404, req._t("error.404NotFound"))
387}
388
389G.serveRedirect = function (req, path) {
390 return this.serveBuffer(302,
391 '<!doctype><html><head>' +
392 '<title>' + req._t('Redirect') + '</title></head><body>' +
393 '<p><a href="' + u.escape(path) + '">' +
394 req._t('Continue') + '</a></p>' +
395 '</body></html>', 'text/html; charset=utf-8', {Location: path})
396}
397
398G.serveMarkdown = function (text, repo) {
399 return this.serveBuffer(200, markdown(text, repo),
400 'text/html; charset=utf-8')
401}
402
403G.renderError = function (err, tag) {
404 tag = tag || 'h3'
405 return '<' + tag + '>' + err.name + '</' + tag + '>' +
406 '<pre>' + u.escape(err.stack) + '</pre>'
407}
408
409function renderTry(read) {
410 var self = this
411 var ended
412 return function (end, cb) {
413 if (ended) return cb(ended)
414 read(end, function (err, data) {
415 if (err === true)
416 cb(true)
417 else if (err) {
418 ended = true
419 cb(null, self.renderError(err))
420 } else
421 cb(null, data)
422 })
423 }
424}
425
426G.serveTemplate = function (req, title, code, read) {
427 if (read === undefined)
428 return this.serveTemplate.bind(this, req, title, code)
429 var q = req._u.query.q && u.escape(req._u.query.q) || ''
430 var app = 'git ssb'
431 var appName = this.ssbAppname
432 if (req._t) app = req._t(app)
433 return cat([
434 pull.values([
435 [code || 200, {
436 'Content-Type': 'text/html'
437 }],
438 '<!doctype html><html><head><meta charset=utf-8>',
439 '<title>' + (title || app) + '</title>',
440 '<link rel=stylesheet href="/static/styles.css"/>',
441 '<link rel=stylesheet href="/highlight/github.css"/>',
442 '</head>\n',
443 '<body>',
444 '<header><form action="/search" method="get">' +
445 '<h1><a href="/">' + app + '' +
446 (appName != 'ssb' ? ' <sub>' + appName + '</sub>' : '') +
447 '</a> ' +
448 '<input class="search-bar" name="q" size="60"' +
449 ' placeholder="🔍" value="' + q + '" />' +
450 '</h1>',
451 '</form></header>',
452 '<article>']),
453 renderTry(read),
454 pull.once('<hr/></article></body></html>')
455 ])
456}
457
458G.serveError = function (req, err, status) {
459 if (err.message == 'stream is closed')
460 this.reconnect && this.reconnect()
461 return pull(
462 pull.once(this.renderError(err, 'h2')),
463 this.serveTemplate(req, err.name, status || 500)
464 )
465}
466
467G.renderObjectData = function (obj, filename, repo, rev, path) {
468 var ext = u.getExtension(filename)
469 return u.readOnce(function (cb) {
470 u.readObjectString(obj, function (err, buf) {
471 buf = buf.toString('utf8')
472 if (err) return cb(err)
473 cb(null, (ext == 'md' || ext == 'markdown')
474 ? markdown(buf, {repo: repo, rev: rev, path: path})
475 : renderCodeTable(buf, ext))
476 })
477 })
478}
479
480function renderCodeTable(buf, ext) {
481 return '<pre><table class="code">' +
482 u.highlight(buf, ext).split('\n').map(function (line, i) {
483 i++
484 return '<tr id="L' + i + '">' +
485 '<td class="code-linenum">' + '<a href="#L' + i + '">' + i + '</td>' +
486 '<td class="code-text">' + line + '</td></tr>'
487 }).join('') +
488 '</table></pre>'
489}
490
491/* Feed */
492
493G.renderFeed = function (req, feedId, filter) {
494 var query = req._u.query
495 var opts = {
496 reverse: !query.forwards,
497 lt: query.lt && +query.lt || Date.now(),
498 gt: query.gt ? +query.gt : -Infinity,
499 id: feedId
500 }
501 return pull(
502 feedId ? this.ssb.createUserStream(opts) : this.ssb.createFeedStream(opts),
503 pull.filter(function (msg) {
504 var c = msg.value.content
505 return c.type in msgTypes
506 || (c.type == 'post' && c.repo && c.issue)
507 }),
508 typeof filter == 'function' ? filter(opts) : filter,
509 pull.take(20),
510 this.addAuthorName(),
511 query.forwards && u.pullReverse(),
512 paginate(
513 function (first, cb) {
514 if (!query.lt && !query.gt) return cb(null, '')
515 var gt = feedId ? first.value.sequence : first.value.timestamp + 1
516 query.gt = gt
517 query.forwards = 1
518 delete query.lt
519 cb(null, '<a href="?' + qs.stringify(query) + '">' +
520 req._t('Newer') + '</a>')
521 },
522 paramap(this.renderFeedItem.bind(this, req), 8),
523 function (last, cb) {
524 query.lt = feedId ? last.value.sequence : last.value.timestamp - 1
525 delete query.gt
526 delete query.forwards
527 cb(null, '<a href="?' + qs.stringify(query) + '">' +
528 req._t('Older') + '</a>')
529 },
530 function (cb) {
531 if (query.forwards) {
532 delete query.gt
533 delete query.forwards
534 query.lt = opts.gt + 1
535 } else {
536 delete query.lt
537 query.gt = opts.lt - 1
538 query.forwards = 1
539 }
540 cb(null, '<a href="?' + qs.stringify(query) + '">' +
541 req._t(query.forwards ? 'Older' : 'Newer') + '</a>')
542 }
543 )
544 )
545}
546
547G.renderFeedItem = function (req, msg, cb) {
548 var self = this
549 var c = msg.value.content
550 var msgLink = u.link([msg.key],
551 new Date(msg.value.timestamp).toLocaleString(req._locale))
552 var author = msg.value.author
553 var authorLink = u.link([msg.value.author], msg.authorName)
554 switch (c.type) {
555 case 'git-repo':
556 var done = multicb({ pluck: 1, spread: true })
557 self.getRepoName(author, msg.key, done())
558 if (c.upstream) {
559 return self.getMsg(c.upstream, function (err, upstreamMsg) {
560 if (err) return cb(null, self.serveError(req, err))
561 self.getRepoName(upstreamMsg.author, c.upstream, done())
562 done(function (err, repoName, upstreamName) {
563 cb(null, '<section class="collapse">' + msgLink + '<br>' +
564 req._t('Forked', {
565 name: authorLink,
566 upstream: u.link([c.upstream], upstreamName),
567 repo: u.link([msg.key], repoName)
568 }) + '</section>')
569 })
570 })
571 } else {
572 return done(function (err, repoName) {
573 if (err) return cb(err)
574 var repoLink = u.link([msg.key], repoName)
575 cb(null, '<section class="collapse">' + msgLink + '<br>' +
576 req._t('CreatedRepo', {
577 name: authorLink,
578 repo: repoLink
579 }) + '</section>')
580 })
581 }
582 case 'git-update':
583 return self.getRepoName(author, c.repo, function (err, repoName) {
584 if (err) return cb(err)
585 var repoLink = u.link([c.repo], repoName)
586 cb(null, '<section class="collapse">' + msgLink + '<br>' +
587 req._t('Pushed', {
588 name: authorLink,
589 repo: repoLink
590 }) + '</section>')
591 })
592 case 'issue':
593 case 'pull-request':
594 var issueLink = u.link([msg.key], c.title)
595 return self.getMsg(c.project, function (err, projectMsg) {
596 if (err) return cb(null,
597 self.repos.serveRepoNotFound(req, c.repo, err))
598 self.getRepoName(projectMsg.author, c.project,
599 function (err, repoName) {
600 if (err) return cb(err)
601 var repoLink = u.link([c.project], repoName)
602 cb(null, '<section class="collapse">' + msgLink + '<br>' +
603 req._t('OpenedIssue', {
604 name: authorLink,
605 type: req._t(c.type == 'pull-request' ?
606 'pull request' : 'issue.'),
607 title: issueLink,
608 project: repoLink
609 }) + '</section>')
610 })
611 })
612 case 'about':
613 return cb(null, '<section class="collapse">' + msgLink + '<br>' +
614 req._t('Named', {
615 author: authorLink,
616 target: '<tt>' + u.escape(c.about) + '</tt>',
617 name: u.link([c.about], c.name)
618 }) + '</section>')
619 case 'post':
620 return this.pullReqs.get(c.issue, function (err, pr) {
621 if (err) return cb(err)
622 var type = pr.msg.value.content.type == 'pull-request' ?
623 'pull request' : 'issue.'
624 return cb(null, '<section class="collapse">' + msgLink + '<br>' +
625 req._t('CommentedOn', {
626 author: authorLink,
627 target: req._t(type) + ' ' + u.link([pr.id], pr.title, true)
628 }) +
629 '<blockquote>' + markdown(c.text) + '</blockquote>' +
630 '</section>')
631 })
632 default:
633 return cb(null, u.json(msg))
634 }
635}
636
637/* Index */
638
639G.serveIndex = function (req) {
640 return this.serveTemplate(req)(this.renderFeed(req))
641}
642
643/* Message */
644
645G.serveMessage = function (req, id, path) {
646 var self = this
647 return u.readNext(function (cb) {
648 self.ssb.get(id, function (err, msg) {
649 if (err) return cb(null, self.serveError(req, err))
650 var c = msg.content || {}
651 switch (c.type) {
652 case 'git-repo':
653 return self.getRepo(id, function (err, repo) {
654 if (err) return cb(null, self.serveError(req, err))
655 cb(null, self.repos.serveRepoPage(req, GitRepo(repo), path))
656 })
657 case 'git-update':
658 return self.getRepo(c.repo, function (err, repo) {
659 if (err) return cb(null,
660 self.repos.serveRepoNotFound(req, c.repo, err))
661 cb(null, self.repos.serveRepoUpdate(req,
662 GitRepo(repo), id, msg, path))
663 })
664 case 'issue':
665 return self.getRepo(c.project, function (err, repo) {
666 if (err) return cb(null,
667 self.repos.serveRepoNotFound(req, c.project, err))
668 self.issues.get(id, function (err, issue) {
669 if (err) return cb(null, self.serveError(req, err))
670 cb(null, self.repos.issues.serveRepoIssue(req,
671 GitRepo(repo), issue, path))
672 })
673 })
674 case 'pull-request':
675 return self.getRepo(c.repo, function (err, repo) {
676 if (err) return cb(null,
677 self.repos.serveRepoNotFound(req, c.project, err))
678 self.pullReqs.get(id, function (err, pr) {
679 if (err) return cb(null, self.serveError(req, err))
680 cb(null, self.repos.pulls.serveRepoPullReq(req,
681 GitRepo(repo), pr, path))
682 })
683 })
684 case 'issue-edit':
685 if (ref.isMsgId(c.issue)) {
686 return self.pullReqs.get(c.issue, function (err, issue) {
687 if (err) return cb(err)
688 var serve = issue.msg.value.content.type == 'pull-request'
689 ? self.repos.pulls.serveRepoPullReq
690 : self.repos.issues.serveRepoIssue
691 self.getRepo(issue.project, function (err, repo) {
692 if (err) {
693 if (!repo) return cb(null,
694 self.repos.serveRepoNotFound(req, c.repo, err))
695 return cb(null, self.serveError(req, err))
696 }
697 cb(null, serve.call(self, req, GitRepo(repo), issue, path, id))
698 })
699 })
700 }
701 // fallthrough
702 case 'post':
703 if (ref.isMsgId(c.issue) && ref.isMsgId(c.repo)) {
704 // comment on an issue
705 var done = multicb({ pluck: 1, spread: true })
706 self.getRepo(c.repo, done())
707 self.pullReqs.get(c.issue, done())
708 return done(function (err, repo, issue) {
709 if (err) {
710 if (!repo) return cb(null,
711 self.repos.serveRepoNotFound(req, c.repo, err))
712 return cb(null, self.serveError(req, err))
713 }
714 var serve = issue.msg.value.content.type == 'pull-request'
715 ? self.repos.pulls.serveRepoPullReq
716 : self.repos.issues.serveRepoIssue
717 cb(null, serve.call(self, req, GitRepo(repo), issue, path, id))
718 })
719 } else if (ref.isMsgId(c.root)) {
720 // comment on issue from patchwork?
721 return self.getMsg(c.root, function (err, root) {
722 if (err) return cb(null, self.serveError(req, err))
723 var repoId = root.content.repo || root.content.project
724 if (!ref.isMsgId(repoId))
725 return cb(null, self.serveGenericMessage(req, id, msg, path))
726 self.getRepo(repoId, function (err, repo) {
727 if (err) return cb(null, self.serveError(req, err))
728 switch (root.content && root.content.type) {
729 case 'issue':
730 return self.issues.get(c.root, function (err, issue) {
731 if (err) return cb(null, self.serveError(req, err))
732 return cb(null,
733 self.repos.issues.serveRepoIssue(req,
734 GitRepo(repo), issue, path, id))
735 })
736 case 'pull-request':
737 return self.pullReqs.get(c.root, function (err, pr) {
738 if (err) return cb(null, self.serveError(req, err))
739 return cb(null,
740 self.repos.pulls.serveRepoPullReq(req,
741 GitRepo(repo), pr, path, id))
742 })
743 }
744 })
745 })
746 }
747 // fallthrough
748 default:
749 if (ref.isMsgId(c.repo))
750 return self.getRepo(c.repo, function (err, repo) {
751 if (err) return cb(null,
752 self.repos.serveRepoNotFound(req, c.repo, err))
753 cb(null, self.repos.serveRepoSomething(req,
754 GitRepo(repo), id, msg, path))
755 })
756 else
757 return cb(null, self.serveGenericMessage(req, id, msg, path))
758 }
759 })
760 })
761}
762
763G.serveGenericMessage = function (req, id, msg, path) {
764 return this.serveTemplate(req, id)(pull.once(
765 '<section><h2>' + u.link([id]) + '</h2>' +
766 u.json(msg) +
767 '</section>'))
768}
769
770/* Search */
771
772G.serveSearch = function (req) {
773 var self = this
774 var q = String(req._u.query.q || '')
775 if (!q) return this.serveIndex(req)
776 var qId = q.replace(/^ssb:\/*/, '')
777 if (ref.type(qId))
778 return this.serveRedirect(req, encodeURIComponent(qId))
779
780 var search = new RegExp(q, 'i')
781 return this.serveTemplate(req, req._t('Search') + ' &middot; ' + q, 200)(
782 this.renderFeed(req, null, function (opts) {
783 opts.type == 'about'
784 return function (read) {
785 return pull(
786 many([
787 self.getRepoNames(opts),
788 read
789 ]),
790 pull.filter(function (msg) {
791 var c = msg.value.content
792 return (
793 search.test(msg.key) ||
794 c.text && search.test(c.text) ||
795 c.name && search.test(c.name) ||
796 c.title && search.test(c.title))
797 })
798 )
799 }
800 })
801 )
802}
803
804G.getRepoNames = function (opts) {
805 return pull(
806 this.ssb.messagesByType({
807 type: 'about',
808 reverse: opts.reverse,
809 lt: opts.lt,
810 gt: opts.gt,
811 }),
812 pull.filter(function (msg) {
813 return '%' == String(msg.value.content.about)[0]
814 && msg.value.content.name
815 })
816 )
817}
818
819G.serveBlobNotFound = function (req, repoId, err) {
820 return this.serveTemplate(req, req._t('error.BlobNotFound'), 404)(pull.once(
821 '<h2>' + req._t('error.BlobNotFound') + '</h2>' +
822 '<p>' + req._t('error.BlobNotFoundInRepo', {
823 repo: u.link([repoId])
824 }) + '</p>' +
825 '<pre>' + u.escape(err.stack) + '</pre>'
826 ))
827}
828
829G.serveRaw = function (length, contentType) {
830 var headers = {
831 'Content-Type': contentType || 'text/plain; charset=utf-8',
832 'Cache-Control': 'max-age=31536000'
833 }
834 if (length != null)
835 headers['Content-Length'] = length
836 return function (read) {
837 return cat([pull.once([200, headers]), read])
838 }
839}
840
841G.getBlob = function (req, key, cb) {
842 var blobs = this.ssb.blobs
843 blobs.want(key, function (err, got) {
844 if (err) cb(err)
845 else if (!got) cb(new Error(req._t('error.MissingBlob', {key: key})))
846 else cb(null, blobs.get(key))
847 })
848}
849
850G.serveBlob = function (req, key) {
851 var self = this
852 return u.readNext(function (cb) {
853 self.getBlob(req, key, function (err, read) {
854 if (err) cb(null, self.serveError(req, err))
855 else if (!read) cb(null, self.serve404(req))
856 else cb(null, self.serveRaw()(read))
857 })
858 })
859}
860

Built with git-ssb-web