git ssb

30+

cel / git-ssb-web



Tree: 2af72ae1dabcac9c431f8b68c9169371740e924e

Files: 2af72ae1dabcac9c431f8b68c9169371740e924e / index.js

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

Built with git-ssb-web