git ssb

30+

cel / git-ssb-web



Tree: 2ad0196e8b9972306535f04ebe1f9821a913c758

Files: 2ad0196e8b9972306535f04ebe1f9821a913c758 / index.js

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

Built with git-ssb-web