git ssb

30+

cel / git-ssb-web



Tree: 35c0fcc7981a4935c49b803de8331006d636db15

Files: 35c0fcc7981a4935c49b803de8331006d636db15 / index.js

28980 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 : 'if-modified-since' in req.headers &&
359 new Date(req.headers['if-modified-since']) >= stats.mtime ?
360 pull.once([304])
361 : stats.isDirectory() ?
362 this.serveBuffer(403, req._t('error.DirectoryNotListable'))
363 : cat([
364 pull.once([200, {
365 'Content-Type': getContentType(filename),
366 'Content-Length': stats.size,
367 'Last-Modified': stats.mtime.toGMTString()
368 }]),
369 toPull(fs.createReadStream(filename))
370 ]))
371 }.bind(this))
372 }.bind(this))
373}
374
375G.serveBuffer = function (code, buf, contentType, headers) {
376 headers = headers || {}
377 headers['Content-Type'] = contentType || 'text/plain; charset=utf-8'
378 headers['Content-Length'] = Buffer.byteLength(buf)
379 return pull.values([
380 [code, headers],
381 buf
382 ])
383}
384
385G.serve404 = function (req) {
386 return this.serveBuffer(404, req._t("error.404NotFound"))
387}
388
389G.serveRedirect = function (req, path) {
390 return this.serveBuffer(302,
391 '<!doctype><html><head>' +
392 '<title>' + req._t('Redirect') + '</title></head><body>' +
393 '<p><a href="' + u.escape(path) + '">' +
394 req._t('Continue') + '</a></p>' +
395 '</body></html>', 'text/html; charset=utf-8', {Location: path})
396}
397
398G.serveMarkdown = function (text, repo) {
399 return this.serveBuffer(200, markdown(text, repo),
400 'text/html; charset=utf-8')
401}
402
403G.renderError = function (err, tag) {
404 tag = tag || 'h3'
405 return '<' + tag + '>' + err.name + '</' + tag + '>' +
406 '<pre>' + u.escape(err.stack) + '</pre>'
407}
408
409G.renderTry = function (read) {
410 var self = this
411 var ended
412 return function (end, cb) {
413 if (ended) return cb(ended)
414 read(end, function (err, data) {
415 if (err === true)
416 cb(true)
417 else if (err) {
418 ended = true
419 cb(null, self.renderError(err))
420 } else
421 cb(null, data)
422 })
423 }
424}
425
426G.serveTemplate = function (req, title, code, read) {
427 var self = this
428 if (read === undefined)
429 return this.serveTemplate.bind(this, req, title, code)
430 var q = req._u.query.q && u.escape(req._u.query.q) || ''
431 var app = 'git ssb'
432 var appName = this.ssbAppname
433 if (req._t) app = req._t(app)
434 return cat([
435 pull.values([
436 [code || 200, {
437 'Content-Type': 'text/html'
438 }],
439 '<!doctype html><html><head><meta charset=utf-8>',
440 '<title>' + (title || app) + '</title>',
441 '<link rel=stylesheet href="/static/styles.css"/>',
442 '<link rel=stylesheet href="/highlight/foundation.css"/>',
443 '</head>\n',
444 '<body>',
445 '<header>'
446 ]),
447 self.isPublic ? null : u.readOnce(function (cb) {
448 self.about(self.myId, function (err, about) {
449 if (err) return cb(err)
450 cb(null,
451 '<a href="' + u.encodeLink(this.myId) + '">' +
452 (about.image ?
453 '<img class="profile-icon icon-right"' +
454 ' src="/' + encodeURIComponent(about.image) + '"' +
455 ' alt="' + u.escape(about.name) + '">' : u.escape(about.name)) +
456 '</a>')
457 })
458 }),
459 pull.once(
460 '<form action="/search" method="get">' +
461 '<h1><a href="/">' + app +
462 (appName == 'ssb' ? '' : ' <sub>' + appName + '</sub>') +
463 '</a></h1> ' +
464 '<input class="search-bar" name="q" size="60"' +
465 ' placeholder=" Search" value="' + q + '" />' +
466 '</form>' +
467 '</header>' +
468 '<article><hr />'),
469 this.renderTry(read),
470 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>')
471 ])
472}
473
474G.serveError = function (req, err, status) {
475 if (err.message == 'stream is closed')
476 this.reconnect && this.reconnect()
477 return pull(
478 pull.once(this.renderError(err, 'h2')),
479 this.serveTemplate(req, err.name, status || 500)
480 )
481}
482
483G.renderObjectData = function (obj, filename, repo, rev, path) {
484 var ext = u.getExtension(filename)
485 return u.readOnce(function (cb) {
486 u.readObjectString(obj, function (err, buf) {
487 buf = buf.toString('utf8')
488 if (err) return cb(err)
489 cb(null, (ext == 'md' || ext == 'markdown')
490 ? markdown(buf, {repo: repo, rev: rev, path: path})
491 : buf.length > 1000000 ? ''
492 : renderCodeTable(buf, ext))
493 })
494 })
495}
496
497function renderCodeTable(buf, ext) {
498 return '<pre><table class="code">' +
499 u.highlight(buf, ext).split('\n').map(function (line, i) {
500 i++
501 return '<tr id="L' + i + '">' +
502 '<td class="code-linenum">' + '<a href="#L' + i + '">' + i + '</td>' +
503 '<td class="code-text">' + line + '</td></tr>'
504 }).join('') +
505 '</table></pre>'
506}
507
508/* Feed */
509
510G.renderFeed = function (req, feedId, filter) {
511 var query = req._u.query
512 var opts = {
513 reverse: !query.forwards,
514 lt: query.lt && +query.lt || Date.now(),
515 gt: query.gt ? +query.gt : -Infinity,
516 id: feedId
517 }
518 return pull(
519 feedId ? this.ssb.createUserStream(opts) : this.ssb.createFeedStream(opts),
520 pull.filter(function (msg) {
521 var c = msg.value.content
522 return c.type in msgTypes
523 || (c.type == 'post' && c.repo && c.issue)
524 }),
525 typeof filter == 'function' ? filter(opts) : filter,
526 pull.take(100),
527 this.addAuthorName(),
528 query.forwards && u.pullReverse(),
529 paginate(
530 function (first, cb) {
531 if (!query.lt && !query.gt) return cb(null, '')
532 var gt = feedId ? first.value.sequence : first.value.timestamp + 1
533 query.gt = gt
534 query.forwards = 1
535 delete query.lt
536 cb(null, '<a href="?' + qs.stringify(query) + '">' +
537 req._t('Next') + '</a>')
538 },
539 paramap(this.renderFeedItem.bind(this, req), 8),
540 function (last, cb) {
541 query.lt = feedId ? last.value.sequence : last.value.timestamp - 1
542 delete query.gt
543 delete query.forwards
544 cb(null, '<a href="?' + qs.stringify(query) + '">' +
545 req._t('Previous') + '</a>')
546 },
547 function (cb) {
548 if (query.forwards) {
549 delete query.gt
550 delete query.forwards
551 query.lt = opts.gt + 1
552 } else {
553 delete query.lt
554 query.gt = opts.lt - 1
555 query.forwards = 1
556 }
557 cb(null, '<a href="?' + qs.stringify(query) + '">' +
558 req._t(query.forwards ? 'Older' : 'Newer') + '</a>')
559 }
560 )
561 )
562}
563
564G.renderFeedItem = function (req, msg, cb) {
565 var self = this
566 var c = msg.value.content
567 var msgDate = moment(new Date(msg.value.timestamp)).fromNow()
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">' +
580 req._t('Forked', {
581 name: authorLink,
582 upstream: u.link([c.upstream], upstreamName),
583 repo: u.link([msg.key], repoName)
584 }) + ' <span class="date">' + msgDate + '</span></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">' +
592 req._t('CreatedRepo', {
593 name: authorLink,
594 repo: repoLink
595 }) + ' <span class="date">' + msgDate + '</span></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">' +
603 req._t('Pushed', {
604 name: authorLink,
605 repo: repoLink
606 }) + ' <span class="date">' + msgDate + '</span></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">' +
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 }) + ' <span class="date">' + msgDate + '</span></section>')
626 })
627 })
628 case 'about':
629 return cb(null, '<section class="collapse">' +
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">' +
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