git ssb

30+

cel / git-ssb-web



Tree: d99881bd82a608d2e7dda4022d97fe832443785a

Files: d99881bd82a608d2e7dda4022d97fe832443785a / index.js

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

Built with git-ssb-web