git ssb

30+

cel / git-ssb-web



Tree: 7a409b06ac2da63a7cb40bf010ed89e2954f32f3

Files: 7a409b06ac2da63a7cb40bf010ed89e2954f32f3 / index.js

28593 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 msgDateLink = u.link([msg.key], msgDate, false, 'class="date"')
564 var author = msg.value.author
565 var authorLink = u.link([msg.value.author], msg.authorName)
566 switch (c.type) {
567 case 'git-repo':
568 var done = multicb({ pluck: 1, spread: true })
569 self.getRepoName(author, msg.key, done())
570 if (c.upstream) {
571 return self.getMsg(c.upstream, function (err, upstreamMsg) {
572 if (err) return cb(null, self.serveError(req, err))
573 self.getRepoName(upstreamMsg.author, c.upstream, done())
574 done(function (err, repoName, upstreamName) {
575 cb(null, '<section class="collapse">' +
576 req._t('Forked', {
577 name: authorLink,
578 upstream: u.link([c.upstream], upstreamName),
579 repo: u.link([msg.key], repoName)
580 }) + ' ' + msgDateLink + '</section>')
581 })
582 })
583 } else {
584 return done(function (err, repoName) {
585 if (err) return cb(err)
586 var repoLink = u.link([msg.key], repoName)
587 cb(null, '<section class="collapse">' +
588 req._t('CreatedRepo', {
589 name: authorLink,
590 repo: repoLink
591 }) + ' ' + msgDateLink + '</section>')
592 })
593 }
594 case 'git-update':
595 return self.getRepoName(author, c.repo, function (err, repoName) {
596 if (err) return cb(err)
597 var repoLink = u.link([c.repo], repoName)
598 cb(null, '<section class="collapse">' +
599 req._t('Pushed', {
600 name: authorLink,
601 repo: repoLink
602 }) + ' ' + msgDateLink + '</section>')
603 })
604 case 'issue':
605 case 'pull-request':
606 var issueLink = u.link([msg.key], u.messageTitle(msg))
607 return self.getMsg(c.project, function (err, projectMsg) {
608 if (err) return cb(null,
609 self.repos.serveRepoNotFound(req, c.repo, err))
610 self.getRepoName(projectMsg.author, c.project,
611 function (err, repoName) {
612 if (err) return cb(err)
613 var repoLink = u.link([c.project], repoName)
614 cb(null, '<section class="collapse">' +
615 req._t('OpenedIssue', {
616 name: authorLink,
617 type: req._t(c.type == 'pull-request' ?
618 'pull request' : 'issue.'),
619 title: issueLink,
620 project: repoLink
621 }) + ' ' + msgDateLink + '</section>')
622 })
623 })
624 case 'about':
625 return cb(null, '<section class="collapse">' +
626 req._t('Named', {
627 author: authorLink,
628 target: '<tt>' + u.escape(c.about) + '</tt>',
629 name: u.link([c.about], c.name)
630 }) + '</section>')
631 case 'post':
632 return this.pullReqs.get(c.issue, function (err, pr) {
633 if (err) return cb(err)
634 var type = pr.msg.value.content.type == 'pull-request' ?
635 'pull request' : 'issue.'
636 var changed = self.issues.isStatusChanged(msg, pr)
637 return cb(null, '<section class="collapse">' +
638 req._t(changed == null ? 'CommentedOn' :
639 changed ? 'ReopenedIssue' : 'ClosedIssue', {
640 name: authorLink,
641 type: req._t(type),
642 title: u.link([pr.id], pr.title, true)
643 }) +
644 (c.text ? '<blockquote>' + markdown(c.text) + '</blockquote>' : '') +
645 '</section>')
646 })
647 default:
648 return cb(null, u.json(msg))
649 }
650}
651
652/* Index */
653
654G.serveIndex = function (req) {
655 return this.serveTemplate(req)(this.renderFeed(req))
656}
657
658/* Message */
659
660G.serveMessage = function (req, id, path) {
661 var self = this
662 return u.readNext(function (cb) {
663 self.ssb.get(id, function (err, msg) {
664 if (err) return cb(null, self.serveError(req, err))
665 var c = msg.content || {}
666 switch (c.type) {
667 case 'git-repo':
668 return self.getRepo(id, function (err, repo) {
669 if (err) return cb(null, self.serveError(req, err))
670 cb(null, self.repos.serveRepoPage(req, GitRepo(repo), path))
671 })
672 case 'git-update':
673 return self.getRepo(c.repo, function (err, repo) {
674 if (err) return cb(null,
675 self.repos.serveRepoNotFound(req, c.repo, err))
676 cb(null, self.repos.serveRepoUpdate(req,
677 GitRepo(repo), id, msg, path))
678 })
679 case 'issue':
680 return self.getRepo(c.project, function (err, repo) {
681 if (err) return cb(null,
682 self.repos.serveRepoNotFound(req, c.project, err))
683 self.issues.get(id, function (err, issue) {
684 if (err) return cb(null, self.serveError(req, err))
685 cb(null, self.repos.issues.serveRepoIssue(req,
686 GitRepo(repo), issue, path))
687 })
688 })
689 case 'pull-request':
690 return self.getRepo(c.repo, function (err, repo) {
691 if (err) return cb(null,
692 self.repos.serveRepoNotFound(req, c.project, err))
693 self.pullReqs.get(id, function (err, pr) {
694 if (err) return cb(null, self.serveError(req, err))
695 cb(null, self.repos.pulls.serveRepoPullReq(req,
696 GitRepo(repo), pr, path))
697 })
698 })
699 case 'issue-edit':
700 if (ref.isMsgId(c.issue)) {
701 return self.pullReqs.get(c.issue, function (err, issue) {
702 if (err) return cb(err)
703 self.getRepo(issue.project, function (err, repo) {
704 if (err) {
705 if (!repo) return cb(null,
706 self.repos.serveRepoNotFound(req, c.repo, err))
707 return cb(null, self.serveError(req, err))
708 }
709 cb(null, self.repos.serveIssueOrPullRequest(req, GitRepo(repo),
710 issue, path, id))
711 })
712 })
713 }
714 // fallthrough
715 case 'post':
716 if (ref.isMsgId(c.issue) && ref.isMsgId(c.repo)) {
717 // comment on an issue
718 var done = multicb({ pluck: 1, spread: true })
719 self.getRepo(c.repo, done())
720 self.pullReqs.get(c.issue, done())
721 return done(function (err, repo, issue) {
722 if (err) {
723 if (!repo) return cb(null,
724 self.repos.serveRepoNotFound(req, c.repo, err))
725 return cb(null, self.serveError(req, err))
726 }
727 cb(null, self.repos.serveIssueOrPullRequest(req, GitRepo(repo),
728 issue, path, id))
729 })
730 } else if (ref.isMsgId(c.root)) {
731 // comment on issue from patchwork?
732 return self.getMsg(c.root, function (err, root) {
733 if (err) return cb(null, self.serveError(req, err))
734 var repoId = root.content.repo || root.content.project
735 if (!ref.isMsgId(repoId))
736 return cb(null, self.serveGenericMessage(req, id, msg, path))
737 self.getRepo(repoId, function (err, repo) {
738 if (err) return cb(null, self.serveError(req, err))
739 switch (root.content && root.content.type) {
740 case 'issue':
741 return self.issues.get(c.root, function (err, issue) {
742 if (err) return cb(null, self.serveError(req, err))
743 return cb(null,
744 self.repos.issues.serveRepoIssue(req,
745 GitRepo(repo), issue, path, id))
746 })
747 case 'pull-request':
748 return self.pullReqs.get(c.root, function (err, pr) {
749 if (err) return cb(null, self.serveError(req, err))
750 return cb(null,
751 self.repos.pulls.serveRepoPullReq(req,
752 GitRepo(repo), pr, path, id))
753 })
754 }
755 })
756 })
757 }
758 // fallthrough
759 default:
760 if (ref.isMsgId(c.repo))
761 return self.getRepo(c.repo, function (err, repo) {
762 if (err) return cb(null,
763 self.repos.serveRepoNotFound(req, c.repo, err))
764 cb(null, self.repos.serveRepoSomething(req,
765 GitRepo(repo), id, msg, path))
766 })
767 else
768 return cb(null, self.serveGenericMessage(req, id, msg, path))
769 }
770 })
771 })
772}
773
774G.serveGenericMessage = function (req, id, msg, path) {
775 return this.serveTemplate(req, id)(pull.once(
776 '<section><h2>' + u.link([id]) + '</h2>' +
777 u.json(msg) +
778 '</section>'))
779}
780
781/* Search */
782
783G.serveSearch = function (req) {
784 var self = this
785 var q = String(req._u.query.q || '')
786 if (!q) return this.serveIndex(req)
787 var qId = q.replace(/^ssb:\/*/, '')
788 if (ref.type(qId))
789 return this.serveRedirect(req, encodeURIComponent(qId))
790
791 var search = new RegExp(q, 'i')
792 return this.serveTemplate(req, req._t('Search') + ' &middot; ' + q, 200)(
793 this.renderFeed(req, null, function (opts) {
794 return function (read) {
795 return pull(
796 many([
797 self.getMsgs('about', 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.getMsgs = function (type, opts) {
815 return this.ssb.messagesByType({
816 type: type,
817 reverse: opts.reverse,
818 lt: opts.lt,
819 gt: opts.gt,
820 })
821}
822
823G.serveBlobNotFound = function (req, repoId, err) {
824 return this.serveTemplate(req, req._t('error.BlobNotFound'), 404)(pull.once(
825 '<h2>' + req._t('error.BlobNotFound') + '</h2>' +
826 '<p>' + req._t('error.BlobNotFoundInRepo', {
827 repo: u.link([repoId])
828 }) + '</p>' +
829 '<pre>' + u.escape(err.stack) + '</pre>'
830 ))
831}
832
833G.serveRaw = function (length, contentType) {
834 var headers = {
835 'Content-Type': contentType || 'text/plain; charset=utf-8',
836 'Cache-Control': 'max-age=31536000'
837 }
838 if (length != null)
839 headers['Content-Length'] = length
840 return function (read) {
841 return cat([pull.once([200, headers]), read])
842 }
843}
844
845G.getBlob = function (req, key, cb) {
846 var blobs = this.ssb.blobs
847 blobs.want(key, function (err, got) {
848 if (err) cb(err)
849 else if (!got) cb(new Error(req._t('error.MissingBlob', {key: key})))
850 else cb(null, blobs.get(key))
851 })
852}
853
854G.serveBlob = function (req, key) {
855 var self = this
856 return u.readNext(function (cb) {
857 self.getBlob(req, key, function (err, read) {
858 if (err) cb(null, self.serveError(req, err))
859 else if (!read) cb(null, self.serve404(req))
860 else cb(null, identToResp(read))
861 })
862 })
863}
864
865function identToResp(read) {
866 var ended, type, queue
867 var id = ident(function (_type) {
868 type = _type && mime.lookup(_type)
869 })(read)
870 return function (end, cb) {
871 if (ended) return cb(ended)
872 if (end) id(end, function (end) {
873 cb(end === true ? null : end)
874 })
875 else if (queue) {
876 var _queue = queue
877 queue = null
878 cb(null, _queue)
879 }
880 else if (!type)
881 id(null, function (end, data) {
882 if (ended = end) return cb(end)
883 queue = data
884 cb(null, [200, {
885 'Content-Type': type || 'text/plain; charset=utf-8',
886 'Cache-Control': 'max-age=31536000'
887 }])
888 })
889 else
890 id(null, cb)
891 }
892}
893

Built with git-ssb-web