git ssb

30+

cel / git-ssb-web



Tree: 568953d78d77276b131c221c8c15f5e4af5edbb4

Files: 568953d78d77276b131c221c8c15f5e4af5edbb4 / index.js

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

Built with git-ssb-web