git ssb

30+

cel / git-ssb-web



Tree: 1708051538ef367952c9c1478a9a64d2928b4088

Files: 1708051538ef367952c9c1478a9a64d2928b4088 / index.js

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

Built with git-ssb-web