git ssb

30+

cel / git-ssb-web



Tree: bba0025c7c18c57a6f79f153fd86a01ba14cc7cd

Files: bba0025c7c18c57a6f79f153fd86a01ba14cc7cd / index.js

29124 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 = String(reqLocales).split(/, */).map(function (item) {
216 return item.split(';')[0]
217 })
218 if (locales)
219 req._locale = locales[0]
220
221 this.i18n.pickCatalog(reqLocales, locale, function (err, t) {
222 if (err) return pull(this.serveError(req, err, 500), serve(req, res))
223 req._t = t
224 pull(this.handleRequest(req), serve(req, res))
225 }.bind(this))
226}
227
228G.handleRequest = function (req) {
229 var path = req._u.pathname.slice(1)
230 var dirs = ref.isLink(path) ? [path] :
231 path.split(/\/+/).map(tryDecodeURIComponent)
232 var dir = dirs[0]
233
234 if (req.method == 'POST')
235 return this.handlePOST(req, dir)
236
237 if (dir == '')
238 return this.serveIndex(req)
239 else if (dir == 'search')
240 return this.serveSearch(req)
241 else if (ref.isBlobId(dir))
242 return this.serveBlob(req, dir)
243 else if (ref.isMsgId(dir))
244 return this.serveMessage(req, dir, dirs.slice(1))
245 else if (ref.isFeedId(dir))
246 return this.users.serveUserPage(req, dir, dirs.slice(1))
247 else if (dir == 'static')
248 return this.serveFile(req, dirs)
249 else if (dir == 'highlight')
250 return this.serveFile(req, [hlCssPath].concat(dirs.slice(1)), true)
251 else if (dir == 'emoji')
252 return this.serveFile(req, [emojiPath].concat(dirs.slice(1)), true)
253 else
254 return this.serve404(req)
255}
256
257G.handlePOST = function (req, dir) {
258 var self = this
259 if (self.isPublic)
260 return self.serveBuffer(405, req._t('error.POSTNotAllowed'))
261 return u.readNext(function (cb) {
262 readReqForm(req, function (err, data) {
263 if (err) return cb(null, self.serveError(req, err, 400))
264 if (!data) return cb(null, self.serveError(req,
265 new ParamError(req._t('error.MissingData')), 400))
266
267 switch (data.action) {
268 case 'fork-prompt':
269 return cb(null, self.serveRedirect(req,
270 u.encodeLink([data.id, 'fork'])))
271
272 case 'fork':
273 if (!data.id)
274 return cb(null, self.serveError(req,
275 new ParamError(req._t('error.MissingId')), 400))
276 return ssbGit.createRepo(self.ssb, {upstream: data.id},
277 function (err, repo) {
278 if (err) return cb(null, self.serveError(req, err))
279 cb(null, self.serveRedirect(req, u.encodeLink(repo.id)))
280 })
281
282 case 'vote':
283 var voteValue = +data.value || 0
284 if (!data.id)
285 return cb(null, self.serveError(req,
286 new ParamError(req._t('error.MissingId')), 400))
287 var msg = schemas.vote(data.id, voteValue)
288 return self.ssb.publish(msg, function (err) {
289 if (err) return cb(null, self.serveError(req, err))
290 cb(null, self.serveRedirect(req, req.url))
291 })
292
293 case 'repo-name':
294 if (!data.id)
295 return cb(null, self.serveError(req,
296 new ParamError(req._t('error.MissingId')), 400))
297 if (!data.name)
298 return cb(null, self.serveError(req,
299 new ParamError(req._t('error.MissingName')), 400))
300 var msg = schemas.name(data.id, data.name)
301 return self.ssb.publish(msg, function (err) {
302 if (err) return cb(null, self.serveError(req, err))
303 cb(null, self.serveRedirect(req, req.url))
304 })
305
306 case 'comment':
307 if (!data.id)
308 return cb(null, self.serveError(req,
309 new ParamError(req._t('error.MissingId')), 400))
310 var msg = schemas.post(data.text, data.id, data.branch || data.id)
311 msg.issue = data.issue
312 msg.repo = data.repo
313 if (data.open != null)
314 Issues.schemas.reopens(msg, data.id)
315 if (data.close != null)
316 Issues.schemas.closes(msg, data.id)
317 var mentions = Mentions(data.text)
318 if (mentions.length)
319 msg.mentions = mentions
320 return self.ssb.publish(msg, function (err) {
321 if (err) return cb(null, self.serveError(req, err))
322 cb(null, self.serveRedirect(req, req.url))
323 })
324
325 case 'new-issue':
326 var msg = Issues.schemas.new(dir, data.text)
327 var mentions = Mentions(data.text)
328 if (mentions.length)
329 msg.mentions = mentions
330 return self.ssb.publish(msg, function (err, msg) {
331 if (err) return cb(null, self.serveError(req, err))
332 cb(null, self.serveRedirect(req, u.encodeLink(msg.key)))
333 })
334
335 case 'new-pull':
336 var msg = PullRequests.schemas.new(dir, data.branch,
337 data.head_repo, data.head_branch, data.text)
338 var mentions = Mentions(data.text)
339 if (mentions.length)
340 msg.mentions = mentions
341 return self.ssb.publish(msg, function (err, msg) {
342 if (err) return cb(null, self.serveError(req, err))
343 cb(null, self.serveRedirect(req, u.encodeLink(msg.key)))
344 })
345
346 case 'markdown':
347 return cb(null, self.serveMarkdown(data.text, {id: data.repo}))
348
349 default:
350 cb(null, self.serveBuffer(400, req._t('error.UnknownAction', data)))
351 }
352 })
353 })
354}
355
356G.serveFile = function (req, dirs, outside) {
357 var filename = path.resolve.apply(path, [__dirname].concat(dirs))
358 // prevent escaping base dir
359 if (!outside && filename.indexOf('../') === 0)
360 return this.serveBuffer(403, req._t("error.403Forbidden"))
361
362 return u.readNext(function (cb) {
363 fs.stat(filename, function (err, stats) {
364 cb(null, err ?
365 err.code == 'ENOENT' ? this.serve404(req)
366 : this.serveBuffer(500, err.message)
367 : u.ifModifiedSince(req, stats.mtime) ?
368 pull.once([304])
369 : stats.isDirectory() ?
370 this.serveBuffer(403, req._t('error.DirectoryNotListable'))
371 : cat([
372 pull.once([200, {
373 'Content-Type': getContentType(filename),
374 'Content-Length': stats.size,
375 'Last-Modified': stats.mtime.toGMTString()
376 }]),
377 toPull(fs.createReadStream(filename))
378 ]))
379 }.bind(this))
380 }.bind(this))
381}
382
383G.serveBuffer = function (code, buf, contentType, headers) {
384 headers = headers || {}
385 headers['Content-Type'] = contentType || 'text/plain; charset=utf-8'
386 headers['Content-Length'] = Buffer.byteLength(buf)
387 return pull.values([
388 [code, headers],
389 buf
390 ])
391}
392
393G.serve404 = function (req) {
394 return this.serveBuffer(404, req._t("error.404NotFound"))
395}
396
397G.serveRedirect = function (req, path) {
398 return this.serveBuffer(302,
399 '<!doctype><html><head>' +
400 '<title>' + req._t('Redirect') + '</title></head><body>' +
401 '<p><a href="' + u.escape(path) + '">' +
402 req._t('Continue') + '</a></p>' +
403 '</body></html>', 'text/html; charset=utf-8', {Location: path})
404}
405
406G.serveMarkdown = function (text, repo) {
407 return this.serveBuffer(200, markdown(text, repo),
408 'text/html; charset=utf-8')
409}
410
411G.renderError = function (err, tag) {
412 tag = tag || 'h3'
413 return '<' + tag + '>' + err.name + '</' + tag + '>' +
414 '<pre>' + u.escape(err.stack) + '</pre>'
415}
416
417G.renderTry = function (read) {
418 var self = this
419 var ended
420 return function (end, cb) {
421 if (ended) return cb(ended)
422 read(end, function (err, data) {
423 if (err === true)
424 cb(true)
425 else if (err) {
426 ended = true
427 cb(null, self.renderError(err))
428 } else
429 cb(null, data)
430 })
431 }
432}
433
434G.serveTemplate = function (req, title, code, read) {
435 var self = this
436 if (read === undefined)
437 return this.serveTemplate.bind(this, req, title, code)
438 var q = req._u.query.q && u.escape(req._u.query.q) || ''
439 var app = 'git ssb'
440 var appName = this.ssbAppname
441 if (req._t) app = req._t(app)
442 return cat([
443 pull.values([
444 [code || 200, {
445 'Content-Type': 'text/html'
446 }],
447 '<!doctype html><html><head><meta charset=utf-8>',
448 '<title>' + app + (title != undefined ? ' - ' + title : '') + '</title>',
449 '<link rel=stylesheet href="/static/styles.css"/>',
450 '<link rel=stylesheet href="/highlight/foundation.css"/>',
451 '</head>\n',
452 '<body>',
453 '<header>'
454 ]),
455 self.isPublic ? null : u.readOnce(function (cb) {
456 self.about(self.myId, function (err, about) {
457 if (err) return cb(err)
458 cb(null,
459 '<a href="' + u.encodeLink(self.myId) + '">' +
460 (about.image ?
461 '<img class="profile-icon icon-right"' +
462 ' src="/' + encodeURIComponent(about.image) + '"' +
463 ' alt="' + u.escape(about.name) + '">' : u.escape(about.name)) +
464 '</a>')
465 })
466 }),
467 pull.once(
468 '<form action="/search" method="get">' +
469 '<h1><a href="/">' + app +
470 (appName == 'ssb' ? '' : ' <sub>' + appName + '</sub>') +
471 '</a></h1> ' +
472 '<input class="search-bar" name="q" size="60"' +
473 ' placeholder=" Search" value="' + q + '" />' +
474 '</form>' +
475 '</header>' +
476 '<article><hr />'),
477 this.renderTry(read),
478 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>')
479 ])
480}
481
482G.serveError = function (req, err, status) {
483 if (err.message == 'stream is closed')
484 this.reconnect && this.reconnect()
485 return pull(
486 pull.once(this.renderError(err, 'h2')),
487 this.serveTemplate(req, err.name, status || 500)
488 )
489}
490
491G.renderObjectData = function (obj, filename, repo, rev, path) {
492 var ext = u.getExtension(filename)
493 return u.readOnce(function (cb) {
494 u.readObjectString(obj, function (err, buf) {
495 buf = buf.toString('utf8')
496 if (err) return cb(err)
497 cb(null, (ext == 'md' || ext == 'markdown')
498 ? markdown(buf, {repo: repo, rev: rev, path: path})
499 : buf.length > 1000000 ? ''
500 : renderCodeTable(buf, ext))
501 })
502 })
503}
504
505function renderCodeTable(buf, ext) {
506 return '<pre><table class="code">' +
507 u.highlight(buf, ext).split('\n').map(function (line, i) {
508 i++
509 return '<tr id="L' + i + '">' +
510 '<td class="code-linenum">' + '<a href="#L' + i + '">' + i + '</td>' +
511 '<td class="code-text">' + line + '</td></tr>'
512 }).join('') +
513 '</table></pre>'
514}
515
516/* Feed */
517
518G.renderFeed = function (req, feedId, filter) {
519 var query = req._u.query
520 var opts = {
521 reverse: !query.forwards,
522 lt: query.lt && +query.lt || Date.now(),
523 gt: query.gt ? +query.gt : -Infinity,
524 id: feedId
525 }
526 return pull(
527 feedId ? this.ssb.createUserStream(opts) : this.ssb.createFeedStream(opts),
528 pull.filter(function (msg) {
529 var c = msg.value.content
530 return c.type in msgTypes
531 || (c.type == 'post' && c.repo && c.issue)
532 }),
533 typeof filter == 'function' ? filter(opts) : filter,
534 pull.take(100),
535 this.addAuthorName(),
536 query.forwards && u.pullReverse(),
537 paginate(
538 function (first, cb) {
539 if (!query.lt && !query.gt) return cb(null, '')
540 var gt = feedId ? first.value.sequence : first.value.timestamp + 1
541 query.gt = gt
542 query.forwards = 1
543 delete query.lt
544 cb(null, '<a href="?' + qs.stringify(query) + '">' +
545 req._t('Next') + '</a>')
546 },
547 paramap(this.renderFeedItem.bind(this, req), 8),
548 function (last, cb) {
549 query.lt = feedId ? last.value.sequence : last.value.timestamp - 1
550 delete query.gt
551 delete query.forwards
552 cb(null, '<a href="?' + qs.stringify(query) + '">' +
553 req._t('Previous') + '</a>')
554 },
555 function (cb) {
556 if (query.forwards) {
557 delete query.gt
558 delete query.forwards
559 query.lt = opts.gt + 1
560 } else {
561 delete query.lt
562 query.gt = opts.lt - 1
563 query.forwards = 1
564 }
565 cb(null, '<a href="?' + qs.stringify(query) + '">' +
566 req._t(query.forwards ? 'Older' : 'Newer') + '</a>')
567 }
568 )
569 )
570}
571
572G.renderFeedItem = function (req, msg, cb) {
573 var self = this
574 var c = msg.value.content
575 var msgDate = moment(new Date(msg.value.timestamp)).fromNow()
576 var msgDateLink = u.link([msg.key], msgDate, false, 'class="date"')
577 var author = msg.value.author
578 var authorLink = u.link([msg.value.author], msg.authorName)
579 switch (c.type) {
580 case 'git-repo':
581 var done = multicb({ pluck: 1, spread: true })
582 self.getRepoName(author, msg.key, done())
583 if (c.upstream) {
584 return self.getMsg(c.upstream, function (err, upstreamMsg) {
585 if (err) return cb(null, self.serveError(req, err))
586 self.getRepoName(upstreamMsg.author, c.upstream, done())
587 done(function (err, repoName, upstreamName) {
588 cb(null, '<section class="collapse">' +
589 req._t('Forked', {
590 name: authorLink,
591 upstream: u.link([c.upstream], upstreamName),
592 repo: u.link([msg.key], repoName)
593 }) + ' ' + msgDateLink + '</section>')
594 })
595 })
596 } else {
597 return done(function (err, repoName) {
598 if (err) return cb(err)
599 var repoLink = u.link([msg.key], repoName)
600 cb(null, '<section class="collapse">' +
601 req._t('CreatedRepo', {
602 name: authorLink,
603 repo: repoLink
604 }) + ' ' + msgDateLink + '</section>')
605 })
606 }
607 case 'git-update':
608 return self.getRepoName(author, c.repo, function (err, repoName) {
609 if (err) return cb(err)
610 var repoLink = u.link([c.repo], repoName)
611 cb(null, '<section class="collapse">' +
612 req._t('Pushed', {
613 name: authorLink,
614 repo: repoLink
615 }) + ' ' + msgDateLink + '</section>')
616 })
617 case 'issue':
618 case 'pull-request':
619 var issueLink = u.link([msg.key], u.messageTitle(msg))
620 return self.getMsg(c.project, function (err, projectMsg) {
621 if (err) return cb(null,
622 self.repos.serveRepoNotFound(req, c.repo, err))
623 self.getRepoName(projectMsg.author, c.project,
624 function (err, repoName) {
625 if (err) return cb(err)
626 var repoLink = u.link([c.project], repoName)
627 cb(null, '<section class="collapse">' +
628 req._t('OpenedIssue', {
629 name: authorLink,
630 type: req._t(c.type == 'pull-request' ?
631 'pull request' : 'issue.'),
632 title: issueLink,
633 project: repoLink
634 }) + ' ' + msgDateLink + '</section>')
635 })
636 })
637 case 'about':
638 return cb(null, '<section class="collapse">' +
639 req._t('Named', {
640 author: authorLink,
641 target: '<tt>' + u.escape(c.about) + '</tt>',
642 name: u.link([c.about], c.name)
643 }) + ' ' + msgDateLink + '</section>')
644 case 'post':
645 return this.pullReqs.get(c.issue, function (err, pr) {
646 if (err) return cb(err)
647 var type = pr.msg.value.content.type == 'pull-request' ?
648 'pull request' : 'issue.'
649 var changed = self.issues.isStatusChanged(msg, pr)
650 return cb(null, '<section class="collapse">' +
651 req._t(changed == null ? 'CommentedOn' :
652 changed ? 'ReopenedIssue' : 'ClosedIssue', {
653 name: authorLink,
654 type: req._t(type),
655 title: u.link([pr.id], pr.title, true)
656 }) + ' ' + msgDateLink +
657 (c.text ? '<blockquote>' + markdown(c.text) + '</blockquote>' : '') +
658 '</section>')
659 })
660 default:
661 return cb(null, u.json(msg))
662 }
663}
664
665/* Index */
666
667G.serveIndex = function (req) {
668 return this.serveTemplate(req)(this.renderFeed(req))
669}
670
671/* Message */
672
673G.serveMessage = function (req, id, path) {
674 var self = this
675 return u.readNext(function (cb) {
676 self.ssb.get(id, function (err, msg) {
677 if (err) return cb(null, self.serveError(req, err))
678 var c = msg.content || {}
679 switch (c.type) {
680 case 'git-repo':
681 return self.getRepo(id, function (err, repo) {
682 if (err) return cb(null, self.serveError(req, err))
683 cb(null, self.repos.serveRepoPage(req, GitRepo(repo), path))
684 })
685 case 'git-update':
686 return self.getRepo(c.repo, function (err, repo) {
687 if (err) return cb(null,
688 self.repos.serveRepoNotFound(req, c.repo, err))
689 cb(null, self.repos.serveRepoUpdate(req,
690 GitRepo(repo), id, msg, path))
691 })
692 case 'issue':
693 return self.getRepo(c.project, function (err, repo) {
694 if (err) return cb(null,
695 self.repos.serveRepoNotFound(req, c.project, err))
696 self.issues.get(id, function (err, issue) {
697 if (err) return cb(null, self.serveError(req, err))
698 cb(null, self.repos.issues.serveRepoIssue(req,
699 GitRepo(repo), issue, path))
700 })
701 })
702 case 'pull-request':
703 return self.getRepo(c.repo, function (err, repo) {
704 if (err) return cb(null,
705 self.repos.serveRepoNotFound(req, c.project, err))
706 self.pullReqs.get(id, function (err, pr) {
707 if (err) return cb(null, self.serveError(req, err))
708 cb(null, self.repos.pulls.serveRepoPullReq(req,
709 GitRepo(repo), pr, path))
710 })
711 })
712 case 'issue-edit':
713 if (ref.isMsgId(c.issue)) {
714 return self.pullReqs.get(c.issue, function (err, issue) {
715 if (err) return cb(err)
716 self.getRepo(issue.project, function (err, repo) {
717 if (err) {
718 if (!repo) return cb(null,
719 self.repos.serveRepoNotFound(req, c.repo, err))
720 return cb(null, self.serveError(req, err))
721 }
722 cb(null, self.repos.serveIssueOrPullRequest(req, GitRepo(repo),
723 issue, path, id))
724 })
725 })
726 }
727 // fallthrough
728 case 'post':
729 if (ref.isMsgId(c.issue) && ref.isMsgId(c.repo)) {
730 // comment on an issue
731 var done = multicb({ pluck: 1, spread: true })
732 self.getRepo(c.repo, done())
733 self.pullReqs.get(c.issue, done())
734 return done(function (err, repo, issue) {
735 if (err) {
736 if (!repo) return cb(null,
737 self.repos.serveRepoNotFound(req, c.repo, err))
738 return cb(null, self.serveError(req, err))
739 }
740 cb(null, self.repos.serveIssueOrPullRequest(req, GitRepo(repo),
741 issue, path, id))
742 })
743 } else if (ref.isMsgId(c.root)) {
744 // comment on issue from patchwork?
745 return self.getMsg(c.root, function (err, root) {
746 if (err) return cb(null, self.serveError(req, err))
747 var repoId = root.content.repo || root.content.project
748 if (!ref.isMsgId(repoId))
749 return cb(null, self.serveGenericMessage(req, id, msg, path))
750 self.getRepo(repoId, function (err, repo) {
751 if (err) return cb(null, self.serveError(req, err))
752 switch (root.content && root.content.type) {
753 case 'issue':
754 return self.issues.get(c.root, function (err, issue) {
755 if (err) return cb(null, self.serveError(req, err))
756 return cb(null,
757 self.repos.issues.serveRepoIssue(req,
758 GitRepo(repo), issue, path, id))
759 })
760 case 'pull-request':
761 return self.pullReqs.get(c.root, function (err, pr) {
762 if (err) return cb(null, self.serveError(req, err))
763 return cb(null,
764 self.repos.pulls.serveRepoPullReq(req,
765 GitRepo(repo), pr, path, id))
766 })
767 }
768 })
769 })
770 }
771 // fallthrough
772 default:
773 if (ref.isMsgId(c.repo))
774 return self.getRepo(c.repo, function (err, repo) {
775 if (err) return cb(null,
776 self.repos.serveRepoNotFound(req, c.repo, err))
777 cb(null, self.repos.serveRepoSomething(req,
778 GitRepo(repo), id, msg, path))
779 })
780 else
781 return cb(null, self.serveGenericMessage(req, id, msg, path))
782 }
783 })
784 })
785}
786
787G.serveGenericMessage = function (req, id, msg, path) {
788 return this.serveTemplate(req, id)(pull.once(
789 '<section><h2>' + u.link([id]) + '</h2>' +
790 u.json(msg) +
791 '</section>'))
792}
793
794/* Search */
795
796G.serveSearch = function (req) {
797 var self = this
798 var q = String(req._u.query.q || '')
799 if (!q) return this.serveIndex(req)
800 var qId = q.replace(/^ssb:\/*/, '')
801 if (ref.type(qId))
802 return this.serveRedirect(req, encodeURIComponent(qId))
803
804 var search = new RegExp(q, 'i')
805 return this.serveTemplate(req, req._t('Search') + ' &middot; ' + q, 200)(
806 this.renderFeed(req, null, function (opts) {
807 return function (read) {
808 return pull(
809 many([
810 self.getMsgs('about', opts),
811 read
812 ]),
813 pull.filter(function (msg) {
814 var c = msg.value.content
815 return (
816 search.test(msg.key) ||
817 c.text && search.test(c.text) ||
818 c.name && search.test(c.name) ||
819 c.title && search.test(c.title))
820 })
821 )
822 }
823 })
824 )
825}
826
827G.getMsgs = function (type, opts) {
828 return this.ssb.messagesByType({
829 type: type,
830 reverse: opts.reverse,
831 lt: opts.lt,
832 gt: opts.gt,
833 })
834}
835
836G.serveBlobNotFound = function (req, repoId, err) {
837 return this.serveTemplate(req, req._t('error.BlobNotFound'), 404)(pull.once(
838 '<h2>' + req._t('error.BlobNotFound') + '</h2>' +
839 '<p>' + req._t('error.BlobNotFoundInRepo', {
840 repo: u.link([repoId])
841 }) + '</p>' +
842 '<pre>' + u.escape(err.stack) + '</pre>'
843 ))
844}
845
846G.serveRaw = function (length, contentType) {
847 var headers = {
848 'Content-Type': contentType || 'text/plain; charset=utf-8',
849 'Cache-Control': 'max-age=31536000'
850 }
851 if (length != null)
852 headers['Content-Length'] = length
853 return function (read) {
854 return cat([pull.once([200, headers]), read])
855 }
856}
857
858G.getBlob = function (req, key, cb) {
859 var blobs = this.ssb.blobs
860 blobs.want(key, function (err, got) {
861 if (err) cb(err)
862 else if (!got) cb(new Error(req._t('error.MissingBlob', {key: key})))
863 else cb(null, blobs.get(key))
864 })
865}
866
867G.serveBlob = function (req, key) {
868 var self = this
869 return u.readNext(function (cb) {
870 self.getBlob(req, key, function (err, read) {
871 if (err) cb(null, self.serveError(req, err))
872 else if (!read) cb(null, self.serve404(req))
873 else cb(null, identToResp(read))
874 })
875 })
876}
877
878function identToResp(read) {
879 var ended, type, queue
880 var id = ident(function (_type) {
881 type = _type && mime.lookup(_type)
882 })(read)
883 return function (end, cb) {
884 if (ended) return cb(ended)
885 if (end) id(end, function (end) {
886 cb(end === true ? null : end)
887 })
888 else if (queue) {
889 var _queue = queue
890 queue = null
891 cb(null, _queue)
892 }
893 else if (!type)
894 id(null, function (end, data) {
895 if (ended = end) return cb(end)
896 queue = data
897 cb(null, [200, {
898 'Content-Type': type || 'text/plain; charset=utf-8',
899 'Cache-Control': 'max-age=31536000'
900 }])
901 })
902 else
903 id(null, cb)
904 }
905}
906

Built with git-ssb-web