git ssb

30+

cel / git-ssb-web



Tree: 20a18df438a0929c10fcd025ca475c2ed6f3b0e9

Files: 20a18df438a0929c10fcd025ca475c2ed6f3b0e9 / index.js

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

Built with git-ssb-web