git ssb

30+

cel / git-ssb-web



Tree: 4fea9e457824fd965a913b00273827f2032f1731

Files: 4fea9e457824fd965a913b00273827f2032f1731 / index.js

28912 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 toString: function () {
167 // hack to fit two parameters into asyncmemo
168 return ownerId + '/' + repoId
169 }
170 }, cb)
171}
172
173G.getRepoFullName = function (author, repoId, cb) {
174 var done = multicb({ pluck: 1, spread: true })
175 this.getRepoName(author, repoId, done())
176 this.about.getName(author, done())
177 done(cb)
178}
179
180G.addAuthorName = function () {
181 var about = this.about
182 return paramap(function (msg, cb) {
183 var author = msg && msg.value && msg.value.author
184 if (!author) return cb(null, msg)
185 about.getName(author, function (err, authorName) {
186 msg.authorName = authorName
187 cb(err, msg)
188 })
189 }, 8)
190}
191
192/* Serving a request */
193
194function serve(req, res) {
195 return pull(
196 pull.filter(function (data) {
197 if (Array.isArray(data)) {
198 res.writeHead.apply(res, data)
199 return false
200 }
201 return true
202 }),
203 toPull(res)
204 )
205}
206
207function G_onRequest(req, res) {
208 this.log('info', req.method, req.url)
209 req._u = url.parse(req.url, true)
210 var locale = req._u.query.locale ||
211 (/locale=([^;]*)/.exec(req.headers.cookie) || [])[1]
212 var reqLocales = req.headers['accept-language']
213 this.i18n.pickCatalog(reqLocales, locale, function (err, t) {
214 if (err) return pull(this.serveError(req, err, 500), serve(req, res))
215 req._t = t
216 req._locale = t.locale
217 pull(this.handleRequest(req), serve(req, res))
218 }.bind(this))
219}
220
221G.handleRequest = function (req) {
222 var path = req._u.pathname.slice(1)
223 var dirs = ref.isLink(path) ? [path] :
224 path.split(/\/+/).map(tryDecodeURIComponent)
225 var dir = dirs[0]
226
227 if (req.method == 'POST')
228 return this.handlePOST(req, dir)
229
230 if (dir == '')
231 return this.serveIndex(req)
232 else if (dir == 'search')
233 return this.serveSearch(req)
234 else if (ref.isBlobId(dir))
235 return this.serveBlob(req, dir)
236 else if (ref.isMsgId(dir))
237 return this.serveMessage(req, dir, dirs.slice(1))
238 else if (ref.isFeedId(dir))
239 return this.users.serveUserPage(req, dir, dirs.slice(1))
240 else if (dir == 'static')
241 return this.serveFile(req, dirs)
242 else if (dir == 'highlight')
243 return this.serveFile(req, [hlCssPath].concat(dirs.slice(1)), true)
244 else
245 return this.serve404(req)
246}
247
248G.handlePOST = function (req, dir) {
249 var self = this
250 if (self.isPublic)
251 return self.serveBuffer(405, req._t('error.POSTNotAllowed'))
252 return u.readNext(function (cb) {
253 readReqForm(req, function (err, data) {
254 if (err) return cb(null, self.serveError(req, err, 400))
255 if (!data) return cb(null, self.serveError(req,
256 new ParamError(req._t('error.MissingData')), 400))
257
258 switch (data.action) {
259 case 'fork-prompt':
260 return cb(null, self.serveRedirect(req,
261 u.encodeLink([data.id, 'fork'])))
262
263 case 'fork':
264 if (!data.id)
265 return cb(null, self.serveError(req,
266 new ParamError(req._t('error.MissingId')), 400))
267 return ssbGit.createRepo(self.ssb, {upstream: data.id},
268 function (err, repo) {
269 if (err) return cb(null, self.serveError(req, err))
270 cb(null, self.serveRedirect(req, u.encodeLink(repo.id)))
271 })
272
273 case 'vote':
274 var voteValue = +data.value || 0
275 if (!data.id)
276 return cb(null, self.serveError(req,
277 new ParamError(req._t('error.MissingId')), 400))
278 var msg = schemas.vote(data.id, voteValue)
279 return self.ssb.publish(msg, function (err) {
280 if (err) return cb(null, self.serveError(req, err))
281 cb(null, self.serveRedirect(req, req.url))
282 })
283
284 case 'repo-name':
285 if (!data.id)
286 return cb(null, self.serveError(req,
287 new ParamError(req._t('error.MissingId')), 400))
288 if (!data.name)
289 return cb(null, self.serveError(req,
290 new ParamError(req._t('error.MissingName')), 400))
291 var msg = schemas.name(data.id, data.name)
292 return self.ssb.publish(msg, function (err) {
293 if (err) return cb(null, self.serveError(req, err))
294 cb(null, self.serveRedirect(req, req.url))
295 })
296
297 case 'comment':
298 if (!data.id)
299 return cb(null, self.serveError(req,
300 new ParamError(req._t('error.MissingId')), 400))
301 var msg = schemas.post(data.text, data.id, data.branch || data.id)
302 msg.issue = data.issue
303 msg.repo = data.repo
304 if (data.open != null)
305 Issues.schemas.reopens(msg, data.id)
306 if (data.close != null)
307 Issues.schemas.closes(msg, data.id)
308 var mentions = Mentions(data.text)
309 if (mentions.length)
310 msg.mentions = mentions
311 return self.ssb.publish(msg, function (err) {
312 if (err) return cb(null, self.serveError(req, err))
313 cb(null, self.serveRedirect(req, req.url))
314 })
315
316 case 'new-issue':
317 var msg = Issues.schemas.new(dir, data.text)
318 var mentions = Mentions(data.text)
319 if (mentions.length)
320 msg.mentions = mentions
321 return self.ssb.publish(msg, function (err, msg) {
322 if (err) return cb(null, self.serveError(req, err))
323 cb(null, self.serveRedirect(req, u.encodeLink(msg.key)))
324 })
325
326 case 'new-pull':
327 var msg = PullRequests.schemas.new(dir, data.branch,
328 data.head_repo, data.head_branch, data.text)
329 var mentions = Mentions(data.text)
330 if (mentions.length)
331 msg.mentions = mentions
332 return self.ssb.publish(msg, function (err, msg) {
333 if (err) return cb(null, self.serveError(req, err))
334 cb(null, self.serveRedirect(req, u.encodeLink(msg.key)))
335 })
336
337 case 'markdown':
338 return cb(null, self.serveMarkdown(data.text, {id: data.repo}))
339
340 default:
341 cb(null, self.serveBuffer(400, req._t('error.UnknownAction', data)))
342 }
343 })
344 })
345}
346
347G.serveFile = function (req, dirs, outside) {
348 var filename = path.resolve.apply(path, [__dirname].concat(dirs))
349 // prevent escaping base dir
350 if (!outside && filename.indexOf('../') === 0)
351 return this.serveBuffer(403, req._t("error.403Forbidden"))
352
353 return u.readNext(function (cb) {
354 fs.stat(filename, function (err, stats) {
355 cb(null, err ?
356 err.code == 'ENOENT' ? this.serve404(req)
357 : this.serveBuffer(500, err.message)
358 : u.ifModifiedSince(req, 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/foundation.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=" Search" value="' + q + '" />' +
465 '</form>' +
466 '</header>' +
467 '<article><hr />'),
468 this.renderTry(read),
469 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>')
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(100),
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('Next') + '</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('Previous') + '</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 msgDate = moment(new Date(msg.value.timestamp)).fromNow()
567 var author = msg.value.author
568 var authorLink = u.link([msg.value.author], msg.authorName)
569 switch (c.type) {
570 case 'git-repo':
571 var done = multicb({ pluck: 1, spread: true })
572 self.getRepoName(author, msg.key, done())
573 if (c.upstream) {
574 return self.getMsg(c.upstream, function (err, upstreamMsg) {
575 if (err) return cb(null, self.serveError(req, err))
576 self.getRepoName(upstreamMsg.author, c.upstream, done())
577 done(function (err, repoName, upstreamName) {
578 cb(null, '<section class="collapse">' +
579 req._t('Forked', {
580 name: authorLink,
581 upstream: u.link([c.upstream], upstreamName),
582 repo: u.link([msg.key], repoName)
583 }) + ' <span class="date">' + msgDate + '</span></section>')
584 })
585 })
586 } else {
587 return done(function (err, repoName) {
588 if (err) return cb(err)
589 var repoLink = u.link([msg.key], repoName)
590 cb(null, '<section class="collapse">' +
591 req._t('CreatedRepo', {
592 name: authorLink,
593 repo: repoLink
594 }) + ' <span class="date">' + msgDate + '</span></section>')
595 })
596 }
597 case 'git-update':
598 return self.getRepoName(author, c.repo, function (err, repoName) {
599 if (err) return cb(err)
600 var repoLink = u.link([c.repo], repoName)
601 cb(null, '<section class="collapse">' +
602 req._t('Pushed', {
603 name: authorLink,
604 repo: repoLink
605 }) + ' <span class="date">' + msgDate + '</span></section>')
606 })
607 case 'issue':
608 case 'pull-request':
609 var issueLink = u.link([msg.key], u.messageTitle(msg))
610 return self.getMsg(c.project, function (err, projectMsg) {
611 if (err) return cb(null,
612 self.repos.serveRepoNotFound(req, c.repo, err))
613 self.getRepoName(projectMsg.author, c.project,
614 function (err, repoName) {
615 if (err) return cb(err)
616 var repoLink = u.link([c.project], repoName)
617 cb(null, '<section class="collapse">' +
618 req._t('OpenedIssue', {
619 name: authorLink,
620 type: req._t(c.type == 'pull-request' ?
621 'pull request' : 'issue.'),
622 title: issueLink,
623 project: repoLink
624 }) + ' <span class="date">' + msgDate + '</span></section>')
625 })
626 })
627 case 'about':
628 return cb(null, '<section class="collapse">' +
629 req._t('Named', {
630 author: authorLink,
631 target: '<tt>' + u.escape(c.about) + '</tt>',
632 name: u.link([c.about], c.name)
633 }) + '</section>')
634 case 'post':
635 return this.pullReqs.get(c.issue, function (err, pr) {
636 if (err) return cb(err)
637 var type = pr.msg.value.content.type == 'pull-request' ?
638 'pull request' : 'issue.'
639 var changed = self.issues.isStatusChanged(msg, pr)
640 return cb(null, '<section class="collapse">' +
641 req._t(changed == null ? 'CommentedOn' :
642 changed ? 'ReopenedIssue' : 'ClosedIssue', {
643 name: authorLink,
644 type: req._t(type),
645 title: u.link([pr.id], pr.title, true)
646 }) +
647 (c.text ? '<blockquote>' + markdown(c.text) + '</blockquote>' : '') +
648 '</section>')
649 })
650 default:
651 return cb(null, u.json(msg))
652 }
653}
654
655/* Index */
656
657G.serveIndex = function (req) {
658 return this.serveTemplate(req)(this.renderFeed(req))
659}
660
661/* Message */
662
663G.serveMessage = function (req, id, path) {
664 var self = this
665 return u.readNext(function (cb) {
666 self.ssb.get(id, function (err, msg) {
667 if (err) return cb(null, self.serveError(req, err))
668 var c = msg.content || {}
669 switch (c.type) {
670 case 'git-repo':
671 return self.getRepo(id, function (err, repo) {
672 if (err) return cb(null, self.serveError(req, err))
673 cb(null, self.repos.serveRepoPage(req, GitRepo(repo), path))
674 })
675 case 'git-update':
676 return self.getRepo(c.repo, function (err, repo) {
677 if (err) return cb(null,
678 self.repos.serveRepoNotFound(req, c.repo, err))
679 cb(null, self.repos.serveRepoUpdate(req,
680 GitRepo(repo), id, msg, path))
681 })
682 case 'issue':
683 return self.getRepo(c.project, function (err, repo) {
684 if (err) return cb(null,
685 self.repos.serveRepoNotFound(req, c.project, err))
686 self.issues.get(id, function (err, issue) {
687 if (err) return cb(null, self.serveError(req, err))
688 cb(null, self.repos.issues.serveRepoIssue(req,
689 GitRepo(repo), issue, path))
690 })
691 })
692 case 'pull-request':
693 return self.getRepo(c.repo, function (err, repo) {
694 if (err) return cb(null,
695 self.repos.serveRepoNotFound(req, c.project, err))
696 self.pullReqs.get(id, function (err, pr) {
697 if (err) return cb(null, self.serveError(req, err))
698 cb(null, self.repos.pulls.serveRepoPullReq(req,
699 GitRepo(repo), pr, path))
700 })
701 })
702 case 'issue-edit':
703 if (ref.isMsgId(c.issue)) {
704 return self.pullReqs.get(c.issue, function (err, issue) {
705 if (err) return cb(err)
706 self.getRepo(issue.project, function (err, repo) {
707 if (err) {
708 if (!repo) return cb(null,
709 self.repos.serveRepoNotFound(req, c.repo, err))
710 return cb(null, self.serveError(req, err))
711 }
712 cb(null, self.repos.serveIssueOrPullRequest(req, GitRepo(repo),
713 issue, path, id))
714 })
715 })
716 }
717 // fallthrough
718 case 'post':
719 if (ref.isMsgId(c.issue) && ref.isMsgId(c.repo)) {
720 // comment on an issue
721 var done = multicb({ pluck: 1, spread: true })
722 self.getRepo(c.repo, done())
723 self.pullReqs.get(c.issue, done())
724 return done(function (err, repo, issue) {
725 if (err) {
726 if (!repo) return cb(null,
727 self.repos.serveRepoNotFound(req, c.repo, err))
728 return cb(null, self.serveError(req, err))
729 }
730 cb(null, self.repos.serveIssueOrPullRequest(req, GitRepo(repo),
731 issue, path, id))
732 })
733 } else if (ref.isMsgId(c.root)) {
734 // comment on issue from patchwork?
735 return self.getMsg(c.root, function (err, root) {
736 if (err) return cb(null, self.serveError(req, err))
737 var repoId = root.content.repo || root.content.project
738 if (!ref.isMsgId(repoId))
739 return cb(null, self.serveGenericMessage(req, id, msg, path))
740 self.getRepo(repoId, function (err, repo) {
741 if (err) return cb(null, self.serveError(req, err))
742 switch (root.content && root.content.type) {
743 case 'issue':
744 return self.issues.get(c.root, function (err, issue) {
745 if (err) return cb(null, self.serveError(req, err))
746 return cb(null,
747 self.repos.issues.serveRepoIssue(req,
748 GitRepo(repo), issue, path, id))
749 })
750 case 'pull-request':
751 return self.pullReqs.get(c.root, function (err, pr) {
752 if (err) return cb(null, self.serveError(req, err))
753 return cb(null,
754 self.repos.pulls.serveRepoPullReq(req,
755 GitRepo(repo), pr, path, id))
756 })
757 }
758 })
759 })
760 }
761 // fallthrough
762 default:
763 if (ref.isMsgId(c.repo))
764 return self.getRepo(c.repo, function (err, repo) {
765 if (err) return cb(null,
766 self.repos.serveRepoNotFound(req, c.repo, err))
767 cb(null, self.repos.serveRepoSomething(req,
768 GitRepo(repo), id, msg, path))
769 })
770 else
771 return cb(null, self.serveGenericMessage(req, id, msg, path))
772 }
773 })
774 })
775}
776
777G.serveGenericMessage = function (req, id, msg, path) {
778 return this.serveTemplate(req, id)(pull.once(
779 '<section><h2>' + u.link([id]) + '</h2>' +
780 u.json(msg) +
781 '</section>'))
782}
783
784/* Search */
785
786G.serveSearch = function (req) {
787 var self = this
788 var q = String(req._u.query.q || '')
789 if (!q) return this.serveIndex(req)
790 var qId = q.replace(/^ssb:\/*/, '')
791 if (ref.type(qId))
792 return this.serveRedirect(req, encodeURIComponent(qId))
793
794 var search = new RegExp(q, 'i')
795 return this.serveTemplate(req, req._t('Search') + ' &middot; ' + q, 200)(
796 this.renderFeed(req, null, function (opts) {
797 opts.type == 'about'
798 return function (read) {
799 return pull(
800 many([
801 self.getRepoNames(opts),
802 read
803 ]),
804 pull.filter(function (msg) {
805 var c = msg.value.content
806 return (
807 search.test(msg.key) ||
808 c.text && search.test(c.text) ||
809 c.name && search.test(c.name) ||
810 c.title && search.test(c.title))
811 })
812 )
813 }
814 })
815 )
816}
817
818G.getRepoNames = function (opts) {
819 return pull(
820 this.ssb.messagesByType({
821 type: 'about',
822 reverse: opts.reverse,
823 lt: opts.lt,
824 gt: opts.gt,
825 }),
826 pull.filter(function (msg) {
827 return '%' == String(msg.value.content.about)[0]
828 && msg.value.content.name
829 })
830 )
831}
832
833G.serveBlobNotFound = function (req, repoId, err) {
834 return this.serveTemplate(req, req._t('error.BlobNotFound'), 404)(pull.once(
835 '<h2>' + req._t('error.BlobNotFound') + '</h2>' +
836 '<p>' + req._t('error.BlobNotFoundInRepo', {
837 repo: u.link([repoId])
838 }) + '</p>' +
839 '<pre>' + u.escape(err.stack) + '</pre>'
840 ))
841}
842
843G.serveRaw = function (length, contentType) {
844 var headers = {
845 'Content-Type': contentType || 'text/plain; charset=utf-8',
846 'Cache-Control': 'max-age=31536000'
847 }
848 if (length != null)
849 headers['Content-Length'] = length
850 return function (read) {
851 return cat([pull.once([200, headers]), read])
852 }
853}
854
855G.getBlob = function (req, key, cb) {
856 var blobs = this.ssb.blobs
857 blobs.want(key, function (err, got) {
858 if (err) cb(err)
859 else if (!got) cb(new Error(req._t('error.MissingBlob', {key: key})))
860 else cb(null, blobs.get(key))
861 })
862}
863
864G.serveBlob = function (req, key) {
865 var self = this
866 return u.readNext(function (cb) {
867 self.getBlob(req, key, function (err, read) {
868 if (err) cb(null, self.serveError(req, err))
869 else if (!read) cb(null, self.serve404(req))
870 else cb(null, identToResp(read))
871 })
872 })
873}
874
875function identToResp(read) {
876 var ended, type, queue
877 var id = ident(function (_type) {
878 type = _type && mime.lookup(_type)
879 })(read)
880 return function (end, cb) {
881 if (ended) return cb(ended)
882 if (end) id(end, function (end) {
883 cb(end === true ? null : end)
884 })
885 else if (queue) {
886 var _queue = queue
887 queue = null
888 cb(null, _queue)
889 }
890 else if (!type)
891 id(null, function (end, data) {
892 if (ended = end) return cb(end)
893 queue = data
894 cb(null, [200, {
895 'Content-Type': type || 'text/plain; charset=utf-8',
896 'Cache-Control': 'max-age=31536000'
897 }])
898 })
899 else
900 id(null, cb)
901 }
902}
903

Built with git-ssb-web