git ssb

30+

cel / git-ssb-web



Tree: 8df9a55e12e25a2e55af110f3c01603e65aa3c47

Files: 8df9a55e12e25a2e55af110f3c01603e65aa3c47 / index.js

29764 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)}, this.getMsgRaw)
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, 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 u.decryptMessages(this.ssb),
528 u.readableMessages(),
529 pull.filter(function (msg) {
530 var c = msg.value.content
531 return c.type in msgTypes
532 || (c.type == 'post' && c.repo && c.issue)
533 }),
534 typeof filter == 'function' ? filter(opts) : filter,
535 pull.take(100),
536 this.addAuthorName(),
537 query.forwards && u.pullReverse(),
538 paginate(
539 function (first, cb) {
540 if (!query.lt && !query.gt) return cb(null, '')
541 var gt = feedId ? first.value.sequence : first.value.timestamp + 1
542 query.gt = gt
543 query.forwards = 1
544 delete query.lt
545 cb(null, '<a href="?' + qs.stringify(query) + '">' +
546 req._t('Next') + '</a>')
547 },
548 paramap(this.renderFeedItem.bind(this, req), 8),
549 function (last, cb) {
550 query.lt = feedId ? last.value.sequence : last.value.timestamp - 1
551 delete query.gt
552 delete query.forwards
553 cb(null, '<a href="?' + qs.stringify(query) + '">' +
554 req._t('Previous') + '</a>')
555 },
556 function (cb) {
557 if (query.forwards) {
558 delete query.gt
559 delete query.forwards
560 query.lt = opts.gt + 1
561 } else {
562 delete query.lt
563 query.gt = opts.lt - 1
564 query.forwards = 1
565 }
566 cb(null, '<a href="?' + qs.stringify(query) + '">' +
567 req._t(query.forwards ? 'Older' : 'Newer') + '</a>')
568 }
569 )
570 )
571}
572
573G.renderFeedItem = function (req, msg, cb) {
574 var self = this
575 var c = msg.value.content
576 var msgDate = moment(new Date(msg.value.timestamp)).fromNow()
577 var msgDateLink = u.link([msg.key], msgDate, false, 'class="date"')
578 var author = msg.value.author
579 var authorLink = u.link([msg.value.author], msg.authorName)
580 switch (c.type) {
581 case 'git-repo':
582 var done = multicb({ pluck: 1, spread: true })
583 self.getRepoName(author, msg.key, done())
584 if (c.upstream) {
585 return self.getMsg(c.upstream, function (err, upstreamMsg) {
586 if (err) return cb(null, self.serveError(req, err))
587 self.getRepoName(upstreamMsg.value.author, c.upstream, done())
588 done(function (err, repoName, upstreamName) {
589 cb(null, '<section class="collapse">' +
590 req._t('Forked', {
591 name: authorLink,
592 upstream: u.link([c.upstream], upstreamName),
593 repo: u.link([msg.key], repoName)
594 }) + ' ' + msgDateLink + '</section>')
595 })
596 })
597 } else {
598 return done(function (err, repoName) {
599 if (err) return cb(err)
600 var repoLink = u.link([msg.key], repoName)
601 cb(null, '<section class="collapse">' +
602 req._t('CreatedRepo', {
603 name: authorLink,
604 repo: repoLink
605 }) + ' ' + msgDateLink +
606 (msg.value.private ?
607 '<br>' + req._t('repo.Recipients') + '<ul>' +
608 (Array.isArray(c.recps) ? c.recps : []).map(function (feed) {
609 return '<li>' + u.link([feed], feed) + '</li>'
610 }).join('') + '</ul>'
611 : '') +
612 '</section>')
613 })
614 }
615 case 'git-update':
616 return self.getRepoName(author, c.repo, function (err, repoName) {
617 if (err) return cb(err)
618 var repoLink = u.link([c.repo], repoName)
619 cb(null, '<section class="collapse">' +
620 req._t('Pushed', {
621 name: authorLink,
622 repo: repoLink
623 }) + ' ' + msgDateLink + '</section>')
624 })
625 case 'issue':
626 case 'pull-request':
627 var issueLink = u.link([msg.key], u.messageTitle(msg))
628 return self.getMsg(c.project, function (err, projectMsg) {
629 if (err) return cb(null,
630 self.repos.serveRepoNotFound(req, c.repo, err))
631 self.getRepoName(projectMsg.value.author, c.project,
632 function (err, repoName) {
633 if (err) return cb(err)
634 var repoLink = u.link([c.project], repoName)
635 cb(null, '<section class="collapse">' +
636 req._t('OpenedIssue', {
637 name: authorLink,
638 type: req._t(c.type == 'pull-request' ?
639 'pull request' : 'issue.'),
640 title: issueLink,
641 project: repoLink
642 }) + ' ' + msgDateLink + '</section>')
643 })
644 })
645 case 'about':
646 return cb(null, '<section class="collapse">' +
647 req._t('Named', {
648 author: authorLink,
649 target: '<tt>' + u.escape(c.about) + '</tt>',
650 name: u.link([c.about], c.name)
651 }) + ' ' + msgDateLink + '</section>')
652 case 'post':
653 return this.pullReqs.get(c.issue, function (err, pr) {
654 if (err) return cb(err)
655 var type = pr.msg.value.content.type == 'pull-request' ?
656 'pull request' : 'issue.'
657 var changed = self.issues.isStatusChanged(msg, pr)
658 return cb(null, '<section class="collapse">' +
659 req._t(changed == null ? 'CommentedOn' :
660 changed ? 'ReopenedIssue' : 'ClosedIssue', {
661 name: authorLink,
662 type: req._t(type),
663 title: u.link([pr.id], pr.title, true)
664 }) + ' ' + msgDateLink +
665 (c.text ? '<blockquote>' + markdown(c.text) + '</blockquote>' : '') +
666 '</section>')
667 })
668 default:
669 return cb(null, u.json(msg))
670 }
671}
672
673/* Index */
674
675G.serveIndex = function (req) {
676 return this.serveTemplate(req)(this.renderFeed(req))
677}
678
679/* Message */
680
681G.serveMessage = function (req, id, path) {
682 var self = this
683 return u.readNext(function (cb) {
684 self.getMsg(id, function (err, msg) {
685 if (err) return cb(null, self.serveError(req, err))
686 var c = msg && msg.value && msg.value.content || {}
687 switch (c.type) {
688 case 'git-repo':
689 return self.getRepo(id, function (err, repo) {
690 if (err) return cb(null, self.serveError(req, err))
691 cb(null, self.repos.serveRepoPage(req, GitRepo(repo), path))
692 })
693 case 'git-update':
694 return self.getRepo(c.repo, function (err, repo) {
695 if (err) return cb(null,
696 self.repos.serveRepoNotFound(req, c.repo, err))
697 cb(null, self.repos.serveRepoUpdate(req,
698 GitRepo(repo), msg, path))
699 })
700 case 'issue':
701 return self.getRepo(c.project, function (err, repo) {
702 if (err) return cb(null,
703 self.repos.serveRepoNotFound(req, c.project, err))
704 self.issues.get(id, function (err, issue) {
705 if (err) return cb(null, self.serveError(req, err))
706 cb(null, self.repos.issues.serveRepoIssue(req,
707 GitRepo(repo), issue, path))
708 })
709 })
710 case 'pull-request':
711 return self.getRepo(c.repo, function (err, repo) {
712 if (err) return cb(null,
713 self.repos.serveRepoNotFound(req, c.project, err))
714 self.pullReqs.get(id, function (err, pr) {
715 if (err) return cb(null, self.serveError(req, err))
716 cb(null, self.repos.pulls.serveRepoPullReq(req,
717 GitRepo(repo), pr, path))
718 })
719 })
720 case 'issue-edit':
721 if (ref.isMsgId(c.issue)) {
722 return self.pullReqs.get(c.issue, function (err, issue) {
723 if (err) return cb(err)
724 self.getRepo(issue.project, function (err, repo) {
725 if (err) {
726 if (!repo) return cb(null,
727 self.repos.serveRepoNotFound(req, c.repo, err))
728 return cb(null, self.serveError(req, err))
729 }
730 cb(null, self.repos.serveIssueOrPullRequest(req, GitRepo(repo),
731 issue, path, id))
732 })
733 })
734 }
735 // fallthrough
736 case 'post':
737 if (ref.isMsgId(c.issue) && ref.isMsgId(c.repo)) {
738 // comment on an issue
739 var done = multicb({ pluck: 1, spread: true })
740 self.getRepo(c.repo, done())
741 self.pullReqs.get(c.issue, done())
742 return done(function (err, repo, issue) {
743 if (err) {
744 if (!repo) return cb(null,
745 self.repos.serveRepoNotFound(req, c.repo, err))
746 return cb(null, self.serveError(req, err))
747 }
748 cb(null, self.repos.serveIssueOrPullRequest(req, GitRepo(repo),
749 issue, path, id))
750 })
751 } else if (ref.isMsgId(c.root)) {
752 // comment on issue from patchwork?
753 return self.getMsg(c.root, function (err, root) {
754 var rc = root.value && root.value.content && root.value.content
755 if (err) return cb(null, self.serveError(req, err))
756 var repoId = rc.repo || rc.project
757 if (!ref.isMsgId(repoId))
758 return cb(null, self.serveGenericMessage(req, msg, path))
759 self.getRepo(repoId, function (err, repo) {
760 if (err) return cb(null, self.serveError(req, err))
761 switch (rc && rc.type) {
762 case 'issue':
763 return self.issues.get(c.root, function (err, issue) {
764 if (err) return cb(null, self.serveError(req, err))
765 return cb(null,
766 self.repos.issues.serveRepoIssue(req,
767 GitRepo(repo), issue, path, id))
768 })
769 case 'pull-request':
770 return self.pullReqs.get(c.root, function (err, pr) {
771 if (err) return cb(null, self.serveError(req, err))
772 return cb(null,
773 self.repos.pulls.serveRepoPullReq(req,
774 GitRepo(repo), pr, path, id))
775 })
776 }
777 })
778 })
779 }
780 // fallthrough
781 default:
782 if (ref.isMsgId(c.repo))
783 return self.getRepo(c.repo, function (err, repo) {
784 if (err) return cb(null,
785 self.repos.serveRepoNotFound(req, c.repo, err))
786 cb(null, self.repos.serveRepoSomething(req,
787 GitRepo(repo), id, msg, path))
788 })
789 else
790 return cb(null, self.serveGenericMessage(req, msg, path))
791 }
792 })
793 })
794}
795
796G.serveGenericMessage = function (req, msg, path) {
797 return this.serveTemplate(req, msg.key)(pull.once(
798 '<section><h2>' + u.link([msg.key]) + '</h2>' +
799 u.json(msg.value) +
800 '</section>'))
801}
802
803/* Search */
804
805G.serveSearch = function (req) {
806 var self = this
807 var q = String(req._u.query.q || '')
808 if (!q) return this.serveIndex(req)
809 var qId = q.replace(/^ssb:\/*/, '')
810 if (ref.type(qId))
811 return this.serveRedirect(req, encodeURIComponent(qId))
812
813 var search = new RegExp(q, 'i')
814 return this.serveTemplate(req, req._t('Search') + ' &middot; ' + q, 200)(
815 this.renderFeed(req, null, function (opts) {
816 return function (read) {
817 return pull(
818 many([
819 self.getMsgs('about', opts),
820 read
821 ]),
822 pull.filter(function (msg) {
823 var c = msg.value.content
824 return (
825 search.test(msg.key) ||
826 c.text && search.test(c.text) ||
827 c.name && search.test(c.name) ||
828 c.title && search.test(c.title))
829 })
830 )
831 }
832 })
833 )
834}
835
836G.getMsgRaw = function (key, cb) {
837 var self = this
838 this.ssb.get(key, function (err, value) {
839 if (err) return cb(err)
840 u.decryptMessage(self.ssb, {key: key, value: value}, cb)
841 })
842}
843
844G.getMsgs = function (type, opts) {
845 return this.ssb.messagesByType({
846 type: type,
847 reverse: opts.reverse,
848 lt: opts.lt,
849 gt: opts.gt,
850 })
851}
852
853G.serveBlobNotFound = function (req, repoId, err) {
854 return this.serveTemplate(req, req._t('error.BlobNotFound'), 404)(pull.once(
855 '<h2>' + req._t('error.BlobNotFound') + '</h2>' +
856 '<p>' + req._t('error.BlobNotFoundInRepo', {
857 repo: u.link([repoId])
858 }) + '</p>' +
859 '<pre>' + u.escape(err.stack) + '</pre>'
860 ))
861}
862
863G.serveRaw = function (length, contentType) {
864 var headers = {
865 'Content-Type': contentType || 'text/plain; charset=utf-8',
866 'Cache-Control': 'max-age=31536000'
867 }
868 if (length != null)
869 headers['Content-Length'] = length
870 return function (read) {
871 return cat([pull.once([200, headers]), read])
872 }
873}
874
875G.getBlob = function (req, key, cb) {
876 var blobs = this.ssb.blobs
877 blobs.want(key, function (err, got) {
878 if (err) cb(err)
879 else if (!got) cb(new Error(req._t('error.MissingBlob', {key: key})))
880 else cb(null, blobs.get(key))
881 })
882}
883
884G.serveBlob = function (req, key) {
885 var self = this
886 return u.readNext(function (cb) {
887 self.getBlob(req, key, function (err, read) {
888 if (err) cb(null, self.serveError(req, err))
889 else if (!read) cb(null, self.serve404(req))
890 else cb(null, identToResp(read))
891 })
892 })
893}
894
895function identToResp(read) {
896 var ended, type, queue
897 var id = ident(function (_type) {
898 type = _type && mime.lookup(_type)
899 })(read)
900 return function (end, cb) {
901 if (ended) return cb(ended)
902 if (end) id(end, function (end) {
903 cb(end === true ? null : end)
904 })
905 else if (queue) {
906 var _queue = queue
907 queue = null
908 cb(null, _queue)
909 }
910 else if (!type)
911 id(null, function (end, data) {
912 if (ended = end) return cb(end)
913 queue = data
914 cb(null, [200, {
915 'Content-Type': type || 'text/plain; charset=utf-8',
916 'Cache-Control': 'max-age=31536000'
917 }])
918 })
919 else
920 id(null, cb)
921 }
922}
923

Built with git-ssb-web