git ssb

30+

cel / git-ssb-web



Tree: f9c88aecad058003d92879bff90545a8b8d9799c

Files: f9c88aecad058003d92879bff90545a8b8d9799c / index.js

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

Built with git-ssb-web