git ssb

30+

cel / git-ssb-web



Tree: cbaa9d67bdc3ff77eb179a880b36532cdcbffa7f

Files: cbaa9d67bdc3ff77eb179a880b36532cdcbffa7f / index.js

27500 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 self.getRepo(issue.project, function (err, repo) {
689 if (err) {
690 if (!repo) return cb(null,
691 self.repos.serveRepoNotFound(req, c.repo, err))
692 return cb(null, self.serveError(req, err))
693 }
694 cb(null, self.repos.serveIssueOrPullRequest(req, GitRepo(repo),
695 issue, path, id))
696 })
697 })
698 }
699 // fallthrough
700 case 'post':
701 if (ref.isMsgId(c.issue) && ref.isMsgId(c.repo)) {
702 // comment on an issue
703 var done = multicb({ pluck: 1, spread: true })
704 self.getRepo(c.repo, done())
705 self.pullReqs.get(c.issue, done())
706 return done(function (err, repo, issue) {
707 if (err) {
708 if (!repo) return cb(null,
709 self.repos.serveRepoNotFound(req, c.repo, err))
710 return cb(null, self.serveError(req, err))
711 }
712 cb(null, self.repos.serveIssueOrPullRequest(req, GitRepo(repo),
713 issue, path, id))
714 })
715 } else if (ref.isMsgId(c.root)) {
716 // comment on issue from patchwork?
717 return self.getMsg(c.root, function (err, root) {
718 if (err) return cb(null, self.serveError(req, err))
719 var repoId = root.content.repo || root.content.project
720 if (!ref.isMsgId(repoId))
721 return cb(null, self.serveGenericMessage(req, id, msg, path))
722 self.getRepo(repoId, function (err, repo) {
723 if (err) return cb(null, self.serveError(req, err))
724 switch (root.content && root.content.type) {
725 case 'issue':
726 return self.issues.get(c.root, function (err, issue) {
727 if (err) return cb(null, self.serveError(req, err))
728 return cb(null,
729 self.repos.issues.serveRepoIssue(req,
730 GitRepo(repo), issue, path, id))
731 })
732 case 'pull-request':
733 return self.pullReqs.get(c.root, function (err, pr) {
734 if (err) return cb(null, self.serveError(req, err))
735 return cb(null,
736 self.repos.pulls.serveRepoPullReq(req,
737 GitRepo(repo), pr, path, id))
738 })
739 }
740 })
741 })
742 }
743 // fallthrough
744 default:
745 if (ref.isMsgId(c.repo))
746 return self.getRepo(c.repo, function (err, repo) {
747 if (err) return cb(null,
748 self.repos.serveRepoNotFound(req, c.repo, err))
749 cb(null, self.repos.serveRepoSomething(req,
750 GitRepo(repo), id, msg, path))
751 })
752 else
753 return cb(null, self.serveGenericMessage(req, id, msg, path))
754 }
755 })
756 })
757}
758
759G.serveGenericMessage = function (req, id, msg, path) {
760 return this.serveTemplate(req, id)(pull.once(
761 '<section><h2>' + u.link([id]) + '</h2>' +
762 u.json(msg) +
763 '</section>'))
764}
765
766/* Search */
767
768G.serveSearch = function (req) {
769 var self = this
770 var q = String(req._u.query.q || '')
771 if (!q) return this.serveIndex(req)
772 var qId = q.replace(/^ssb:\/*/, '')
773 if (ref.type(qId))
774 return this.serveRedirect(req, encodeURIComponent(qId))
775
776 var search = new RegExp(q, 'i')
777 return this.serveTemplate(req, req._t('Search') + ' &middot; ' + q, 200)(
778 this.renderFeed(req, null, function (opts) {
779 opts.type == 'about'
780 return function (read) {
781 return pull(
782 many([
783 self.getRepoNames(opts),
784 read
785 ]),
786 pull.filter(function (msg) {
787 var c = msg.value.content
788 return (
789 search.test(msg.key) ||
790 c.text && search.test(c.text) ||
791 c.name && search.test(c.name) ||
792 c.title && search.test(c.title))
793 })
794 )
795 }
796 })
797 )
798}
799
800G.getRepoNames = function (opts) {
801 return pull(
802 this.ssb.messagesByType({
803 type: 'about',
804 reverse: opts.reverse,
805 lt: opts.lt,
806 gt: opts.gt,
807 }),
808 pull.filter(function (msg) {
809 return '%' == String(msg.value.content.about)[0]
810 && msg.value.content.name
811 })
812 )
813}
814
815G.serveBlobNotFound = function (req, repoId, err) {
816 return this.serveTemplate(req, req._t('error.BlobNotFound'), 404)(pull.once(
817 '<h2>' + req._t('error.BlobNotFound') + '</h2>' +
818 '<p>' + req._t('error.BlobNotFoundInRepo', {
819 repo: u.link([repoId])
820 }) + '</p>' +
821 '<pre>' + u.escape(err.stack) + '</pre>'
822 ))
823}
824
825G.serveRaw = function (length, contentType) {
826 var headers = {
827 'Content-Type': contentType || 'text/plain; charset=utf-8',
828 'Cache-Control': 'max-age=31536000'
829 }
830 if (length != null)
831 headers['Content-Length'] = length
832 return function (read) {
833 return cat([pull.once([200, headers]), read])
834 }
835}
836
837G.getBlob = function (req, key, cb) {
838 var blobs = this.ssb.blobs
839 blobs.want(key, function (err, got) {
840 if (err) cb(err)
841 else if (!got) cb(new Error(req._t('error.MissingBlob', {key: key})))
842 else cb(null, blobs.get(key))
843 })
844}
845
846G.serveBlob = function (req, key) {
847 var self = this
848 return u.readNext(function (cb) {
849 self.getBlob(req, key, function (err, read) {
850 if (err) cb(null, self.serveError(req, err))
851 else if (!read) cb(null, self.serve404(req))
852 else cb(null, self.serveRaw()(read))
853 })
854 })
855}
856

Built with git-ssb-web