git ssb

30+

cel / git-ssb-web



Tree: 8acc1d1aa35122646294a41cd1ec6fa54f18baad

Files: 8acc1d1aa35122646294a41cd1ec6fa54f18baad / index.js

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

Built with git-ssb-web