git ssb

30+

cel / git-ssb-web



Tree: 88de0177402d7aacfb3a27acbb67242d60c3cc39

Files: 88de0177402d7aacfb3a27acbb67242d60c3cc39 / index.js

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

Built with git-ssb-web