git ssb

30+

cel / git-ssb-web



Tree: 7f17070bd0ea7f8d0c5e6ab971e847e2a09e28ce

Files: 7f17070bd0ea7f8d0c5e6ab971e847e2a09e28ce / index.js

28810 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 || 'ssb'
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 if (read === undefined)
440 return this.serveTemplate.bind(this, req, title, code)
441 var q = req._u.query.q && u.escape(req._u.query.q) || ''
442 var app = 'git ssb'
443 var appName = this.ssbAppname
444 if (req._t) app = req._t(app)
445 return cat([
446 pull.values([
447 [code || 200, {
448 'Content-Type': 'text/html'
449 }],
450 '<!doctype html><html><head><meta charset=utf-8>',
451 '<title>' + (title || app) + '</title>',
452 '<link rel=stylesheet href="/static/styles.css"/>',
453 '<link rel=stylesheet href="/highlight/github.css"/>',
454 '</head>\n',
455 '<body>',
456 '<header><form action="/search" method="get">' +
457 '<h1><a href="/">' + app + '' +
458 (appName != 'ssb' ? ' <sub>' + appName + '</sub>' : '') +
459 '</a> ' +
460 '<input class="search-bar" name="q" size="60"' +
461 ' placeholder="🔍" value="' + q + '" />' +
462 '</h1>',
463 '</form></header>',
464 '<article>']),
465 this.renderTry(read),
466 pull.once('<hr/></article></body></html>')
467 ])
468}
469
470G.serveError = function (req, err, status) {
471 if (err.message == 'stream is closed')
472 this.reconnect && this.reconnect()
473 return pull(
474 pull.once(this.renderError(err, 'h2')),
475 this.serveTemplate(req, err.name, status || 500)
476 )
477}
478
479G.renderObjectData = function (obj, filename, repo, rev, path) {
480 var ext = u.getExtension(filename)
481 return u.readOnce(function (cb) {
482 u.readObjectString(obj, function (err, buf) {
483 buf = buf.toString('utf8')
484 if (err) return cb(err)
485 cb(null, (ext == 'md' || ext == 'markdown')
486 ? markdown(buf, {repo: repo, rev: rev, path: path})
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(20),
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('Newer') + '</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('Older') + '</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 msgLink = u.link([msg.key],
563 new Date(msg.value.timestamp).toLocaleString(req._locale))
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">' + msgLink + '<br>' +
576 req._t('Forked', {
577 name: authorLink,
578 upstream: u.link([c.upstream], upstreamName),
579 repo: u.link([msg.key], repoName)
580 }) + '</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">' + msgLink + '<br>' +
588 req._t('CreatedRepo', {
589 name: authorLink,
590 repo: repoLink
591 }) + '</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">' + msgLink + '<br>' +
599 req._t('Pushed', {
600 name: authorLink,
601 repo: repoLink
602 }) + '</section>')
603 })
604 case 'issue':
605 case 'pull-request':
606 var issueLink = u.link([msg.key], c.title)
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">' + msgLink + '<br>' +
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 }) + '</section>')
622 })
623 })
624 case 'about':
625 return cb(null, '<section class="collapse">' + msgLink + '<br>' +
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">' + msgLink + '<br>' +
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 opts.type == 'about'
795 return function (read) {
796 return pull(
797 many([
798 self.getRepoNames(opts),
799 read
800 ]),
801 pull.filter(function (msg) {
802 var c = msg.value.content
803 return (
804 search.test(msg.key) ||
805 c.text && search.test(c.text) ||
806 c.name && search.test(c.name) ||
807 c.title && search.test(c.title))
808 })
809 )
810 }
811 })
812 )
813}
814
815G.getRepoNames = function (opts) {
816 return pull(
817 this.ssb.messagesByType({
818 type: 'about',
819 reverse: opts.reverse,
820 lt: opts.lt,
821 gt: opts.gt,
822 }),
823 pull.filter(function (msg) {
824 return '%' == String(msg.value.content.about)[0]
825 && msg.value.content.name
826 })
827 )
828}
829
830G.serveBlobNotFound = function (req, repoId, err) {
831 return this.serveTemplate(req, req._t('error.BlobNotFound'), 404)(pull.once(
832 '<h2>' + req._t('error.BlobNotFound') + '</h2>' +
833 '<p>' + req._t('error.BlobNotFoundInRepo', {
834 repo: u.link([repoId])
835 }) + '</p>' +
836 '<pre>' + u.escape(err.stack) + '</pre>'
837 ))
838}
839
840G.serveRaw = function (length, contentType) {
841 var headers = {
842 'Content-Type': contentType || 'text/plain; charset=utf-8',
843 'Cache-Control': 'max-age=31536000'
844 }
845 if (length != null)
846 headers['Content-Length'] = length
847 return function (read) {
848 return cat([pull.once([200, headers]), read])
849 }
850}
851
852G.getBlob = function (req, key, cb) {
853 var blobs = this.ssb.blobs
854 blobs.want(key, function (err, got) {
855 if (err) cb(err)
856 else if (!got) cb(new Error(req._t('error.MissingBlob', {key: key})))
857 else cb(null, blobs.get(key))
858 })
859}
860
861G.serveBlob = function (req, key) {
862 var self = this
863 return u.readNext(function (cb) {
864 self.getBlob(req, key, function (err, read) {
865 if (err) cb(null, self.serveError(req, err))
866 else if (!read) cb(null, self.serve404(req))
867 else cb(null, identToResp(read))
868 })
869 })
870}
871
872function identToResp(read) {
873 var ended, type, queue
874 var id = ident(function (_type) {
875 type = _type && mime.lookup(_type)
876 })(read)
877 return function (end, cb) {
878 if (ended) return cb(ended)
879 if (end) id(end, function (end) {
880 cb(end === true ? null : end)
881 })
882 else if (queue) {
883 var _queue = queue
884 queue = null
885 cb(null, _queue)
886 }
887 else if (!type)
888 id(null, function (end, data) {
889 if (ended = end) return cb(end)
890 queue = data
891 cb(null, [200, {
892 'Content-Type': type || 'text/plain; charset=utf-8',
893 'Cache-Control': 'max-age=31536000'
894 }])
895 })
896 else
897 id(null, cb)
898 }
899}
900

Built with git-ssb-web