git ssb

30+

cel / git-ssb-web



Tree: 2324d21c2b5a2390f8620efc18d98402e2474ee6

Files: 2324d21c2b5a2390f8620efc18d98402e2474ee6 / index.js

28048 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 if (config.logging && config.logging.level)
109 this.logLevel = this.logLevels.indexOf(config.logging.level)
110 this.ssbAppname = config.appname || 'ssb'
111 this.isPublic = config.public
112 this.getVotes = require('./lib/votes')(ssb)
113 this.getMsg = asyncMemo(ssb.get)
114 this.issues = Issues.init(ssb)
115 this.pullReqs = PullRequests.init(ssb)
116 this.getRepo = asyncMemo(function (id, cb) {
117 this.getMsg(id, function (err, msg) {
118 if (err) return cb(err)
119 ssbGit.getRepo(ssb, {key: id, value: msg}, {live: true}, cb)
120 })
121 })
122
123 this.about = function (id, cb) { cb(null, {name: id}) }
124 ssb.whoami(function (err, feed) {
125 this.myId = feed.id
126 this.about = require('./lib/about')(ssb, this.myId)
127 }.bind(this))
128
129 this.i18n = require('./lib/i18n')(path.join(__dirname, 'locale'), 'en')
130 this.users = require('./lib/users')(this)
131 this.repos = require('./lib/repos')(this)
132
133 var webConfig = config['git-ssb-web'] || {}
134 var addr = parseAddr(config.listenAddr, {
135 host: webConfig.host || 'localhost',
136 port: webConfig.port || 7718
137 })
138 this.listen(addr.host, addr.port)
139}
140
141var G = GitSSBWeb.prototype
142
143G.logLevels = ['error', 'warning', 'notice', 'info']
144G.logLevel = G.logLevels.indexOf('notice')
145
146G.log = function (level) {
147 if (this.logLevels.indexOf(level) > this.logLevel) return
148 console.log.apply(console, [].slice.call(arguments, 1))
149}
150
151G.listen = function (host, port) {
152 this.httpServer = http.createServer(G_onRequest.bind(this))
153 this.httpServer.listen(port, host, function () {
154 var hostName = ~host.indexOf(':') ? '[' + host + ']' : host
155 this.log('notice', 'Listening on http://' + hostName + ':' + port + '/')
156 }.bind(this))
157}
158
159G.getRepoName = function (ownerId, repoId, cb) {
160 this.about.getName({
161 owner: ownerId,
162 target: repoId,
163 toString: function () {
164 // hack to fit two parameters into asyncmemo
165 return ownerId + '/' + repoId
166 }
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
242 return this.serve404(req)
243}
244
245G.handlePOST = function (req, dir) {
246 var self = this
247 if (self.isPublic)
248 return self.serveBuffer(405, req._t('error.POSTNotAllowed'))
249 return u.readNext(function (cb) {
250 readReqForm(req, function (err, data) {
251 if (err) return cb(null, self.serveError(req, err, 400))
252 if (!data) return cb(null, self.serveError(req,
253 new ParamError(req._t('error.MissingData')), 400))
254
255 switch (data.action) {
256 case 'fork-prompt':
257 return cb(null, self.serveRedirect(req,
258 u.encodeLink([data.id, 'fork'])))
259
260 case 'fork':
261 if (!data.id)
262 return cb(null, self.serveError(req,
263 new ParamError(req._t('error.MissingId')), 400))
264 return ssbGit.createRepo(self.ssb, {upstream: data.id},
265 function (err, repo) {
266 if (err) return cb(null, self.serveError(req, err))
267 cb(null, self.serveRedirect(req, u.encodeLink(repo.id)))
268 })
269
270 case 'vote':
271 var voteValue = +data.value || 0
272 if (!data.id)
273 return cb(null, self.serveError(req,
274 new ParamError(req._t('error.MissingId')), 400))
275 var msg = schemas.vote(data.id, voteValue)
276 return self.ssb.publish(msg, function (err) {
277 if (err) return cb(null, self.serveError(req, err))
278 cb(null, self.serveRedirect(req, req.url))
279 })
280
281 case 'repo-name':
282 if (!data.id)
283 return cb(null, self.serveError(req,
284 new ParamError(req._t('error.MissingId')), 400))
285 if (!data.name)
286 return cb(null, self.serveError(req,
287 new ParamError(req._t('error.MissingName')), 400))
288 var msg = schemas.name(data.id, data.name)
289 return self.ssb.publish(msg, function (err) {
290 if (err) return cb(null, self.serveError(req, err))
291 cb(null, self.serveRedirect(req, req.url))
292 })
293
294 case 'issue-title':
295 if (!data.id)
296 return cb(null, self.serveError(req,
297 new ParamError(req._t('error.MissingId')), 400))
298 if (!data.name)
299 return cb(null, self.serveError(req,
300 new ParamError(req._t('error.MissingName')), 400))
301 var msg = Issues.schemas.edit(data.id, {title: data.name})
302 return self.ssb.publish(msg, function (err) {
303 if (err) return cb(null, self.serveError(req, err))
304 cb(null, self.serveRedirect(req, req.url))
305 })
306
307 case 'comment':
308 if (!data.id)
309 return cb(null, self.serveError(req,
310 new ParamError(req._t('error.MissingId')), 400))
311 var msg = schemas.post(data.text, data.id, data.branch || data.id)
312 msg.issue = data.issue
313 msg.repo = data.repo
314 if (data.open != null)
315 Issues.schemas.reopens(msg, data.id)
316 if (data.close != null)
317 Issues.schemas.closes(msg, data.id)
318 var mentions = Mentions(data.text)
319 if (mentions.length)
320 msg.mentions = mentions
321 return self.ssb.publish(msg, function (err) {
322 if (err) return cb(null, self.serveError(req, err))
323 cb(null, self.serveRedirect(req, req.url))
324 })
325
326 case 'new-issue':
327 var msg = Issues.schemas.new(dir, data.title, 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 'new-pull':
337 var msg = PullRequests.schemas.new(dir, data.branch,
338 data.head_repo, data.head_branch, data.title, data.text)
339 var mentions = Mentions(data.text)
340 if (mentions.length)
341 msg.mentions = mentions
342 return self.ssb.publish(msg, function (err, msg) {
343 if (err) return cb(null, self.serveError(req, err))
344 cb(null, self.serveRedirect(req, u.encodeLink(msg.key)))
345 })
346
347 case 'markdown':
348 return cb(null, self.serveMarkdown(data.text, {id: data.repo}))
349
350 default:
351 cb(null, self.serveBuffer(400, req._t('error.UnknownAction', data)))
352 }
353 })
354 })
355}
356
357G.serveFile = function (req, dirs, outside) {
358 var filename = path.resolve.apply(path, [__dirname].concat(dirs))
359 // prevent escaping base dir
360 if (!outside && filename.indexOf('../') === 0)
361 return this.serveBuffer(403, req._t("error.403Forbidden"))
362
363 return u.readNext(function (cb) {
364 fs.stat(filename, function (err, stats) {
365 cb(null, err ?
366 err.code == 'ENOENT' ? this.serve404(req)
367 : this.serveBuffer(500, err.message)
368 : 'if-modified-since' in req.headers &&
369 new Date(req.headers['if-modified-since']) >= stats.mtime ?
370 pull.once([304])
371 : stats.isDirectory() ?
372 this.serveBuffer(403, req._t('error.DirectoryNotListable'))
373 : cat([
374 pull.once([200, {
375 'Content-Type': getContentType(filename),
376 'Content-Length': stats.size,
377 'Last-Modified': stats.mtime.toGMTString()
378 }]),
379 toPull(fs.createReadStream(filename))
380 ]))
381 }.bind(this))
382 }.bind(this))
383}
384
385G.serveBuffer = function (code, buf, contentType, headers) {
386 headers = headers || {}
387 headers['Content-Type'] = contentType || 'text/plain; charset=utf-8'
388 headers['Content-Length'] = Buffer.byteLength(buf)
389 return pull.values([
390 [code, headers],
391 buf
392 ])
393}
394
395G.serve404 = function (req) {
396 return this.serveBuffer(404, req._t("error.404NotFound"))
397}
398
399G.serveRedirect = function (req, path) {
400 return this.serveBuffer(302,
401 '<!doctype><html><head>' +
402 '<title>' + req._t('Redirect') + '</title></head><body>' +
403 '<p><a href="' + u.escape(path) + '">' +
404 req._t('Continue') + '</a></p>' +
405 '</body></html>', 'text/html; charset=utf-8', {Location: path})
406}
407
408G.serveMarkdown = function (text, repo) {
409 return this.serveBuffer(200, markdown(text, repo),
410 'text/html; charset=utf-8')
411}
412
413G.renderError = function (err, tag) {
414 tag = tag || 'h3'
415 return '<' + tag + '>' + err.name + '</' + tag + '>' +
416 '<pre>' + u.escape(err.stack) + '</pre>'
417}
418
419G.renderTry = function (read) {
420 var self = this
421 var ended
422 return function (end, cb) {
423 if (ended) return cb(ended)
424 read(end, function (err, data) {
425 if (err === true)
426 cb(true)
427 else if (err) {
428 ended = true
429 cb(null, self.renderError(err))
430 } else
431 cb(null, data)
432 })
433 }
434}
435
436G.serveTemplate = function (req, title, code, read) {
437 if (read === undefined)
438 return this.serveTemplate.bind(this, req, title, code)
439 var q = req._u.query.q && u.escape(req._u.query.q) || ''
440 var app = 'git ssb'
441 var appName = this.ssbAppname
442 if (req._t) app = req._t(app)
443 return cat([
444 pull.values([
445 [code || 200, {
446 'Content-Type': 'text/html'
447 }],
448 '<!doctype html><html><head><meta charset=utf-8>',
449 '<title>' + (title || app) + '</title>',
450 '<link rel=stylesheet href="/static/styles.css"/>',
451 '<link rel=stylesheet href="/highlight/github.css"/>',
452 '</head>\n',
453 '<body>',
454 '<header><form action="/search" method="get">' +
455 '<h1><a href="/">' + app + '' +
456 (appName != 'ssb' ? ' <sub>' + appName + '</sub>' : '') +
457 '</a> ' +
458 '<input class="search-bar" name="q" size="60"' +
459 ' placeholder="🔍" value="' + q + '" />' +
460 '</h1>',
461 '</form></header>',
462 '<article>']),
463 this.renderTry(read),
464 pull.once('<hr/></article></body></html>')
465 ])
466}
467
468G.serveError = function (req, err, status) {
469 if (err.message == 'stream is closed')
470 this.reconnect && this.reconnect()
471 return pull(
472 pull.once(this.renderError(err, 'h2')),
473 this.serveTemplate(req, err.name, status || 500)
474 )
475}
476
477G.renderObjectData = function (obj, filename, repo, rev, path) {
478 var ext = u.getExtension(filename)
479 return u.readOnce(function (cb) {
480 u.readObjectString(obj, function (err, buf) {
481 buf = buf.toString('utf8')
482 if (err) return cb(err)
483 cb(null, (ext == 'md' || ext == 'markdown')
484 ? markdown(buf, {repo: repo, rev: rev, path: path})
485 : renderCodeTable(buf, ext))
486 })
487 })
488}
489
490function renderCodeTable(buf, ext) {
491 return '<pre><table class="code">' +
492 u.highlight(buf, ext).split('\n').map(function (line, i) {
493 i++
494 return '<tr id="L' + i + '">' +
495 '<td class="code-linenum">' + '<a href="#L' + i + '">' + i + '</td>' +
496 '<td class="code-text">' + line + '</td></tr>'
497 }).join('') +
498 '</table></pre>'
499}
500
501/* Feed */
502
503G.renderFeed = function (req, feedId, filter) {
504 var query = req._u.query
505 var opts = {
506 reverse: !query.forwards,
507 lt: query.lt && +query.lt || Date.now(),
508 gt: query.gt ? +query.gt : -Infinity,
509 id: feedId
510 }
511 return pull(
512 feedId ? this.ssb.createUserStream(opts) : this.ssb.createFeedStream(opts),
513 pull.filter(function (msg) {
514 var c = msg.value.content
515 return c.type in msgTypes
516 || (c.type == 'post' && c.repo && c.issue)
517 }),
518 typeof filter == 'function' ? filter(opts) : filter,
519 pull.take(20),
520 this.addAuthorName(),
521 query.forwards && u.pullReverse(),
522 paginate(
523 function (first, cb) {
524 if (!query.lt && !query.gt) return cb(null, '')
525 var gt = feedId ? first.value.sequence : first.value.timestamp + 1
526 query.gt = gt
527 query.forwards = 1
528 delete query.lt
529 cb(null, '<a href="?' + qs.stringify(query) + '">' +
530 req._t('Newer') + '</a>')
531 },
532 paramap(this.renderFeedItem.bind(this, req), 8),
533 function (last, cb) {
534 query.lt = feedId ? last.value.sequence : last.value.timestamp - 1
535 delete query.gt
536 delete query.forwards
537 cb(null, '<a href="?' + qs.stringify(query) + '">' +
538 req._t('Older') + '</a>')
539 },
540 function (cb) {
541 if (query.forwards) {
542 delete query.gt
543 delete query.forwards
544 query.lt = opts.gt + 1
545 } else {
546 delete query.lt
547 query.gt = opts.lt - 1
548 query.forwards = 1
549 }
550 cb(null, '<a href="?' + qs.stringify(query) + '">' +
551 req._t(query.forwards ? 'Older' : 'Newer') + '</a>')
552 }
553 )
554 )
555}
556
557G.renderFeedItem = function (req, msg, cb) {
558 var self = this
559 var c = msg.value.content
560 var msgLink = u.link([msg.key],
561 new Date(msg.value.timestamp).toLocaleString(req._locale))
562 var author = msg.value.author
563 var authorLink = u.link([msg.value.author], msg.authorName)
564 switch (c.type) {
565 case 'git-repo':
566 var done = multicb({ pluck: 1, spread: true })
567 self.getRepoName(author, msg.key, done())
568 if (c.upstream) {
569 return self.getMsg(c.upstream, function (err, upstreamMsg) {
570 if (err) return cb(null, self.serveError(req, err))
571 self.getRepoName(upstreamMsg.author, c.upstream, done())
572 done(function (err, repoName, upstreamName) {
573 cb(null, '<section class="collapse">' + msgLink + '<br>' +
574 req._t('Forked', {
575 name: authorLink,
576 upstream: u.link([c.upstream], upstreamName),
577 repo: u.link([msg.key], repoName)
578 }) + '</section>')
579 })
580 })
581 } else {
582 return done(function (err, repoName) {
583 if (err) return cb(err)
584 var repoLink = u.link([msg.key], repoName)
585 cb(null, '<section class="collapse">' + msgLink + '<br>' +
586 req._t('CreatedRepo', {
587 name: authorLink,
588 repo: repoLink
589 }) + '</section>')
590 })
591 }
592 case 'git-update':
593 return self.getRepoName(author, c.repo, function (err, repoName) {
594 if (err) return cb(err)
595 var repoLink = u.link([c.repo], repoName)
596 cb(null, '<section class="collapse">' + msgLink + '<br>' +
597 req._t('Pushed', {
598 name: authorLink,
599 repo: repoLink
600 }) + '</section>')
601 })
602 case 'issue':
603 case 'pull-request':
604 var issueLink = u.link([msg.key], c.title)
605 return self.getMsg(c.project, function (err, projectMsg) {
606 if (err) return cb(null,
607 self.repos.serveRepoNotFound(req, c.repo, err))
608 self.getRepoName(projectMsg.author, c.project,
609 function (err, repoName) {
610 if (err) return cb(err)
611 var repoLink = u.link([c.project], repoName)
612 cb(null, '<section class="collapse">' + msgLink + '<br>' +
613 req._t('OpenedIssue', {
614 name: authorLink,
615 type: req._t(c.type == 'pull-request' ?
616 'pull request' : 'issue.'),
617 title: issueLink,
618 project: repoLink
619 }) + '</section>')
620 })
621 })
622 case 'about':
623 return cb(null, '<section class="collapse">' + msgLink + '<br>' +
624 req._t('Named', {
625 author: authorLink,
626 target: '<tt>' + u.escape(c.about) + '</tt>',
627 name: u.link([c.about], c.name)
628 }) + '</section>')
629 case 'post':
630 return this.pullReqs.get(c.issue, function (err, pr) {
631 if (err) return cb(err)
632 var type = pr.msg.value.content.type == 'pull-request' ?
633 'pull request' : 'issue.'
634 var changed = self.issues.isStatusChanged(msg, pr)
635 return cb(null, '<section class="collapse">' + msgLink + '<br>' +
636 req._t(changed == null ? 'CommentedOn' :
637 changed ? 'ReopenedIssue' : 'ClosedIssue', {
638 name: authorLink,
639 type: req._t(type),
640 title: u.link([pr.id], pr.title, true)
641 }) +
642 (c.text ? '<blockquote>' + markdown(c.text) + '</blockquote>' : '') +
643 '</section>')
644 })
645 default:
646 return cb(null, u.json(msg))
647 }
648}
649
650/* Index */
651
652G.serveIndex = function (req) {
653 return this.serveTemplate(req)(this.renderFeed(req))
654}
655
656/* Message */
657
658G.serveMessage = function (req, id, path) {
659 var self = this
660 return u.readNext(function (cb) {
661 self.ssb.get(id, function (err, msg) {
662 if (err) return cb(null, self.serveError(req, err))
663 var c = msg.content || {}
664 switch (c.type) {
665 case 'git-repo':
666 return self.getRepo(id, function (err, repo) {
667 if (err) return cb(null, self.serveError(req, err))
668 cb(null, self.repos.serveRepoPage(req, GitRepo(repo), path))
669 })
670 case 'git-update':
671 return self.getRepo(c.repo, function (err, repo) {
672 if (err) return cb(null,
673 self.repos.serveRepoNotFound(req, c.repo, err))
674 cb(null, self.repos.serveRepoUpdate(req,
675 GitRepo(repo), id, msg, path))
676 })
677 case 'issue':
678 return self.getRepo(c.project, function (err, repo) {
679 if (err) return cb(null,
680 self.repos.serveRepoNotFound(req, c.project, err))
681 self.issues.get(id, function (err, issue) {
682 if (err) return cb(null, self.serveError(req, err))
683 cb(null, self.repos.issues.serveRepoIssue(req,
684 GitRepo(repo), issue, path))
685 })
686 })
687 case 'pull-request':
688 return self.getRepo(c.repo, function (err, repo) {
689 if (err) return cb(null,
690 self.repos.serveRepoNotFound(req, c.project, err))
691 self.pullReqs.get(id, function (err, pr) {
692 if (err) return cb(null, self.serveError(req, err))
693 cb(null, self.repos.pulls.serveRepoPullReq(req,
694 GitRepo(repo), pr, path))
695 })
696 })
697 case 'issue-edit':
698 if (ref.isMsgId(c.issue)) {
699 return self.pullReqs.get(c.issue, function (err, issue) {
700 if (err) return cb(err)
701 self.getRepo(issue.project, function (err, repo) {
702 if (err) {
703 if (!repo) return cb(null,
704 self.repos.serveRepoNotFound(req, c.repo, err))
705 return cb(null, self.serveError(req, err))
706 }
707 cb(null, self.repos.serveIssueOrPullRequest(req, GitRepo(repo),
708 issue, path, id))
709 })
710 })
711 }
712 // fallthrough
713 case 'post':
714 if (ref.isMsgId(c.issue) && ref.isMsgId(c.repo)) {
715 // comment on an issue
716 var done = multicb({ pluck: 1, spread: true })
717 self.getRepo(c.repo, done())
718 self.pullReqs.get(c.issue, done())
719 return done(function (err, repo, issue) {
720 if (err) {
721 if (!repo) return cb(null,
722 self.repos.serveRepoNotFound(req, c.repo, err))
723 return cb(null, self.serveError(req, err))
724 }
725 cb(null, self.repos.serveIssueOrPullRequest(req, GitRepo(repo),
726 issue, path, id))
727 })
728 } else if (ref.isMsgId(c.root)) {
729 // comment on issue from patchwork?
730 return self.getMsg(c.root, function (err, root) {
731 if (err) return cb(null, self.serveError(req, err))
732 var repoId = root.content.repo || root.content.project
733 if (!ref.isMsgId(repoId))
734 return cb(null, self.serveGenericMessage(req, id, msg, path))
735 self.getRepo(repoId, function (err, repo) {
736 if (err) return cb(null, self.serveError(req, err))
737 switch (root.content && root.content.type) {
738 case 'issue':
739 return self.issues.get(c.root, function (err, issue) {
740 if (err) return cb(null, self.serveError(req, err))
741 return cb(null,
742 self.repos.issues.serveRepoIssue(req,
743 GitRepo(repo), issue, path, id))
744 })
745 case 'pull-request':
746 return self.pullReqs.get(c.root, function (err, pr) {
747 if (err) return cb(null, self.serveError(req, err))
748 return cb(null,
749 self.repos.pulls.serveRepoPullReq(req,
750 GitRepo(repo), pr, path, id))
751 })
752 }
753 })
754 })
755 }
756 // fallthrough
757 default:
758 if (ref.isMsgId(c.repo))
759 return self.getRepo(c.repo, function (err, repo) {
760 if (err) return cb(null,
761 self.repos.serveRepoNotFound(req, c.repo, err))
762 cb(null, self.repos.serveRepoSomething(req,
763 GitRepo(repo), id, msg, path))
764 })
765 else
766 return cb(null, self.serveGenericMessage(req, id, msg, path))
767 }
768 })
769 })
770}
771
772G.serveGenericMessage = function (req, id, msg, path) {
773 return this.serveTemplate(req, id)(pull.once(
774 '<section><h2>' + u.link([id]) + '</h2>' +
775 u.json(msg) +
776 '</section>'))
777}
778
779/* Search */
780
781G.serveSearch = function (req) {
782 var self = this
783 var q = String(req._u.query.q || '')
784 if (!q) return this.serveIndex(req)
785 var qId = q.replace(/^ssb:\/*/, '')
786 if (ref.type(qId))
787 return this.serveRedirect(req, encodeURIComponent(qId))
788
789 var search = new RegExp(q, 'i')
790 return this.serveTemplate(req, req._t('Search') + ' &middot; ' + q, 200)(
791 this.renderFeed(req, null, function (opts) {
792 opts.type == 'about'
793 return function (read) {
794 return pull(
795 many([
796 self.getRepoNames(opts),
797 read
798 ]),
799 pull.filter(function (msg) {
800 var c = msg.value.content
801 return (
802 search.test(msg.key) ||
803 c.text && search.test(c.text) ||
804 c.name && search.test(c.name) ||
805 c.title && search.test(c.title))
806 })
807 )
808 }
809 })
810 )
811}
812
813G.getRepoNames = function (opts) {
814 return pull(
815 this.ssb.messagesByType({
816 type: 'about',
817 reverse: opts.reverse,
818 lt: opts.lt,
819 gt: opts.gt,
820 }),
821 pull.filter(function (msg) {
822 return '%' == String(msg.value.content.about)[0]
823 && msg.value.content.name
824 })
825 )
826}
827
828G.serveBlobNotFound = function (req, repoId, err) {
829 return this.serveTemplate(req, req._t('error.BlobNotFound'), 404)(pull.once(
830 '<h2>' + req._t('error.BlobNotFound') + '</h2>' +
831 '<p>' + req._t('error.BlobNotFoundInRepo', {
832 repo: u.link([repoId])
833 }) + '</p>' +
834 '<pre>' + u.escape(err.stack) + '</pre>'
835 ))
836}
837
838G.serveRaw = function (length, contentType) {
839 var headers = {
840 'Content-Type': contentType || 'text/plain; charset=utf-8',
841 'Cache-Control': 'max-age=31536000'
842 }
843 if (length != null)
844 headers['Content-Length'] = length
845 return function (read) {
846 return cat([pull.once([200, headers]), read])
847 }
848}
849
850G.getBlob = function (req, key, cb) {
851 var blobs = this.ssb.blobs
852 blobs.want(key, function (err, got) {
853 if (err) cb(err)
854 else if (!got) cb(new Error(req._t('error.MissingBlob', {key: key})))
855 else cb(null, blobs.get(key))
856 })
857}
858
859G.serveBlob = function (req, key) {
860 var self = this
861 return u.readNext(function (cb) {
862 self.getBlob(req, key, function (err, read) {
863 if (err) cb(null, self.serveError(req, err))
864 else if (!read) cb(null, self.serve404(req))
865 else cb(null, self.serveRaw()(read))
866 })
867 })
868}
869

Built with git-ssb-web