git ssb

30+

cel / git-ssb-web



Tree: 24932ccb6a1e535b89e82f8f65f67957e0345f4d

Files: 24932ccb6a1e535b89e82f8f65f67957e0345f4d / index.js

80865 bytesRaw
1var fs = require('fs')
2var http = require('http')
3var path = require('path')
4var url = require('url')
5var qs = require('querystring')
6var ref = require('ssb-ref')
7var pull = require('pull-stream')
8var ssbGit = require('ssb-git-repo')
9var toPull = require('stream-to-pull-stream')
10var cat = require('pull-cat')
11var Repo = require('pull-git-repo')
12var ssbAbout = require('./about')
13var ssbVotes = require('./votes')
14var marked = require('ssb-marked')
15var asyncMemo = require('asyncmemo')
16var multicb = require('multicb')
17var schemas = require('ssb-msg-schemas')
18var Issues = require('ssb-issues')
19var PullRequests = require('ssb-pull-requests')
20var paramap = require('pull-paramap')
21var gitPack = require('pull-git-pack')
22var Mentions = require('ssb-mentions')
23var Highlight = require('highlight.js')
24var JsDiff = require('diff')
25var many = require('pull-many')
26
27var hlCssPath = path.resolve(require.resolve('highlight.js'), '../../styles')
28
29// render links to git objects and ssb objects
30var blockRenderer = new marked.Renderer()
31blockRenderer.urltransform = function (url) {
32 if (ref.isLink(url))
33 return encodeLink(url)
34 if (/^[0-9a-f]{40}$/.test(url) && this.options.repo)
35 return encodeLink([this.options.repo.id, 'commit', url])
36 return url
37}
38
39blockRenderer.image = function (href, title, text) {
40 href = href.replace(/^&/, '&')
41 var url
42 if (ref.isBlobId(href))
43 url = encodeLink(href)
44 else if (this.options.repo && this.options.rev && this.options.path)
45 url = path.join('/', encodeURIComponent(this.options.repo.id),
46 'raw', this.options.rev, this.options.path.join('/'), href)
47 else
48 return text
49 return '<img src="' + escapeHTML(url) + '" alt="' + text + '"' +
50 (title ? ' title="' + title + '"' : '') + '/>'
51}
52
53function getExtension(filename) {
54 return (/\.([^.]+)$/.exec(filename) || [,filename])[1]
55}
56
57function highlight(code, lang) {
58 try {
59 return lang
60 ? Highlight.highlight(lang, code).value
61 : Highlight.highlightAuto(code).value
62 } catch(e) {
63 if (/^Unknown language/.test(e.message))
64 return escapeHTML(code)
65 throw e
66 }
67}
68
69marked.setOptions({
70 gfm: true,
71 mentions: true,
72 tables: true,
73 breaks: true,
74 pedantic: false,
75 sanitize: true,
76 smartLists: true,
77 smartypants: false,
78 highlight: highlight,
79 renderer: blockRenderer
80})
81
82// hack to make git link mentions work
83var mdRules = new marked.InlineLexer(1, marked.defaults).rules
84mdRules.mention =
85 /^(\s)?([@%&][A-Za-z0-9\._\-+=\/]*[A-Za-z0-9_\-+=\/]|[0-9a-f]{40})/
86mdRules.text = /^[\s\S]+?(?=[\\<!\[_*`]| {2,}\n| [@%&]|[0-9a-f]{40}|$)/
87
88function markdown(text, options, cb) {
89 if (!text) return ''
90 if (typeof text != 'string') text = String(text)
91 if (!options) options = {}
92 else if (options.id) options = {repo: options}
93 if (!options.rev) options.rev = 'HEAD'
94 if (!options.path) options.path = []
95
96 return marked(text, options, cb)
97}
98
99function parseAddr(str, def) {
100 if (!str) return def
101 var i = str.lastIndexOf(':')
102 if (~i) return {host: str.substr(0, i), port: str.substr(i+1)}
103 if (isNaN(str)) return {host: str, port: def.port}
104 return {host: def.host, port: str}
105}
106
107function isArray(arr) {
108 return Object.prototype.toString.call(arr) == '[object Array]'
109}
110
111function encodeLink(url) {
112 if (!isArray(url)) url = [url]
113 return '/' + url.map(encodeURIComponent).join('/')
114}
115
116function link(parts, text, raw, props) {
117 if (text == null) text = parts[parts.length-1]
118 if (!raw) text = escapeHTML(text)
119 return '<a href="' + encodeLink(parts) + '"' +
120 (props ? ' ' + props : '') +
121 '>' + text + '</a>'
122}
123
124function linkify(text) {
125 // regex is from ssb-ref
126 return text.replace(/(@|%|&)[A-Za-z0-9\/+]{43}=\.[\w\d]+/g, function (str) {
127 return '<a href="/' + encodeURIComponent(str) + '">' + str + '</a>'
128 })
129}
130
131function timestamp(time) {
132 time = Number(time)
133 var d = new Date(time)
134 return '<span title="' + time + '">' + d.toLocaleString() + '</span>'
135}
136
137function pre(text) {
138 return '<pre>' + escapeHTML(text) + '</pre>'
139}
140
141function json(obj) {
142 return linkify(pre(JSON.stringify(obj, null, 2)))
143}
144
145function escapeHTML(str) {
146 return String(str)
147 .replace(/&/g, '&amp;')
148 .replace(/</g, '&lt;')
149 .replace(/>/g, '&gt;')
150 .replace(/"/g, '&quot;')
151}
152
153function ucfirst(str) {
154 return str[0].toLocaleUpperCase() + str.slice(1)
155}
156
157function table(props) {
158 return function (read) {
159 return cat([
160 pull.once('<table' + (props ? ' ' + props : '') + '>'),
161 pull(
162 read,
163 pull.map(function (row) {
164 return row ? '<tr>' + row.map(function (cell) {
165 return '<td>' + cell + '</td>'
166 }).join('') + '</tr>' : ''
167 })
168 ),
169 pull.once('</table>')
170 ])
171 }
172}
173
174function ul(props) {
175 return function (read) {
176 return cat([
177 pull.once('<ul' + (props ? ' ' + props : '') + '>'),
178 pull(
179 read,
180 pull.map(function (li) {
181 return '<li>' + li + '</li>'
182 })
183 ),
184 pull.once('</ul>')
185 ])
186 }
187}
188
189function nav(links, page, after) {
190 return ['<nav>'].concat(
191 links.map(function (link) {
192 var href = typeof link[0] == 'string' ? link[0] : encodeLink(link[0])
193 var props = link[2] == page ? ' class="active"' : ''
194 return '<a href="' + href + '"' + props + '>' + link[1] + '</a>'
195 }), after || '', '</nav>').join('')
196}
197
198function renderNameForm(enabled, id, name, action, inputId, title, header) {
199 if (!inputId) inputId = action
200 return '<form class="petname" action="" method="post">' +
201 (enabled ?
202 '<input type="checkbox" class="name-checkbox" id="' + inputId + '" ' +
203 'onfocus="this.form.name.focus()" />' +
204 '<input name="name" class="name" value="' + escapeHTML(name) + '" ' +
205 'onkeyup="if (event.keyCode == 27) this.form.reset()" />' +
206 '<input type="hidden" name="action" value="' + action + '">' +
207 '<input type="hidden" name="id" value="' +
208 escapeHTML(id) + '">' +
209 '<label class="name-toggle" for="' + inputId + '" ' +
210 'title="' + title + '"><i>✍</i></label> ' +
211 '<input class="btn name-btn" type="submit" value="Rename">' +
212 header :
213 header + '<br clear="all"/>'
214 ) +
215 '</form>'
216}
217
218function renderPostForm(repo, placeholder, rows) {
219 return '<input type="radio" class="tab-radio" id="tab1" name="tab" checked="checked"/>' +
220 '<input type="radio" class="tab-radio" id="tab2" name="tab"/>' +
221 '<div id="tab-links" class="tab-links" style="display:none">' +
222 '<label for="tab1" id="write-tab-link" class="tab1-link">Write</label>' +
223 '<label for="tab2" id="preview-tab-link" class="tab2-link">Preview</label>' +
224 '</div>' +
225 '<input type="hidden" id="repo-id" value="' + repo.id + '"/>' +
226 '<div id="write-tab" class="tab1">' +
227 '<textarea id="post-text" name="text" class="wide-input"' +
228 ' rows="' + (rows||4) + '" cols="77"' +
229 (placeholder ? ' placeholder="' + placeholder + '"' : '') +
230 '></textarea>' +
231 '</div>' +
232 '<div class="preview-text tab2" id="preview-tab"></div>' +
233 '<script>' + issueCommentScript + '</script>'
234}
235
236function hiddenInputs(values) {
237 return Object.keys(values).map(function (key) {
238 return '<input type="hidden"' +
239 ' name="' + escapeHTML(key) + '"' +
240 ' value="' + escapeHTML(values[key]) + '"/>'
241 }).join('')
242}
243
244function readNext(fn) {
245 var next
246 return function (end, cb) {
247 if (next) return next(end, cb)
248 fn(function (err, _next) {
249 if (err) return cb(err)
250 next = _next
251 next(null, cb)
252 })
253 }
254}
255
256function readOnce(fn) {
257 var ended
258 return function (end, cb) {
259 fn(function (err, data) {
260 if (err || ended) return cb(err || ended)
261 ended = true
262 cb(null, data)
263 })
264 }
265}
266
267function paginate(onFirst, through, onLast, onEmpty) {
268 var ended, last, first = true, queue = []
269 return function (read) {
270 var mappedRead = through(function (end, cb) {
271 if (ended = end) return read(ended, cb)
272 if (queue.length)
273 return cb(null, queue.shift())
274 read(null, function (end, data) {
275 if (end) return cb(end)
276 last = data
277 cb(null, data)
278 })
279 })
280 return function (end, cb) {
281 var tmp
282 if (ended) return cb(ended)
283 if (ended = end) return read(ended, cb)
284 if (first)
285 return read(null, function (end, data) {
286 if (ended = end) {
287 if (end === true && onEmpty)
288 return onEmpty(cb)
289 return cb(ended)
290 }
291 first = false
292 last = data
293 queue.push(data)
294 if (onFirst)
295 onFirst(data, cb)
296 else
297 mappedRead(null, cb)
298 })
299 mappedRead(null, function (end, data) {
300 if (ended = end) {
301 if (end === true && last)
302 return onLast(last, cb)
303 }
304 cb(end, data)
305 })
306 }
307 }
308}
309
310function readObjectString(obj, cb) {
311 pull(obj.read, pull.collect(function (err, bufs) {
312 if (err) return cb(err)
313 cb(null, Buffer.concat(bufs, obj.length).toString('utf8'))
314 }))
315}
316
317function getRepoObjectString(repo, id, cb) {
318 if (!id) return cb(null, '')
319 repo.getObjectFromAny(id, function (err, obj) {
320 if (err) return cb(err)
321 readObjectString(obj, cb)
322 })
323}
324
325function compareMsgs(a, b) {
326 return (a.value.timestamp - b.value.timestamp) || (a.key - b.key)
327}
328
329function pullSort(comparator) {
330 return function (read) {
331 return readNext(function (cb) {
332 pull(read, pull.collect(function (err, items) {
333 if (err) return cb(err)
334 items.sort(comparator)
335 cb(null, pull.values(items))
336 }))
337 })
338 }
339}
340
341function sortMsgs() {
342 return pullSort(compareMsgs)
343}
344
345function pullReverse() {
346 return function (read) {
347 return readNext(function (cb) {
348 pull(read, pull.collect(function (err, items) {
349 cb(err, items && pull.values(items.reverse()))
350 }))
351 })
352 }
353}
354
355function tryDecodeURIComponent(str) {
356 if (!str || (str[0] == '%' && ref.isBlobId(str)))
357 return str
358 try {
359 str = decodeURIComponent(str)
360 } finally {
361 return str
362 }
363}
364
365function getRepoName(about, ownerId, repoId, cb) {
366 about.getName({
367 owner: ownerId,
368 target: repoId,
369 toString: function () {
370 // hack to fit two parameters into asyncmemo
371 return ownerId + '/' + repoId
372 }
373 }, cb)
374}
375
376function getRepoFullName(about, author, repoId, cb) {
377 var done = multicb({ pluck: 1, spread: true })
378 getRepoName(about, author, repoId, done())
379 about.getName(author, done())
380 done(cb)
381}
382
383function addAuthorName(about) {
384 return paramap(function (msg, cb) {
385 var author = msg && msg.value && msg.value.author
386 if (!author) return cb(null, msg)
387 about.getName(author, function (err, authorName) {
388 msg.authorName = authorName
389 cb(err, msg)
390 })
391 }, 8)
392}
393
394function getMention(msg, id) {
395 if (msg.key == id) return msg
396 var mentions = msg.value.content.mentions
397 if (mentions) for (var i = 0; i < mentions.length; i++) {
398 var mention = mentions[i]
399 if (mention.link == id)
400 return mention
401 }
402 return null
403}
404
405var hasOwnProp = Object.prototype.hasOwnProperty
406
407function getContentType(filename) {
408 var ext = getExtension(filename)
409 return contentTypes[ext] || imgMimes[ext] || 'text/plain; charset=utf-8'
410}
411
412var contentTypes = {
413 css: 'text/css'
414}
415
416function readReqForm(req, cb) {
417 pull(
418 toPull(req),
419 pull.collect(function (err, bufs) {
420 if (err) return cb(err)
421 var data
422 try {
423 data = qs.parse(Buffer.concat(bufs).toString('ascii'))
424 } catch(e) {
425 return cb(e)
426 }
427 cb(null, data)
428 })
429 )
430}
431
432var issueCommentScript = '(' + function () {
433 var $ = document.getElementById.bind(document)
434 $('tab-links').style.display = 'block'
435 $('preview-tab-link').onclick = function (e) {
436 with (new XMLHttpRequest()) {
437 open('POST', '', true)
438 onload = function() {
439 $('preview-tab').innerHTML = responseText
440 }
441 send('action=markdown' +
442 '&repo=' + encodeURIComponent($('repo-id').value) +
443 '&text=' + encodeURIComponent($('post-text').value))
444 }
445 }
446}.toString() + ')()'
447
448var msgTypes = {
449 'git-repo': true,
450 'git-update': true,
451 'issue': true,
452 'pull-request': true
453}
454
455var imgMimes = {
456 png: 'image/png',
457 jpeg: 'image/jpeg',
458 jpg: 'image/jpeg',
459 gif: 'image/gif',
460 tif: 'image/tiff',
461 svg: 'image/svg+xml',
462 bmp: 'image/bmp'
463}
464
465module.exports = function (opts, cb) {
466 var ssb, reconnect, myId, getRepo, getVotes, getMsg, issues
467 var about = function (id, cb) { cb(null, {name: id}) }
468 var reqQueue = []
469 var isPublic = opts.public
470 var ssbAppname = opts.appname || 'ssb'
471
472 var addr = parseAddr(opts.listenAddr, {host: 'localhost', port: 7718})
473 http.createServer(onRequest).listen(addr.port, addr.host, onListening)
474
475 var server = {
476 setSSB: function (_ssb, _reconnect) {
477 _ssb.whoami(function (err, feed) {
478 if (err) throw err
479 ssb = _ssb
480 reconnect = _reconnect
481 myId = feed.id
482 about = ssbAbout(ssb, myId)
483 while (reqQueue.length)
484 onRequest.apply(this, reqQueue.shift())
485 getRepo = asyncMemo(function (id, cb) {
486 getMsg(id, function (err, msg) {
487 if (err) return cb(err)
488 ssbGit.getRepo(ssb, {key: id, value: msg}, {live: true}, cb)
489 })
490 })
491 getVotes = ssbVotes(ssb)
492 getMsg = asyncMemo(ssb.get)
493 issues = Issues.init(ssb)
494 pullReqs = PullRequests.init(ssb)
495 })
496 }
497 }
498
499 function onListening() {
500 var host = ~addr.host.indexOf(':') ? '[' + addr.host + ']' : addr.host
501 console.log('Listening on http://' + host + ':' + addr.port + '/')
502 cb(null, server)
503 }
504
505 /* Serving a request */
506
507 function onRequest(req, res) {
508 console.log(req.method, req.url)
509 if (!ssb) return reqQueue.push(arguments)
510 pull(
511 handleRequest(req),
512 pull.filter(function (data) {
513 if (Array.isArray(data)) {
514 res.writeHead.apply(res, data)
515 return false
516 }
517 return true
518 }),
519 toPull(res)
520 )
521 }
522
523 function handleRequest(req) {
524 var u = req._u = url.parse(req.url, true)
525 var path = u.pathname.slice(1)
526 var dirs = ref.isLink(path) ? [path] :
527 path.split(/\/+/).map(tryDecodeURIComponent)
528 var dir = dirs[0]
529
530 if (req.method == 'POST') {
531 if (isPublic)
532 return serveBuffer(405, 'POST not allowed on public site')
533 return readNext(function (cb) {
534 readReqForm(req, function (err, data) {
535 if (err) return cb(null, serveError(err, 400))
536 if (!data) return cb(null, serveError(new Error('No data'), 400))
537
538 switch (data.action) {
539 case 'repo':
540 if (data.fork != null) {
541 var repoId = data.id
542 if (!repoId) return cb(null,
543 serveError(new Error('Missing repo id'), 400))
544 ssbGit.createRepo(ssb, {upstream: repoId},
545 function (err, repo) {
546 if (err) return cb(null, serveError(err))
547 cb(null, serveRedirect(encodeLink(repo.id)))
548 })
549 } else if (data.vote != null) {
550 // fallthrough
551 } else {
552 return cb(null, serveError(new Error('Unknown action'), 400))
553 }
554
555 case 'vote':
556 var voteValue = +data.value || 0
557 if (!data.id)
558 return cb(null, serveError(new Error('Missing vote id'), 400))
559 var msg = schemas.vote(data.id, voteValue)
560 return ssb.publish(msg, function (err) {
561 if (err) return cb(null, serveError(err))
562 cb(null, serveRedirect(req.url))
563 })
564
565 case 'repo-name':
566 if (!data.name)
567 return cb(null, serveError(new Error('Missing name'), 400))
568 if (!data.id)
569 return cb(null, serveError(new Error('Missing id'), 400))
570 var msg = schemas.name(data.id, data.name)
571 return ssb.publish(msg, function (err) {
572 if (err) return cb(null, serveError(err))
573 cb(null, serveRedirect(req.url))
574 })
575
576 case 'issue-title':
577 if (!data.name)
578 return cb(null, serveError(new Error('Missing name'), 400))
579 if (!data.id)
580 return cb(null, serveError(new Error('Missing id'), 400))
581 var msg = Issues.schemas.edit(data.id, {title: data.name})
582 return ssb.publish(msg, function (err) {
583 if (err) return cb(null, serveError(err))
584 cb(null, serveRedirect(req.url))
585 })
586
587 case 'comment':
588 if (!data.id)
589 return cb(null, serveError(new Error('Missing id'), 400))
590
591 var msg = schemas.post(data.text, data.id, data.branch || data.id)
592 msg.issue = data.issue
593 msg.repo = data.repo
594 if (data.open != null)
595 Issues.schemas.opens(msg, data.id)
596 if (data.close != null)
597 Issues.schemas.closes(msg, data.id)
598 var mentions = Mentions(data.text)
599 if (mentions.length)
600 msg.mentions = mentions
601 return ssb.publish(msg, function (err) {
602 if (err) return cb(null, serveError(err))
603 cb(null, serveRedirect(req.url))
604 })
605
606 case 'new-issue':
607 var msg = Issues.schemas.new(dir, data.title, data.text)
608 var mentions = Mentions(data.text)
609 if (mentions.length)
610 msg.mentions = mentions
611 return ssb.publish(msg, function (err, msg) {
612 if (err) return cb(null, serveError(err))
613 cb(null, serveRedirect(encodeLink(msg.key)))
614 })
615
616 case 'new-pull':
617 var msg = PullRequests.schemas.new(dir, data.branch,
618 data.head_repo, data.head_branch, data.title, data.text)
619 var mentions = Mentions(data.text)
620 if (mentions.length)
621 msg.mentions = mentions
622 return ssb.publish(msg, function (err, msg) {
623 if (err) return cb(null, serveError(err))
624 cb(null, serveRedirect(encodeLink(msg.key)))
625 })
626
627 case 'markdown':
628 return cb(null, serveMarkdown(data.text, {id: data.repo}))
629
630 default:
631 cb(null, serveBuffer(400, 'What are you trying to do?'))
632 }
633 })
634 })
635 }
636
637 if (dir == '')
638 return serveIndex(req)
639 else if (ref.isBlobId(dir))
640 return serveBlob(req, dir)
641 else if (ref.isMsgId(dir))
642 return serveMessage(req, dir, dirs.slice(1))
643 else if (ref.isFeedId(dir))
644 return serveUserPage(req, dir, dirs.slice(1))
645 else if (dir == 'static')
646 return serveFile(req, dirs)
647 else if (dir == 'highlight')
648 return serveFile(req, [hlCssPath].concat(dirs.slice(1)), true)
649 else
650 return serve404(req)
651 }
652
653 function serveFile(req, dirs, outside) {
654 var filename = path.resolve.apply(path, [__dirname].concat(dirs))
655 // prevent escaping base dir
656 if (!outside && filename.indexOf('../') === 0)
657 return serveBuffer(403, '403 Forbidden')
658
659 return readNext(function (cb) {
660 fs.stat(filename, function (err, stats) {
661 cb(null, err ?
662 err.code == 'ENOENT' ? serve404(req)
663 : serveBuffer(500, err.message)
664 : 'if-modified-since' in req.headers &&
665 new Date(req.headers['if-modified-since']) >= stats.mtime ?
666 pull.once([304])
667 : stats.isDirectory() ?
668 serveBuffer(403, 'Directory not listable')
669 : cat([
670 pull.once([200, {
671 'Content-Type': getContentType(filename),
672 'Content-Length': stats.size,
673 'Last-Modified': stats.mtime.toGMTString()
674 }]),
675 toPull(fs.createReadStream(filename))
676 ]))
677 })
678 })
679 }
680
681 function servePlainError(code, msg) {
682 return pull.values([
683 [code, {
684 'Content-Length': Buffer.byteLength(msg),
685 'Content-Type': 'text/plain; charset=utf-8'
686 }],
687 msg
688 ])
689 }
690
691 function serveBuffer(code, buf, contentType, headers) {
692 headers = headers || {}
693 headers['Content-Type'] = contentType || 'text/plain; charset=utf-8'
694 headers['Content-Length'] = Buffer.byteLength(buf)
695 return pull.values([
696 [code, headers],
697 buf
698 ])
699 }
700
701 function serve404(req) {
702 return serveBuffer(404, '404 Not Found')
703 }
704
705 function serveRedirect(path) {
706 return serveBuffer(302,
707 '<!doctype><html><head>' +
708 '<title>Redirect</title></head><body>' +
709 '<p><a href="' + escapeHTML(path) + '">Continue</a></p>' +
710 '</body></html>', 'text/html; charset=utf-8', {Location: path})
711 }
712
713 function serveMarkdown(text, repo) {
714 return serveBuffer(200, markdown(text, repo), 'text/html; charset=utf-8')
715 }
716
717 function renderError(err, tag) {
718 tag = tag || 'h3'
719 return '<' + tag + '>' + err.name + '</' + tag + '>' +
720 '<pre>' + escapeHTML(err.stack) + '</pre>'
721 }
722
723 function renderTry(read) {
724 var ended
725 return function (end, cb) {
726 if (ended) return cb(ended)
727 read(end, function (err, data) {
728 if (err === true)
729 cb(true)
730 else if (err) {
731 ended = true
732 cb(null, renderError(err, 'h3'))
733 } else
734 cb(null, data)
735 })
736 }
737 }
738
739 function serveTemplate(title, code, read) {
740 if (read === undefined) return serveTemplate.bind(this, title, code)
741 return cat([
742 pull.values([
743 [code || 200, {
744 'Content-Type': 'text/html'
745 }],
746 '<!doctype html><html><head><meta charset=utf-8>',
747 '<title>' + escapeHTML(title || 'git ssb') + '</title>',
748 '<link rel=stylesheet href="/static/styles.css"/>',
749 '<link rel=stylesheet href="/highlight/github.css"/>',
750 '</head>\n',
751 '<body>',
752 '<header>',
753 '<h1><a href="/">git ssb' +
754 (ssbAppname != 'ssb' ? ' <sub>' + ssbAppname + '</sub>' : '') +
755 '</a></h1>',
756 '</header>',
757 '<article>']),
758 renderTry(read),
759 pull.once('<hr/></article></body></html>')
760 ])
761 }
762
763 function serveError(err, status) {
764 if (err.message == 'stream is closed')
765 reconnect()
766 return pull(
767 pull.once(renderError(err, 'h2')),
768 serveTemplate(err.name, status || 500)
769 )
770 }
771
772 function renderObjectData(obj, filename, repo, rev, path) {
773 var ext = getExtension(filename)
774 return readOnce(function (cb) {
775 readObjectString(obj, function (err, buf) {
776 buf = buf.toString('utf8')
777 if (err) return cb(err)
778 cb(null, (ext == 'md' || ext == 'markdown')
779 ? markdown(buf, {repo: repo, rev: rev, path: path})
780 : renderCodeTable(buf, ext))
781 })
782 })
783 }
784
785 function renderCodeTable(buf, ext) {
786 return '<pre><table class="code">' +
787 highlight(buf, ext).split('\n').map(function (line, i) {
788 i++
789 return '<tr id="L' + i + '">' +
790 '<td class="code-linenum">' + '<a href="#L' + i + '">' + i + '</td>' +
791 '<td class="code-text">' + line + '</td></tr>'
792 }).join('') +
793 '</table></pre>'
794 }
795
796 /* Feed */
797
798 function renderFeed(req, feedId) {
799 var query = req._u.query
800 var opts = {
801 reverse: !query.forwards,
802 lt: query.lt && +query.lt || Date.now(),
803 gt: query.gt && +query.gt,
804 id: feedId
805 }
806 return pull(
807 feedId ? ssb.createUserStream(opts) : ssb.createFeedStream(opts),
808 pull.filter(function (msg) {
809 return msg.value.content.type in msgTypes
810 }),
811 pull.take(20),
812 addAuthorName(about),
813 query.forwards && pullReverse(),
814 paginate(
815 function (first, cb) {
816 if (!query.lt && !query.gt) return cb(null, '')
817 var gt = feedId ? first.value.sequence : first.value.timestamp + 1
818 var q = qs.stringify({
819 gt: gt,
820 forwards: 1
821 })
822 cb(null, '<a href="?' + q + '">Newer</a>')
823 },
824 paramap(renderFeedItem, 8),
825 function (last, cb) {
826 cb(null, '<a href="?' + qs.stringify({
827 lt: feedId ? last.value.sequence : last.value.timestamp - 1
828 }) + '">Older</a>')
829 },
830 function (cb) {
831 cb(null, query.forwards ?
832 '<a href="?lt=' + (opts.gt + 1) + '">Older</a>' :
833 '<a href="?gt=' + (opts.lt - 1) + '&amp;forwards=1">Newer</a>')
834 }
835 )
836 )
837 }
838
839 function renderFeedItem(msg, cb) {
840 var c = msg.value.content
841 var msgLink = link([msg.key],
842 new Date(msg.value.timestamp).toLocaleString())
843 var author = msg.value.author
844 var authorLink = link([msg.value.author], msg.authorName)
845 switch (c.type) {
846 case 'git-repo':
847 var done = multicb({ pluck: 1, spread: true })
848 getRepoName(about, author, msg.key, done())
849 if (c.upstream) {
850 getRepoName(about, author, c.upstream, done())
851 return done(function (err, repoName, upstreamName) {
852 cb(null, '<section class="collapse">' + msgLink + '<br>' +
853 authorLink + ' forked ' + link([c.upstream], upstreamName) +
854 ' to ' + link([msg.key], repoName) + '</section>')
855 })
856 } else {
857 return done(function (err, repoName) {
858 if (err) return cb(err)
859 var repoLink = link([msg.key], repoName)
860 cb(null, '<section class="collapse">' + msgLink + '<br>' +
861 authorLink + ' created repo ' + repoLink + '</section>')
862 })
863 }
864 case 'git-update':
865 return getRepoName(about, author, c.repo, function (err, repoName) {
866 if (err) return cb(err)
867 var repoLink = link([c.repo], repoName)
868 cb(null, '<section class="collapse">' + msgLink + '<br>' +
869 authorLink + ' pushed to ' + repoLink + '</section>')
870 })
871 case 'issue':
872 case 'pull-request':
873 var issueLink = link([msg.key], c.title)
874 return getRepoName(about, author, c.project, function (err, repoName) {
875 if (err) return cb(err)
876 var repoLink = link([c.project], repoName)
877 cb(null, '<section class="collapse">' + msgLink + '<br>' +
878 authorLink + ' opened ' + c.type + ' ' + issueLink +
879 ' on ' + repoLink + '</section>')
880 })
881 }
882 }
883
884 /* Index */
885
886 function serveIndex(req) {
887 return serveTemplate('git ssb')(renderFeed(req))
888 }
889
890 function serveUserPage(req, feedId, dirs) {
891 switch (dirs[0]) {
892 case undefined:
893 case '':
894 case 'activity':
895 return serveUserActivity(req, feedId)
896 case 'repos':
897 return serveUserRepos(feedId)
898 }
899 }
900
901 function renderUserPage(feedId, page, body) {
902 return serveTemplate(feedId)(cat([
903 readOnce(function (cb) {
904 about.getName(feedId, function (err, name) {
905 cb(null, '<h2>' + link([feedId], name) +
906 '<code class="user-id">' + feedId + '</code></h2>' +
907 nav([
908 [[feedId], 'Activity', 'activity'],
909 [[feedId, 'repos'], 'Repos', 'repos']
910 ], page))
911 })
912 }),
913 body,
914 ]))
915 }
916
917 function serveUserActivity(req, feedId) {
918 return renderUserPage(feedId, 'activity', renderFeed(req, feedId))
919 }
920
921 function serveUserRepos(feedId) {
922 return renderUserPage(feedId, 'repos', pull(
923 ssb.messagesByType({
924 type: 'git-repo',
925 reverse: true
926 }),
927 pull.filter(function (msg) {
928 return msg.value.author == feedId
929 }),
930 pull.take(20),
931 paramap(function (msg, cb) {
932 getRepoName(about, feedId, msg.key, function (err, repoName) {
933 if (err) return cb(err)
934 cb(null, '<section class="collapse">' +
935 link([msg.key], repoName) +
936 '</section>')
937 })
938 }, 8)
939 ))
940 }
941
942 /* Message */
943
944 function serveMessage(req, id, path) {
945 return readNext(function (cb) {
946 ssb.get(id, function (err, msg) {
947 if (err) return cb(null, serveError(err))
948 var c = msg.content || {}
949 switch (c.type) {
950 case 'git-repo':
951 return getRepo(id, function (err, repo) {
952 if (err) return cb(null, serveError(err))
953 cb(null, serveRepoPage(req, Repo(repo), path))
954 })
955 case 'git-update':
956 return getRepo(c.repo, function (err, repo) {
957 if (err) return cb(null, serveRepoNotFound(c.repo, err))
958 cb(null, serveRepoUpdate(req, Repo(repo), id, msg, path))
959 })
960 case 'issue':
961 return getRepo(c.project, function (err, repo) {
962 if (err) return cb(null, serveRepoNotFound(c.project, err))
963 issues.get(id, function (err, issue) {
964 if (err) return cb(null, serveError(err))
965 cb(null, serveRepoIssue(req, Repo(repo), issue, path))
966 })
967 })
968 case 'pull-request':
969 return getRepo(c.repo, function (err, repo) {
970 if (err) return cb(null, serveRepoNotFound(c.project, err))
971 pullReqs.get(id, function (err, pr) {
972 if (err) return cb(null, serveError(err))
973 cb(null, serveRepoPullReq(req, Repo(repo), pr, path))
974 })
975 })
976 case 'issue-edit':
977 if (ref.isMsgId(c.issue)) {
978 return pullReqs.get(c.issue, function (err, issue) {
979 if (err) return cb(err)
980 var serve = issue.msg.value.content.type == 'pull-request' ?
981 serveRepoPullReq : serveRepoIssue
982 getRepo(issue.project, function (err, repo) {
983 if (err) {
984 if (!repo) return cb(null, serveRepoNotFound(c.repo, err))
985 return cb(null, serveError(err))
986 }
987 cb(null, serve(req, Repo(repo), issue, path, id))
988 })
989 })
990 }
991 // fallthrough
992 case 'post':
993 if (ref.isMsgId(c.issue) && ref.isMsgId(c.repo)) {
994 var done = multicb({ pluck: 1, spread: true })
995 getRepo(c.repo, done())
996 pullReqs.get(c.issue, done())
997 return done(function (err, repo, issue) {
998 if (err) {
999 if (!repo) return cb(null, serveRepoNotFound(c.repo, err))
1000 return cb(null, serveError(err))
1001 }
1002 var serve = issue.msg.value.content.type == 'pull-request' ?
1003 serveRepoPullReq : serveRepoIssue
1004 cb(null, serve(req, Repo(repo), issue, path, id))
1005 })
1006 }
1007 // fallthrough
1008 default:
1009 if (ref.isMsgId(c.repo))
1010 return getRepo(c.repo, function (err, repo) {
1011 if (err) return cb(null, serveRepoNotFound(c.repo, err))
1012 cb(null, serveRepoSomething(req, Repo(repo), id, msg, path))
1013 })
1014 else
1015 return cb(null, serveGenericMessage(req, id, msg, path))
1016 }
1017 })
1018 })
1019 }
1020
1021 function serveGenericMessage(req, id, msg, path) {
1022 return serveTemplate(id)(pull.once(
1023 '<section><h2>' + link([id]) + '</h2>' +
1024 json(msg) +
1025 '</section>'))
1026 }
1027
1028 /* Repo */
1029
1030 function serveRepoPage(req, repo, path) {
1031 var defaultBranch = 'master'
1032 var query = req._u.query
1033
1034 if (query.rev != null) {
1035 // Allow navigating revs using GET query param.
1036 // Replace the branch in the path with the rev query value
1037 path[0] = path[0] || 'tree'
1038 path[1] = query.rev
1039 req._u.pathname = encodeLink([repo.id].concat(path))
1040 delete req._u.query.rev
1041 delete req._u.search
1042 return serveRedirect(url.format(req._u))
1043 }
1044
1045 // get branch
1046 return path[1] ?
1047 serveRepoPage2(req, repo, path) :
1048 readNext(function (cb) {
1049 // TODO: handle this in pull-git-repo or ssb-git-repo
1050 repo.getSymRef('HEAD', true, function (err, ref) {
1051 if (err) return cb(err)
1052 repo.resolveRef(ref, function (err, rev) {
1053 path[1] = rev ? ref : null
1054 cb(null, serveRepoPage2(req, repo, path))
1055 })
1056 })
1057 })
1058 }
1059
1060 function serveRepoPage2(req, repo, path) {
1061 var branch = path[1]
1062 var filePath = path.slice(2)
1063 switch (path[0]) {
1064 case undefined:
1065 case '':
1066 return serveRepoTree(repo, branch, [])
1067 case 'activity':
1068 return serveRepoActivity(repo, branch)
1069 case 'commits':
1070 return serveRepoCommits(req, repo, branch)
1071 case 'commit':
1072 return serveRepoCommit(repo, path[1])
1073 case 'tree':
1074 return serveRepoTree(repo, branch, filePath)
1075 case 'blob':
1076 return serveRepoBlob(repo, branch, filePath)
1077 case 'raw':
1078 return serveRepoRaw(repo, branch, filePath)
1079 case 'digs':
1080 return serveRepoDigs(repo)
1081 case 'forks':
1082 return serveRepoForks(repo)
1083 case 'issues':
1084 switch (path[1]) {
1085 case 'new':
1086 if (filePath.length == 0)
1087 return serveRepoNewIssue(repo)
1088 break
1089 default:
1090 return serveRepoIssues(req, repo, filePath)
1091 }
1092 case 'pulls':
1093 return serveRepoPullReqs(req, repo)
1094 case 'compare':
1095 return serveRepoCompare(req, repo)
1096 case 'comparing':
1097 return serveRepoComparing(req, repo)
1098 default:
1099 return serve404(req)
1100 }
1101 }
1102
1103 function serveRepoNotFound(id, err) {
1104 return serveTemplate('Repo not found', 404, pull.values([
1105 '<h2>Repo not found</h2>',
1106 '<p>Repo ' + id + ' was not found</p>',
1107 '<pre>' + escapeHTML(err.stack) + '</pre>',
1108 ]))
1109 }
1110
1111 function renderRepoPage(repo, page, branch, body) {
1112 var gitUrl = 'ssb://' + repo.id
1113 var gitLink = '<input class="clone-url" readonly="readonly" ' +
1114 'value="' + gitUrl + '" size="45" ' +
1115 'onclick="this.select()"/>'
1116 var digsPath = [repo.id, 'digs']
1117
1118 var done = multicb({ pluck: 1, spread: true })
1119 getRepoName(about, repo.feed, repo.id, done())
1120 about.getName(repo.feed, done())
1121 getVotes(repo.id, done())
1122
1123 if (repo.upstream) {
1124 getRepoName(about, repo.upstream.feed, repo.upstream.id, done())
1125 about.getName(repo.upstream.feed, done())
1126 }
1127
1128 return readNext(function (cb) {
1129 done(function (err, repoName, authorName, votes,
1130 upstreamName, upstreamAuthorName) {
1131 if (err) return cb(null, serveError(err))
1132 var upvoted = votes.upvoters[myId] > 0
1133 var upstreamLink = !repo.upstream ? '' :
1134 link([repo.upstream])
1135 cb(null, serveTemplate(repo.id)(cat([
1136 pull.once(
1137 '<div class="repo-title">' +
1138 '<form class="right-bar" action="" method="post">' +
1139 '<button class="btn" name="vote" ' +
1140 (isPublic ? 'disabled="disabled"' : ' type="submit"') + '>' +
1141 '<i>✌</i> ' + (!isPublic && upvoted ? 'Undig' : 'Dig') +
1142 '</button>' +
1143 (isPublic ? '' : '<input type="hidden" name="value" value="' +
1144 (upvoted ? '0' : '1') + '">' +
1145 '<input type="hidden" name="action" value="repo">' +
1146 '<input type="hidden" name="id" value="' +
1147 escapeHTML(repo.id) + '">') + ' ' +
1148 '<strong>' + link(digsPath, votes.upvotes) + '</strong> ' +
1149 (isPublic ? '' :
1150 '<button class="btn" type="submit" name="fork">' +
1151 '<i>⑂</i> Fork' +
1152 '</button>') + ' ' +
1153 link([repo.id, 'forks'], '+', false, ' title="Forks"') +
1154 '</form>' +
1155 renderNameForm(!isPublic, repo.id, repoName, 'repo-name', null,
1156 'Rename the repo',
1157 '<h2 class="bgslash">' + link([repo.feed], authorName) + ' / ' +
1158 link([repo.id], repoName) + '</h2>') +
1159 '</div>' +
1160 (repo.upstream ?
1161 '<small>forked from ' +
1162 link([repo.upstream.feed], upstreamAuthorName) + '\'s ' +
1163 link([repo.upstream.id], upstreamName) +
1164 '</small>' : '') +
1165 nav([
1166 [[repo.id], 'Code', 'code'],
1167 [[repo.id, 'activity'], 'Activity', 'activity'],
1168 [[repo.id, 'commits', branch || ''], 'Commits', 'commits'],
1169 [[repo.id, 'issues'], 'Issues', 'issues'],
1170 [[repo.id, 'pulls'], 'Pull Requests', 'pulls']
1171 ], page, gitLink)),
1172 body
1173 ])))
1174 })
1175 })
1176 }
1177
1178 function serveEmptyRepo(repo) {
1179 if (repo.feed != myId)
1180 return renderRepoPage(repo, 'code', null, pull.once(
1181 '<section>' +
1182 '<h3>Empty repository</h3>' +
1183 '</section>'))
1184
1185 var gitUrl = 'ssb://' + repo.id
1186 return renderRepoPage(repo, 'code', null, pull.once(
1187 '<section>' +
1188 '<h3>Getting started</h3>' +
1189 '<h4>Create a new repository</h4><pre>' +
1190 'touch README.md\n' +
1191 'git init\n' +
1192 'git add README.md\n' +
1193 'git commit -m "Initial commit"\n' +
1194 'git remote add origin ' + gitUrl + '\n' +
1195 'git push -u origin master</pre>\n' +
1196 '<h4>Push an existing repository</h4>\n' +
1197 '<pre>git remote add origin ' + gitUrl + '\n' +
1198 'git push -u origin master</pre>' +
1199 '</section>'))
1200 }
1201
1202 function serveRepoTree(repo, rev, path) {
1203 if (!rev) return serveEmptyRepo(repo)
1204 var type = repo.isCommitHash(rev) ? 'Tree' : 'Branch'
1205 return renderRepoPage(repo, 'code', rev, cat([
1206 pull.once('<section><form action="" method="get">' +
1207 '<h3>' + type + ': ' + rev + ' '),
1208 revMenu(repo, rev),
1209 pull.once('</h3></form>'),
1210 type == 'Branch' && renderRepoLatest(repo, rev),
1211 pull.once('</section><section>'),
1212 renderRepoTree(repo, rev, path),
1213 pull.once('</section>'),
1214 renderRepoReadme(repo, rev, path)
1215 ]))
1216 }
1217
1218 /* Repo activity */
1219
1220 function serveRepoActivity(repo, branch) {
1221 return renderRepoPage(repo, 'activity', branch, cat([
1222 pull.once('<h3>Activity</h3>'),
1223 pull(
1224 ssb.links({
1225 dest: repo.id,
1226 source: repo.feed,
1227 rel: 'repo',
1228 values: true,
1229 reverse: true
1230 }),
1231 pull.map(renderRepoUpdate.bind(this, repo))
1232 ),
1233 readOnce(function (cb) {
1234 var done = multicb({ pluck: 1, spread: true })
1235 about.getName(repo.feed, done())
1236 getMsg(repo.id, done())
1237 done(function (err, authorName, msg) {
1238 if (err) return cb(err)
1239 renderFeedItem({
1240 key: repo.id,
1241 value: msg,
1242 authorName: authorName
1243 }, cb)
1244 })
1245 })
1246 ]))
1247 }
1248
1249 function renderRepoUpdate(repo, msg, full) {
1250 var c = msg.value.content
1251
1252 if (c.type != 'git-update') {
1253 return ''
1254 // return renderFeedItem(msg, cb)
1255 // TODO: render post, issue, pull-request
1256 }
1257
1258 var refs = c.refs ? Object.keys(c.refs).map(function (ref) {
1259 return {name: ref, value: c.refs[ref]}
1260 }) : []
1261 var numObjects = c.objects ? Object.keys(c.objects).length : 0
1262
1263 return '<section class="collapse">' +
1264 link([msg.key], new Date(msg.value.timestamp).toLocaleString()) +
1265 '<br>' +
1266 (numObjects ? 'Pushed ' + numObjects + ' objects<br>' : '') +
1267 refs.map(function (update) {
1268 var name = escapeHTML(update.name)
1269 if (!update.value) {
1270 return 'Deleted ' + name
1271 } else {
1272 var commitLink = link([repo.id, 'commit', update.value])
1273 return name + ' &rarr; ' + commitLink
1274 }
1275 }).join('<br>') +
1276 '</section>'
1277 }
1278
1279 /* Repo commits */
1280
1281 function serveRepoCommits(req, repo, branch) {
1282 var query = req._u.query
1283 return renderRepoPage(repo, 'commits', branch, cat([
1284 pull.once('<h3>Commits</h3>'),
1285 pull(
1286 repo.readLog(query.start || branch),
1287 pull.take(20),
1288 paramap(repo.getCommitParsed.bind(repo), 8),
1289 paginate(
1290 !query.start ? '' : function (first, cb) {
1291 cb(null, '&hellip;')
1292 },
1293 pull.map(renderCommit.bind(this, repo)),
1294 function (commit, cb) {
1295 cb(null, commit.parents && commit.parents[0] ?
1296 '<a href="?start=' + commit.id + '">Older</a>' : '')
1297 }
1298 )
1299 )
1300 ]))
1301 }
1302
1303 function renderCommit(repo, commit) {
1304 var commitPath = [repo.id, 'commit', commit.id]
1305 var treePath = [repo.id, 'tree', commit.id]
1306 return '<section class="collapse">' +
1307 '<strong>' + link(commitPath, commit.title) + '</strong><br>' +
1308 '<tt>' + commit.id + '</tt> ' +
1309 link(treePath, 'Tree') + '<br>' +
1310 escapeHTML(commit.author.name) + ' &middot; ' + commit.author.date.toLocaleString() +
1311 (commit.separateAuthor ? '<br>' + escapeHTML(commit.committer.name) + ' committed on ' + commit.committer.date.toLocaleString() : "") +
1312 '</section>'
1313}
1314
1315 /* Branch menu */
1316
1317 function formatRevOptions(currentName) {
1318 return function (name) {
1319 var htmlName = escapeHTML(name)
1320 return '<option value="' + htmlName + '"' +
1321 (name == currentName ? ' selected="selected"' : '') +
1322 '>' + htmlName + '</option>'
1323 }
1324 }
1325
1326 function revMenu(repo, currentName) {
1327 return readOnce(function (cb) {
1328 repo.getRefNames(true, function (err, refs) {
1329 if (err) return cb(err)
1330 cb(null, '<select name="rev" onchange="this.form.submit()">' +
1331 Object.keys(refs).map(function (group) {
1332 return '<optgroup label="' + group + '">' +
1333 refs[group].map(formatRevOptions(currentName)).join('') +
1334 '</optgroup>'
1335 }).join('') +
1336 '</select><noscript> <input type="submit" value="Go"/></noscript>')
1337 })
1338 })
1339 }
1340
1341 function branchMenu(repo, name, currentName) {
1342 return cat([
1343 pull.once('<select name="' + name + '">'),
1344 pull(
1345 repo.refs(),
1346 pull.map(function (ref) {
1347 var m = ref.name.match(/^refs\/([^\/]*)\/(.*)$/) || [,, ref.name]
1348 return m[1] == 'heads' && m[2]
1349 }),
1350 pull.filter(Boolean),
1351 pullSort(),
1352 pull.map(formatRevOptions(currentName))
1353 ),
1354 pull.once('</select>')
1355 ])
1356 }
1357
1358 /* Repo tree */
1359
1360 function renderRepoLatest(repo, rev) {
1361 return readOnce(function (cb) {
1362 repo.getCommitParsed(rev, function (err, commit) {
1363 if (err) return cb(err)
1364 var commitPath = [repo.id, 'commit', commit.id]
1365 cb(null,
1366 'Latest: <strong>' + link(commitPath, commit.title) +
1367 '</strong><br>' +
1368 '<tt>' + commit.id + '</tt><br> ' +
1369 escapeHTML(commit.committer.name) + ' committed on ' +
1370 commit.committer.date.toLocaleString() +
1371 (commit.separateAuthor ? '<br>' +
1372 escapeHTML(commit.author.name) + ' authored on ' +
1373 commit.author.date.toLocaleString() : ''))
1374 })
1375 })
1376 }
1377
1378 // breadcrumbs
1379 function linkPath(basePath, path) {
1380 path = path.slice()
1381 var last = path.pop()
1382 return path.map(function (dir, i) {
1383 return link(basePath.concat(path.slice(0, i+1)), dir)
1384 }).concat(last).join(' / ')
1385 }
1386
1387 function renderRepoTree(repo, rev, path) {
1388 var pathLinks = path.length === 0 ? '' :
1389 ': ' + linkPath([repo.id, 'tree'], [rev].concat(path))
1390 return cat([
1391 pull.once('<h3>Files' + pathLinks + '</h3>'),
1392 pull(
1393 repo.readDir(rev, path),
1394 pull.map(function (file) {
1395 var type = (file.mode === 040000) ? 'tree' :
1396 (file.mode === 0160000) ? 'commit' : 'blob'
1397 if (type == 'commit')
1398 return ['<span title="git commit link">🖈</span>', '<span title="' + escapeHTML(file.id) + '">' + escapeHTML(file.name) + '</span>']
1399 var filePath = [repo.id, type, rev].concat(path, file.name)
1400 return ['<i>' + (type == 'tree' ? '📁' : '📄') + '</i>',
1401 link(filePath, file.name)]
1402 }),
1403 table('class="files"')
1404 )
1405 ])
1406 }
1407
1408 /* Repo readme */
1409
1410 function renderRepoReadme(repo, branch, path) {
1411 return readNext(function (cb) {
1412 pull(
1413 repo.readDir(branch, path),
1414 pull.filter(function (file) {
1415 return /readme(\.|$)/i.test(file.name)
1416 }),
1417 pull.take(1),
1418 pull.collect(function (err, files) {
1419 if (err) return cb(null, pull.empty())
1420 var file = files[0]
1421 if (!file)
1422 return cb(null, pull.once(path.length ? '' : '<p>No readme</p>'))
1423 repo.getObjectFromAny(file.id, function (err, obj) {
1424 if (err) return cb(err)
1425 cb(null, cat([
1426 pull.once('<section><h4><a name="readme">' +
1427 escapeHTML(file.name) + '</a></h4><hr/>'),
1428 renderObjectData(obj, file.name, repo, branch, path),
1429 pull.once('</section>')
1430 ]))
1431 })
1432 })
1433 )
1434 })
1435 }
1436
1437 /* Repo commit */
1438
1439 function serveRepoCommit(repo, rev) {
1440 return renderRepoPage(repo, null, rev, cat([
1441 readNext(function (cb) {
1442 repo.getCommitParsed(rev, function (err, commit) {
1443 if (err) return cb(err)
1444 var commitPath = [repo.id, 'commit', commit.id]
1445 var treePath = [repo.id, 'tree', commit.id]
1446 cb(null, cat([pull.once(
1447 '<h3>' + link(commitPath, 'Commit ' + rev) + '</h3>' +
1448 '<section class="collapse">' +
1449 '<div class="right-bar">' +
1450 link(treePath, 'Browse Files') +
1451 '</div>' +
1452 '<h4>' + escapeHTML(commit.title) + '</h4>' +
1453 (commit.body ? pre(commit.body) : '') +
1454 (commit.separateAuthor ? escapeHTML(commit.author.name) +
1455 ' authored on ' + commit.author.date.toLocaleString() + '<br>'
1456 : '') +
1457 escapeHTML(commit.committer.name) + ' committed on ' +
1458 commit.committer.date.toLocaleString() + '<br/>' +
1459 commit.parents.map(function (id) {
1460 return 'Parent: ' + link([repo.id, 'commit', id], id)
1461 }).join('<br>') +
1462 '</section>' +
1463 '<section><h3>Files changed</h3>'),
1464 // TODO: show diff from all parents (merge commits)
1465 renderDiffStat([repo, repo], [commit.parents[0], commit.id]),
1466 pull.once('</section>')
1467 ]))
1468 })
1469 })
1470 ]))
1471 }
1472
1473 /* Diff stat */
1474
1475 function renderDiffStat(repos, treeIds) {
1476 if (treeIds.length == 0) treeIds = [null]
1477 var id = treeIds[0]
1478 var lastI = treeIds.length - 1
1479 var oldTree = treeIds[0]
1480 var changedFiles = []
1481 return cat([
1482 pull(
1483 Repo.diffTrees(repos, treeIds, true),
1484 pull.map(function (item) {
1485 var filename = escapeHTML(item.filename = item.path.join('/'))
1486 var oldId = item.id && item.id[0]
1487 var newId = item.id && item.id[lastI]
1488 var oldMode = item.mode && item.mode[0]
1489 var newMode = item.mode && item.mode[lastI]
1490 var action =
1491 !oldId && newId ? 'added' :
1492 oldId && !newId ? 'deleted' :
1493 oldMode != newMode ?
1494 'changed mode from ' + oldMode.toString(8) +
1495 ' to ' + newMode.toString(8) :
1496 'changed'
1497 if (item.id)
1498 changedFiles.push(item)
1499 var commitId = item.id[lastI] ? id : treeIds.filter(Boolean)[0]
1500 var blobsPath = treeIds[1]
1501 ? [repos[1].id, 'blob', treeIds[1]]
1502 : [repos[0].id, 'blob', treeIds[0]]
1503 var rawsPath = treeIds[1]
1504 ? [repos[1].id, 'raw', treeIds[1]]
1505 : [repos[0].id, 'raw', treeIds[0]]
1506 item.blobPath = blobsPath.concat(item.path)
1507 item.rawPath = rawsPath.concat(item.path)
1508 var fileHref = item.id ?
1509 '#' + encodeURIComponent(item.path.join('/')) :
1510 encodeLink(item.blobPath)
1511 return ['<a href="' + fileHref + '">' + filename + '</a>', action]
1512 }),
1513 table()
1514 ),
1515 pull(
1516 pull.values(changedFiles),
1517 paramap(function (item, cb) {
1518 var extension = getExtension(item.filename)
1519 if (extension in imgMimes) {
1520 var filename = escapeHTML(item.filename)
1521 return cb(null,
1522 '<pre><table class="code">' +
1523 '<tr><th id="' + escapeHTML(item.filename) + '">' +
1524 filename + '</th></tr>' +
1525 '<tr><td><img src="' + encodeLink(item.rawPath) + '"' +
1526 ' alt="' + filename + '"/></td></tr>' +
1527 '</table></pre>')
1528 }
1529 var done = multicb({ pluck: 1, spread: true })
1530 getRepoObjectString(repos[0], item.id[0], done())
1531 getRepoObjectString(repos[1], item.id[lastI], done())
1532 done(function (err, strOld, strNew) {
1533 if (err) return cb(err)
1534 cb(null, htmlLineDiff(item.filename, item.filename,
1535 strOld, strNew,
1536 encodeLink(item.blobPath)))
1537 })
1538 }, 4)
1539 )
1540 ])
1541 }
1542
1543 function htmlLineDiff(filename, anchor, oldStr, newStr, blobHref) {
1544 var diff = JsDiff.structuredPatch('', '', oldStr, newStr)
1545 var groups = diff.hunks.map(function (hunk) {
1546 var oldLine = hunk.oldStart
1547 var newLine = hunk.newStart
1548 var header = '<tr class="diff-hunk-header"><td colspan=2></td><td>' +
1549 '@@ -' + oldLine + ',' + hunk.oldLines + ' ' +
1550 '+' + newLine + ',' + hunk.newLines + ' @@' +
1551 '</td></tr>'
1552 return [header].concat(hunk.lines.map(function (line) {
1553 var s = line[0]
1554 if (s == '\\') return
1555 var html = highlight(line, getExtension(filename))
1556 var trClass = s == '+' ? 'diff-new' : s == '-' ? 'diff-old' : ''
1557 var lineNums = [s == '+' ? '' : oldLine++, s == '-' ? '' : newLine++]
1558 var id = [filename].concat(lineNums).join('-')
1559 return '<tr id="' + escapeHTML(id) + '" class="' + trClass + '">' +
1560 lineNums.map(function (num) {
1561 return '<td class="code-linenum">' +
1562 (num ? '<a href="#' + encodeURIComponent(id) + '">' +
1563 num + '</a>' : '') + '</td>'
1564 }).join('') +
1565 '<td class="code-text">' + html + '</td></tr>'
1566 }))
1567 })
1568 return '<pre><table class="code">' +
1569 '<tr><th colspan=3 id="' + escapeHTML(anchor) + '">' + filename +
1570 '<span class="right-bar">' +
1571 '<a href="' + blobHref + '">View</a> ' +
1572 '</span></th></tr>' +
1573 [].concat.apply([], groups).join('') +
1574 '</table></pre>'
1575 }
1576
1577 /* An unknown message linking to a repo */
1578
1579 function serveRepoSomething(req, repo, id, msg, path) {
1580 return renderRepoPage(repo, null, null,
1581 pull.once('<section><h3>' + link([id]) + '</h3>' +
1582 json(msg) + '</section>'))
1583 }
1584
1585 /* Repo update */
1586
1587 function objsArr(objs) {
1588 return Array.isArray(objs) ? objs :
1589 Object.keys(objs).map(function (sha1) {
1590 var obj = Object.create(objs[sha1])
1591 obj.sha1 = sha1
1592 return obj
1593 })
1594 }
1595
1596 function serveRepoUpdate(req, repo, id, msg, path) {
1597 var raw = req._u.query.raw != null
1598
1599 if (raw)
1600 return renderRepoPage(repo, 'activity', null, pull.once(
1601 '<a href="?" class="raw-link header-align">Info</a>' +
1602 '<h3>Update</h3>' +
1603 '<section class="collapse">' + json({key: id, value: msg}) + '</section>'))
1604
1605 // convert packs to old single-object style
1606 if (msg.content.indexes) {
1607 for (var i = 0; i < msg.content.indexes.length; i++) {
1608 msg.content.packs[i] = {
1609 pack: {link: msg.content.packs[i].link},
1610 idx: msg.content.indexes[i]
1611 }
1612 }
1613 }
1614
1615 return renderRepoPage(repo, 'activity', null, cat([
1616 pull.once(
1617 '<a href="?raw" class="raw-link header-align">Data</a>' +
1618 '<h3>Update</h3>' +
1619 renderRepoUpdate(repo, {key: id, value: msg}, true) +
1620 (msg.content.objects ? '<h3>Objects</h3>' +
1621 objsArr(msg.content.objects).map(renderObject).join('\n') : '') +
1622 (msg.content.packs ? '<h3>Packs</h3>' +
1623 msg.content.packs.map(renderPack).join('\n') : '')),
1624 cat(!msg.content.packs ? [] : [
1625 pull.once('<h3>Commits</h3>'),
1626 pull(
1627 pull.values(msg.content.packs),
1628 pull.asyncMap(function (pack, cb) {
1629 var done = multicb({ pluck: 1, spread: true })
1630 getBlob(pack.pack.link, done())
1631 getBlob(pack.idx.link, done())
1632 done(function (err, readPack, readIdx) {
1633 if (err) return cb(renderError(err))
1634 cb(null, gitPack.decodeWithIndex(repo, readPack, readIdx))
1635 })
1636 }),
1637 pull.flatten(),
1638 pull.asyncMap(function (obj, cb) {
1639 if (obj.type == 'commit')
1640 Repo.getCommitParsed(obj, cb)
1641 else
1642 pull(obj.read, pull.drain(null, cb))
1643 }),
1644 pull.filter(),
1645 pull.map(function (commit) {
1646 return renderCommit(repo, commit)
1647 })
1648 )
1649 ])
1650 ]))
1651 }
1652
1653 function renderObject(obj) {
1654 return '<section class="collapse">' +
1655 obj.type + ' ' + link([obj.link], obj.sha1) + '<br>' +
1656 obj.length + ' bytes' +
1657 '</section>'
1658 }
1659
1660 function renderPack(info) {
1661 return '<section class="collapse">' +
1662 (info.pack ? 'Pack: ' + link([info.pack.link]) + '<br>' : '') +
1663 (info.idx ? 'Index: ' + link([info.idx.link]) : '') + '</section>'
1664 }
1665
1666 /* Blob */
1667
1668 function serveRepoBlob(repo, rev, path) {
1669 return readNext(function (cb) {
1670 repo.getFile(rev, path, function (err, object) {
1671 if (err) return cb(null, serveBlobNotFound(repo.id, err))
1672 var type = repo.isCommitHash(rev) ? 'Tree' : 'Branch'
1673 var pathLinks = path.length === 0 ? '' :
1674 ': ' + linkPath([repo.id, 'tree'], [rev].concat(path))
1675 var rawFilePath = [repo.id, 'raw', rev].concat(path)
1676 var dirPath = path.slice(0, path.length-1)
1677 var filename = path[path.length-1]
1678 var extension = getExtension(filename)
1679 cb(null, renderRepoPage(repo, 'code', rev, cat([
1680 pull.once('<section><form action="" method="get">' +
1681 '<h3>' + type + ': ' + rev + ' '),
1682 revMenu(repo, rev),
1683 pull.once('</h3></form>'),
1684 type == 'Branch' && renderRepoLatest(repo, rev),
1685 pull.once('</section><section class="collapse">' +
1686 '<h3>Files' + pathLinks + '</h3>' +
1687 '<div>' + object.length + ' bytes' +
1688 '<span class="raw-link">' + link(rawFilePath, 'Raw') + '</span>' +
1689 '</div></section>' +
1690 '<section>'),
1691 extension in imgMimes
1692 ? pull.once('<img src="' + encodeLink(rawFilePath) +
1693 '" alt="' + escapeHTML(filename) + '" />')
1694 : renderObjectData(object, filename, repo, rev, dirPath),
1695 pull.once('</section>')
1696 ])))
1697 })
1698 })
1699 }
1700
1701 function serveBlobNotFound(repoId, err) {
1702 return serveTemplate('Blob not found', 404, pull.values([
1703 '<h2>Blob not found</h2>',
1704 '<p>Blob in repo ' + link([repoId]) + ' was not found</p>',
1705 '<pre>' + escapeHTML(err.stack) + '</pre>'
1706 ]))
1707 }
1708
1709 /* Raw blob */
1710
1711 function serveRepoRaw(repo, branch, path) {
1712 return readNext(function (cb) {
1713 repo.getFile(branch, path, function (err, object) {
1714 if (err) return cb(null, serveBuffer(404, 'Blob not found'))
1715 var extension = getExtension(path[path.length-1])
1716 var contentType = imgMimes[extension]
1717 cb(null, pull(object.read, serveRaw(object.length, contentType)))
1718 })
1719 })
1720 }
1721
1722 function serveRaw(length, contentType) {
1723 var inBody
1724 var headers = {
1725 'Content-Type': contentType || 'text/plain; charset=utf-8',
1726 'Cache-Control': 'max-age=31536000'
1727 }
1728 if (length != null)
1729 headers['Content-Length'] = length
1730 return function (read) {
1731 return function (end, cb) {
1732 if (inBody) return read(end, cb)
1733 if (end) return cb(true)
1734 cb(null, [200, headers])
1735 inBody = true
1736 }
1737 }
1738 }
1739
1740 function getBlob(key, cb) {
1741 ssb.blobs.want(key, function (err, got) {
1742 if (err) cb(err)
1743 else if (!got) cb(new Error('Missing blob ' + key))
1744 else cb(null, ssb.blobs.get(key))
1745 })
1746 }
1747
1748 function serveBlob(req, key) {
1749 getBlob(key, function (err, read) {
1750 if (err) cb(null, serveError(err))
1751 else if (!got) cb(null, serve404(req))
1752 else cb(null, serveRaw()(read))
1753 })
1754 }
1755
1756 /* Digs */
1757
1758 function serveRepoDigs(repo) {
1759 return readNext(function (cb) {
1760 getVotes(repo.id, function (err, votes) {
1761 cb(null, renderRepoPage(repo, null, null, cat([
1762 pull.once('<section><h3>Digs</h3>' +
1763 '<div>Total: ' + votes.upvotes + '</div>'),
1764 pull(
1765 pull.values(Object.keys(votes.upvoters)),
1766 paramap(function (feedId, cb) {
1767 about.getName(feedId, function (err, name) {
1768 if (err) return cb(err)
1769 cb(null, link([feedId], name))
1770 })
1771 }, 8),
1772 ul()
1773 ),
1774 pull.once('</section>')
1775 ])))
1776 })
1777 })
1778 }
1779
1780 /* Forks */
1781
1782 function getForks(repo, includeSelf) {
1783 return pull(
1784 cat([
1785 includeSelf && readOnce(function (cb) {
1786 getMsg(repo.id, function (err, value) {
1787 cb(err, value && {key: repo.id, value: value})
1788 })
1789 }),
1790 ssb.links({
1791 dest: repo.id,
1792 values: true,
1793 rel: 'upstream'
1794 })
1795 ]),
1796 pull.filter(function (msg) {
1797 return msg.value.content && msg.value.content.type == 'git-repo'
1798 }),
1799 paramap(function (msg, cb) {
1800 getRepoFullName(about, msg.value.author, msg.key,
1801 function (err, repoName, authorName) {
1802 if (err) return cb(err)
1803 cb(null, {
1804 key: msg.key,
1805 value: msg.value,
1806 repoName: repoName,
1807 authorName: authorName
1808 })
1809 })
1810 }, 8)
1811 )
1812 }
1813
1814 function serveRepoForks(repo) {
1815 var hasForks
1816 return renderRepoPage(repo, null, null, cat([
1817 pull.once('<h3>Forks</h3>'),
1818 pull(
1819 getForks(repo),
1820 pull.map(function (msg) {
1821 hasForks = true
1822 return '<section class="collapse">' +
1823 link([msg.value.author], msg.authorName) + ' / ' +
1824 link([msg.key], msg.repoName) +
1825 '<span class="right-bar">' +
1826 timestamp(msg.value.timestamp) +
1827 '</span></section>'
1828 })
1829 ),
1830 readOnce(function (cb) {
1831 cb(null, hasForks ? '' : 'No forks')
1832 })
1833 ]))
1834 }
1835
1836 /* Issues */
1837
1838 function serveRepoIssues(req, repo, path) {
1839 var numIssues = 0
1840 var state = req._u.query.state || 'open'
1841 return renderRepoPage(repo, 'issues', null, cat([
1842 pull.once(
1843 (isPublic ? '' :
1844 '<div class="right-bar">' + link([repo.id, 'issues', 'new'],
1845 '<button class="btn">&plus; New Issue</button>', true) +
1846 '</div>') +
1847 '<h3>Issues</h3>' +
1848 nav([
1849 ['?state=open', 'Open', 'open'],
1850 ['?state=closed', 'Closed', 'closed'],
1851 ['?state=all', 'All', 'all']
1852 ], state)),
1853 pull(
1854 issues.createFeedStream({ project: repo.id }),
1855 pull.filter(function (issue) {
1856 return state == 'all' ? true : (state == 'closed') == !issue.open
1857 }),
1858 pull.map(function (issue) {
1859 numIssues++
1860 var state = (issue.open ? 'open' : 'closed')
1861 return '<section class="collapse">' +
1862 '<i class="issue-state issue-state-' + state + '"' +
1863 ' title="' + ucfirst(state) + '">◼</i> ' +
1864 '<a href="' + encodeLink(issue.id) + '">' +
1865 escapeHTML(issue.title) +
1866 '<span class="right-bar">' +
1867 new Date(issue.created_at).toLocaleString() +
1868 '</span>' +
1869 '</a>' +
1870 '</section>'
1871 })
1872 ),
1873 readOnce(function (cb) {
1874 cb(null, numIssues > 0 ? '' : '<p>No issues</p>')
1875 })
1876 ]))
1877 }
1878
1879 /* Pull Requests */
1880
1881 function serveRepoPullReqs(req, repo) {
1882 var count = 0
1883 var state = req._u.query.state || 'open'
1884 return renderRepoPage(repo, 'pulls', null, cat([
1885 pull.once(
1886 (isPublic ? '' :
1887 '<div class="right-bar">' + link([repo.id, 'compare'],
1888 '<button class="btn">&plus; New Pull Request</button>', true) +
1889 '</div>') +
1890 '<h3>Pull Requests</h3>' +
1891 nav([
1892 ['?', 'Open', 'open'],
1893 ['?state=closed', 'Closed', 'closed'],
1894 ['?state=all', 'All', 'all']
1895 ], state)),
1896 pull(
1897 pullReqs.list({
1898 repo: repo.id,
1899 open: {open: true, closed: false}[state]
1900 }),
1901 pull.map(function (issue) {
1902 count++
1903 var state = (issue.open ? 'open' : 'closed')
1904 return '<section class="collapse">' +
1905 '<i class="issue-state issue-state-' + state + '"' +
1906 ' title="' + ucfirst(state) + '">◼</i> ' +
1907 '<a href="' + encodeLink(issue.id) + '">' +
1908 escapeHTML(issue.title) +
1909 '<span class="right-bar">' +
1910 new Date(issue.created_at).toLocaleString() +
1911 '</span>' +
1912 '</a>' +
1913 '</section>'
1914 })
1915 ),
1916 readOnce(function (cb) {
1917 cb(null, count > 0 ? '' : '<p>No pull requests</p>')
1918 })
1919 ]))
1920 }
1921
1922 /* New Issue */
1923
1924 function serveRepoNewIssue(repo, issueId, path) {
1925 return renderRepoPage(repo, 'issues', null, pull.once(
1926 '<h3>New Issue</h3>' +
1927 '<section><form action="" method="post">' +
1928 '<input type="hidden" name="action" value="new-issue">' +
1929 '<p><input class="wide-input" name="title" placeholder="Issue Title" size="77" /></p>' +
1930 renderPostForm(repo, 'Description', 8) +
1931 '<button type="submit" class="btn">Create</button>' +
1932 '</form></section>'))
1933 }
1934
1935 /* Issue */
1936
1937 function serveRepoIssue(req, repo, issue, path, postId) {
1938 var isAuthor = (myId == issue.author) || (myId == repo.feed)
1939 var newestMsg = {key: issue.id, value: {timestamp: issue.created_at}}
1940 return renderRepoPage(repo, 'issues', null, cat([
1941 pull.once(
1942 renderNameForm(!isPublic, issue.id, issue.title, 'issue-title', null,
1943 'Rename the issue',
1944 '<h3>' + link([issue.id], issue.title) + '</h3>') +
1945 '<code>' + issue.id + '</code>' +
1946 '<section class="collapse">' +
1947 (issue.open
1948 ? '<strong class="issue-status open">Open</strong>'
1949 : '<strong class="issue-status closed">Closed</strong>')),
1950 readOnce(function (cb) {
1951 about.getName(issue.author, function (err, authorName) {
1952 if (err) return cb(err)
1953 var authorLink = link([issue.author], authorName)
1954 cb(null, authorLink + ' opened this issue on ' +
1955 timestamp(issue.created_at))
1956 })
1957 }),
1958 pull.once('<hr/>' + markdown(issue.text, repo) + '</section>'),
1959 // render posts and edits
1960 pull(
1961 ssb.links({
1962 dest: issue.id,
1963 values: true
1964 }),
1965 pull.unique('key'),
1966 addAuthorName(about),
1967 sortMsgs(),
1968 pull.through(function (msg) {
1969 if (msg.value.timestamp > newestMsg.value.timestamp)
1970 newestMsg = msg
1971 }),
1972 pull.map(renderIssueActivityMsg.bind(null, repo, issue,
1973 'issue', postId))
1974 ),
1975 isPublic ? pull.empty() : readOnce(function (cb) {
1976 cb(null, renderIssueCommentForm(issue, repo, newestMsg.key, isAuthor,
1977 'issue'))
1978 })
1979 ]))
1980 }
1981
1982 function renderIssueActivityMsg(repo, issue, type, postId, msg) {
1983 var authorLink = link([msg.value.author], msg.authorName)
1984 var msgTimeLink = link([msg.key],
1985 new Date(msg.value.timestamp).toLocaleString(), false,
1986 'name="' + escapeHTML(msg.key) + '"')
1987 var c = msg.value.content
1988 switch (c.type) {
1989 case 'post':
1990 if (c.root == issue.id) {
1991 var changed = issues.isStatusChanged(msg, issue)
1992 return '<section class="collapse">' +
1993 (msg.key == postId ? '<div class="highlight">' : '') +
1994 authorLink +
1995 (changed == null ? '' : ' ' + (
1996 changed ? 'reopened this ' : 'closed this ') + type) +
1997 ' &middot; ' + msgTimeLink +
1998 (msg.key == postId ? '</div>' : '') +
1999 markdown(c.text, repo) +
2000 '</section>'
2001 } else {
2002 var text = c.text || (c.type + ' ' + msg.key)
2003 return '<section class="collapse mention-preview">' +
2004 authorLink + ' mentioned this issue in ' +
2005 '<a href="/' + msg.key + '#' + msg.key + '">' +
2006 String(text).substr(0, 140) + '</a>' +
2007 '</section>'
2008 }
2009 case 'issue':
2010 case 'pull-request':
2011 return '<section class="collapse mention-preview">' +
2012 authorLink + ' mentioned this ' + type + ' in ' +
2013 link([msg.key], String(c.title || msg.key).substr(0, 140)) +
2014 '</section>'
2015 case 'issue-edit':
2016 return '<section class="collapse">' +
2017 (msg.key == postId ? '<div class="highlight">' : '') +
2018 (c.title == null ? '' :
2019 authorLink + ' renamed this ' + type + ' to <q>' +
2020 escapeHTML(c.title) + '</q>') +
2021 ' &middot; ' + msgTimeLink +
2022 (msg.key == postId ? '</div>' : '') +
2023 '</section>'
2024 case 'git-update':
2025 var mention = issues.getMention(msg, issue)
2026 if (mention) {
2027 var commitLink = link([repo.id, 'commit', mention.object],
2028 mention.label || mention.object)
2029 return '<section class="collapse">' +
2030 authorLink + ' ' +
2031 (mention.open ? 'reopened this ' :
2032 'closed this ') + type +
2033 ' &middot; ' + msgTimeLink + '<br/>' +
2034 commitLink +
2035 '</section>'
2036 } else if ((mention = getMention(msg, issue.id))) {
2037 var commitLink = link(mention.object ?
2038 [repo.id, 'commit', mention.object] : [msg.key],
2039 mention.label || mention.object || msg.key)
2040 return '<section class="collapse">' +
2041 authorLink + ' mentioned this ' + type +
2042 ' &middot; ' + msgTimeLink + '<br/>' +
2043 commitLink +
2044 '</section>'
2045 } else {
2046 // fallthrough
2047 }
2048
2049 default:
2050 return '<section class="collapse">' +
2051 authorLink +
2052 ' &middot; ' + msgTimeLink +
2053 json(c) +
2054 '</section>'
2055 }
2056 }
2057
2058 function renderIssueCommentForm(issue, repo, branch, isAuthor, type) {
2059 return '<section><form action="" method="post">' +
2060 '<input type="hidden" name="action" value="comment">' +
2061 '<input type="hidden" name="id" value="' + issue.id + '">' +
2062 '<input type="hidden" name="issue" value="' + issue.id + '">' +
2063 '<input type="hidden" name="repo" value="' + repo.id + '">' +
2064 '<input type="hidden" name="branch" value="' + branch + '">' +
2065 renderPostForm(repo) +
2066 '<input type="submit" class="btn open" value="Comment" />' +
2067 (isAuthor ?
2068 '<input type="submit" class="btn"' +
2069 ' name="' + (issue.open ? 'close' : 'open') + '"' +
2070 ' value="' + (issue.open ? 'Close ' : 'Reopen ') + type + '"' +
2071 '/>' : '') +
2072 '</form></section>'
2073 }
2074
2075 /* Pull Request */
2076
2077 function serveRepoPullReq(req, repo, pr, path, postId) {
2078 var headRepo, authorLink
2079 var page = path[0] || 'activity'
2080 return renderRepoPage(repo, 'pulls', null, cat([
2081 pull.once('<div class="pull-request">' +
2082 renderNameForm(!isPublic, pr.id, pr.title, 'issue-title', null,
2083 'Rename the pull request',
2084 '<h3>' + link([pr.id], pr.title) + '</h3>') +
2085 '<code>' + pr.id + '</code>'),
2086 readOnce(function (cb) {
2087 var done = multicb({ pluck: 1, spread: true })
2088 about.getName(pr.author, done())
2089 var sameRepo = (pr.headRepo == pr.baseRepo)
2090 getRepo(pr.headRepo, function (err, headRepo) {
2091 if (err) return cb(err)
2092 done()(null, headRepo)
2093 getRepoName(about, headRepo.feed, headRepo.id, done())
2094 about.getName(headRepo.feed, done())
2095 })
2096
2097 done(function (err, issueAuthorName, _headRepo,
2098 headRepoName, headRepoAuthorName) {
2099 if (err) return cb(err)
2100 headRepo = _headRepo
2101 authorLink = link([pr.author], issueAuthorName)
2102 var repoLink = link([pr.headRepo], headRepoName)
2103 var headRepoAuthorLink = link([headRepo.feed], headRepoAuthorName)
2104 var headRepoLink = link([headRepo.id], headRepoName)
2105 var headBranchLink = link([headRepo.id, 'tree', pr.headBranch])
2106 var baseBranchLink = link([repo.id, 'tree', pr.baseBranch])
2107 cb(null, '<section class="collapse">' +
2108 (pr.open
2109 ? '<strong class="issue-status open">Open</strong>'
2110 : '<strong class="issue-status closed">Closed</strong>') +
2111 authorLink + ' wants to merge commits into ' +
2112 '<code>' + baseBranchLink + '</code> from ' +
2113 (sameRepo ? '<code>' + headBranchLink + '</code>' :
2114 '<code class="bgslash">' +
2115 headRepoAuthorLink + ' / ' +
2116 headRepoLink + ' / ' +
2117 headBranchLink + '</code>') +
2118 '</section>')
2119 })
2120 }),
2121 pull.once(
2122 nav([
2123 [[pr.id], 'Discussion', 'activity'],
2124 [[pr.id, 'commits'], 'Commits', 'commits'],
2125 [[pr.id, 'files'], 'Files', 'files']
2126 ], page)),
2127 readNext(function (cb) {
2128 if (page == 'commits') cb(null,
2129 renderPullReqCommits(pr, repo, headRepo))
2130 else if (page == 'files') cb(null,
2131 renderPullReqFiles(pr, repo, headRepo))
2132 else cb(null,
2133 renderPullReqActivity(pr, repo, headRepo, authorLink, postId))
2134 })
2135 ]))
2136 }
2137
2138 function renderPullReqCommits(pr, baseRepo, headRepo) {
2139 return cat([
2140 pull.once('<section>'),
2141 renderCommitLog(baseRepo, pr.baseBranch, headRepo, pr.headBranch),
2142 pull.once('</section>')
2143 ])
2144 }
2145
2146 function renderPullReqFiles(pr, baseRepo, headRepo) {
2147 return cat([
2148 pull.once('<section>'),
2149 renderDiffStat([baseRepo, headRepo], [pr.baseBranch, pr.headBranch]),
2150 pull.once('</section>')
2151 ])
2152 }
2153
2154 function renderPullReqActivity(pr, repo, headRepo, authorLink, postId) {
2155 var msgTimeLink = link([pr.id], new Date(pr.created_at).toLocaleString())
2156 var newestMsg = {key: pr.id, value: {timestamp: pr.created_at}}
2157 var isAuthor = (myId == pr.author) || (myId == repo.feed)
2158 return cat([
2159 readOnce(function (cb) {
2160 cb(null,
2161 '<section class="collapse">' +
2162 authorLink + ' &middot; ' + msgTimeLink +
2163 markdown(pr.text, repo) + '</section>')
2164 }),
2165 // render posts, edits, and updates
2166 pull(
2167 many([
2168 ssb.links({
2169 dest: pr.id,
2170 values: true
2171 }),
2172 readNext(function (cb) {
2173 cb(null, pull(
2174 ssb.links({
2175 dest: headRepo.id,
2176 source: headRepo.feed,
2177 rel: 'repo',
2178 values: true,
2179 reverse: true
2180 }),
2181 pull.take(function (link) {
2182 return link.value.timestamp > pr.created_at
2183 }),
2184 pull.filter(function (link) {
2185 return link.value.content.type == 'git-update'
2186 && ('refs/heads/' + pr.headBranch) in link.value.content.refs
2187 })
2188 ))
2189 })
2190 ]),
2191 addAuthorName(about),
2192 pull.unique('key'),
2193 pull.through(function (msg) {
2194 if (msg.value.timestamp > newestMsg.value.timestamp)
2195 newestMsg = msg
2196 }),
2197 sortMsgs(),
2198 pull.map(function (item) {
2199 if (item.value.content.type == 'git-update')
2200 return renderBranchUpdate(pr, item)
2201 return renderIssueActivityMsg(repo, pr,
2202 'pull request', postId, item)
2203 })
2204 ),
2205 !isPublic && isAuthor && pull.once(
2206 '<section class="merge-instructions">' +
2207 '<input type="checkbox" class="toggle" id="merge-instructions"/>' +
2208 '<h4><label for="merge-instructions" class="toggle-link"><a>' +
2209 'Merge via command line…' +
2210 '</a></label></h4>' +
2211 '<div class="contents">' +
2212 '<p>Check out the branch and test the changes:</p>' +
2213 '<pre>' +
2214 'git fetch ssb://' + escapeHTML(pr.headRepo) + ' ' +
2215 escapeHTML(pr.headBranch) + '\n' +
2216 'git checkout -b ' + escapeHTML(pr.headBranch) + ' FETCH_HEAD' +
2217 '</pre>' +
2218 '<p>Merge the changes and push to update the base branch:</p>' +
2219 '<pre>' +
2220 'git checkout ' + escapeHTML(pr.baseBranch) + '\n' +
2221 'git merge ' + escapeHTML(pr.headBranch) + '\n' +
2222 'git push ssb ' + escapeHTML(pr.baseBranch) +
2223 '</pre>' +
2224 '</div></section>'),
2225 !isPublic && readOnce(function (cb) {
2226 cb(null, renderIssueCommentForm(pr, repo, newestMsg.key, isAuthor,
2227 'pull request'))
2228 })
2229 ])
2230 }
2231
2232 function renderBranchUpdate(pr, msg) {
2233 var authorLink = link([msg.value.author], msg.authorName)
2234 var msgLink = link([msg.key],
2235 new Date(msg.value.timestamp).toLocaleString())
2236 var rev = msg.value.content.refs['refs/heads/' + pr.headBranch]
2237 if (!rev)
2238 return '<section class="collapse">' +
2239 authorLink + ' deleted the <code>' + pr.headBranch + '</code> branch' +
2240 ' &middot; ' + msgLink +
2241 '</section>'
2242
2243 var revLink = link([pr.headRepo, 'commit', rev], rev.substr(0, 8))
2244 return '<section class="collapse">' +
2245 authorLink + ' updated the branch to <code>' + revLink + '</code>' +
2246 ' &middot; ' + msgLink +
2247 '</section>'
2248 }
2249
2250 /* Compare changes */
2251
2252 function serveRepoCompare(req, repo) {
2253 var query = req._u.query
2254 var base
2255 var count = 0
2256
2257 return renderRepoPage(repo, 'pulls', null, cat([
2258 pull.once('<h3>Compare changes</h3>' +
2259 '<form action="' + encodeLink(repo.id) + '/comparing" method="get">' +
2260 '<section>'),
2261 pull.once('Base branch: '),
2262 readNext(function (cb) {
2263 if (query.base) gotBase(null, query.base)
2264 else repo.getSymRef('HEAD', true, gotBase)
2265 function gotBase(err, ref) {
2266 if (err) return cb(err)
2267 cb(null, branchMenu(repo, 'base', base = ref || 'HEAD'))
2268 }
2269 }),
2270 pull.once('<br/>Comparison repo/branch:'),
2271 pull(
2272 getForks(repo, true),
2273 pull.asyncMap(function (msg, cb) {
2274 getRepo(msg.key, function (err, repo) {
2275 if (err) return cb(err)
2276 cb(null, {
2277 msg: msg,
2278 repo: repo
2279 })
2280 })
2281 }),
2282 pull.map(renderFork),
2283 pull.flatten()
2284 ),
2285 pull.once('</section>'),
2286 readOnce(function (cb) {
2287 cb(null, count == 0 ? 'No branches to compare!' :
2288 '<button type="submit" class="btn">Compare</button>')
2289 }),
2290 pull.once('</form>')
2291 ]))
2292
2293 function renderFork(fork) {
2294 return pull(
2295 fork.repo.refs(),
2296 pull.map(function (ref) {
2297 var m = /^refs\/([^\/]*)\/(.*)$/.exec(ref.name) || [,ref.name]
2298 return {
2299 type: m[1],
2300 name: m[2],
2301 value: ref.value
2302 }
2303 }),
2304 pull.filter(function (ref) {
2305 return ref.type == 'heads'
2306 && !(ref.name == base && fork.msg.key == repo.id)
2307 }),
2308 pull.map(function (ref) {
2309 var branchLink = link([fork.msg.key, 'tree', ref.name], ref.name)
2310 var authorLink = link([fork.msg.value.author], fork.msg.authorName)
2311 var repoLink = link([fork.msg.key], fork.msg.repoName)
2312 var value = fork.msg.key + ':' + ref.name
2313 count++
2314 return '<div class="bgslash">' +
2315 '<input type="radio" name="head"' +
2316 ' value="' + escapeHTML(value) + '"' +
2317 (query.head == value ? ' checked="checked"' : '') + '> ' +
2318 authorLink + ' / ' + repoLink + ' / ' + branchLink + '</div>'
2319 })
2320 )
2321 }
2322 }
2323
2324 function serveRepoComparing(req, repo) {
2325 var query = req._u.query
2326 var baseBranch = query.base
2327 var s = (query.head || '').split(':')
2328
2329 if (!s || !baseBranch)
2330 return serveRedirect(encodeLink([repo.id, 'compare']))
2331
2332 var headRepoId = s[0]
2333 var headBranch = s[1]
2334 var baseLink = link([repo.id, 'tree', baseBranch])
2335 var headBranchLink = link([headRepoId, 'tree', headBranch])
2336 var backHref = encodeLink([repo.id, 'compare']) + req._u.search
2337
2338 return renderRepoPage(repo, 'pulls', null, cat([
2339 pull.once('<h3>' +
2340 (query.expand ? 'Open a pull request' : 'Comparing changes') +
2341 '</h3>'),
2342 readNext(function (cb) {
2343 getRepo(headRepoId, function (err, headRepo) {
2344 if (err) return cb(err)
2345 getRepoFullName(about, headRepo.feed, headRepo.id,
2346 function (err, repoName, authorName) {
2347 if (err) return cb(err)
2348 cb(null, renderRepoInfo(Repo(headRepo), repoName, authorName))
2349 }
2350 )
2351 })
2352 })
2353 ]))
2354
2355 function renderRepoInfo(headRepo, headRepoName, headRepoAuthorName) {
2356 var authorLink = link([headRepo.feed], headRepoAuthorName)
2357 var repoLink = link([headRepoId], headRepoName)
2358 return cat([
2359 pull.once('<section>' +
2360 'Base: ' + baseLink + '<br/>' +
2361 'Head: <span class="bgslash">' + authorLink + ' / ' + repoLink +
2362 ' / ' + headBranchLink + '</span>' +
2363 '</section>' +
2364 (query.expand ? '<section><form method="post" action="">' +
2365 hiddenInputs({
2366 action: 'new-pull',
2367 branch: baseBranch,
2368 head_repo: headRepoId,
2369 head_branch: headBranch
2370 }) +
2371 '<input class="wide-input" name="title"' +
2372 ' placeholder="Title" size="77"/>' +
2373 renderPostForm(repo, 'Description', 8) +
2374 '<button type="submit" class="btn open">Create</button>' +
2375 '</form></section>'
2376 : '<section><form method="get" action="">' +
2377 hiddenInputs({
2378 base: baseBranch,
2379 head: query.head
2380 }) +
2381 '<button class="btn open" type="submit" name="expand" value="1">' +
2382 '<i>⎇</i> Create pull request</button> ' +
2383 '<a href="' + backHref + '">Back</a>' +
2384 '</form></section>') +
2385 '<div id="commits"></div>' +
2386 '<div class="tab-links">' +
2387 '<a href="#" id="files-link">Files changed</a> ' +
2388 '<a href="#commits" id="commits-link">Commits</a>' +
2389 '</div>' +
2390 '<section id="files-tab">'),
2391 renderDiffStat([repo, headRepo], [baseBranch, headBranch]),
2392 pull.once('</section>' +
2393 '<section id="commits-tab">'),
2394 renderCommitLog(repo, baseBranch, headRepo, headBranch),
2395 pull.once('</section>')
2396 ])
2397 }
2398 }
2399
2400 function renderCommitLog(baseRepo, baseBranch, headRepo, headBranch) {
2401 return cat([
2402 pull.once('<table class="compare-commits">'),
2403 readNext(function (cb) {
2404 baseRepo.resolveRef(baseBranch, function (err, baseBranchRev) {
2405 if (err) return cb(err)
2406 var currentDay
2407 return cb(null, pull(
2408 headRepo.readLog(headBranch),
2409 pull.take(function (rev) { return rev != baseBranchRev }),
2410 pullReverse(),
2411 paramap(headRepo.getCommitParsed.bind(headRepo), 8),
2412 pull.map(function (commit) {
2413 var commitPath = [baseRepo.id, 'commit', commit.id]
2414 var commitIdShort = '<tt>' + commit.id.substr(0, 8) + '</tt>'
2415 var day = Math.floor(commit.author.date / 86400000)
2416 var dateRow = day == currentDay ? '' :
2417 '<tr><th colspan=3 class="date-info">' +
2418 commit.author.date.toLocaleDateString() +
2419 '</th><tr>'
2420 currentDay = day
2421 return dateRow + '<tr>' +
2422 '<td>' + escapeHTML(commit.author.name) + '</td>' +
2423 '<td>' + link(commitPath, commit.title) + '</td>' +
2424 '<td>' + link(commitPath, commitIdShort, true) + '</td>' +
2425 '</tr>'
2426 })
2427 ))
2428 })
2429 }),
2430 pull.once('</table>')
2431 ])
2432 }
2433}
2434

Built with git-ssb-web