git ssb

30+

cel / git-ssb-web



Commit dd4e83623a7471b95f8faa0bd8c16453c6b9f6f5

Refactor

Close %MF5xNDxiqidFUpmTO3ZoEQ0s/5shkodQLho6zWijE/E=.sha256
Charles Lehner committed on 4/22/2016, 2:59:35 AM
Parent: b8aa0f8352d62cc90e2ab57a3b56f4925e06ed9b

Files changed

index.jschanged
about.jsdeleted
i18n.jsdeleted
lib/about.jsadded
lib/i18n.jsadded
lib/markdown.jsadded
lib/paginate.jsadded
lib/repos/index.jsadded
lib/repos/issues.jsadded
lib/repos/pulls.jsadded
lib/users.jsadded
lib/util.jsadded
lib/votes.jsadded
votes.jsdeleted
index.jsView
@@ -8,107 +8,23 @@
88 var pull = require('pull-stream')
99 var ssbGit = require('ssb-git-repo')
1010 var toPull = require('stream-to-pull-stream')
1111 var cat = require('pull-cat')
12-var Repo = require('pull-git-repo')
13-var ssbAbout = require('./about')
14-var ssbVotes = require('./votes')
15-var i18n = require('./i18n')
16-var marked = require('ssb-marked')
12 +var GitRepo = require('pull-git-repo')
13 +var u = require('./lib/util')
14 +var markdown = require('./lib/markdown')
15 +var paginate = require('./lib/paginate')
1716 var asyncMemo = require('asyncmemo')
1817 var multicb = require('multicb')
1918 var schemas = require('ssb-msg-schemas')
2019 var Issues = require('ssb-issues')
2120 var PullRequests = require('ssb-pull-requests')
2221 var paramap = require('pull-paramap')
23-var gitPack = require('pull-git-pack')
2422 var Mentions = require('ssb-mentions')
25-var Highlight = require('highlight.js')
26-var JsDiff = require('diff')
2723 var many = require('pull-many')
2824
2925 var hlCssPath = path.resolve(require.resolve('highlight.js'), '../../styles')
3026
31-// render links to git objects and ssb objects
32-var blockRenderer = new marked.Renderer()
33-blockRenderer.urltransform = function (url) {
34- if (ref.isLink(url))
35- return encodeLink(url)
36- if (/^[0-9a-f]{40}$/.test(url) && this.options.repo)
37- return encodeLink([this.options.repo.id, 'commit', url])
38- return url
39-}
40-
41-blockRenderer.image = function (href, title, text) {
42- href = href.replace(/^&/, '&')
43- var url
44- if (ref.isBlobId(href))
45- url = encodeLink(href)
46- else if (/^https?:\/\//.test(href))
47- url = href
48- else if (this.options.repo && this.options.rev && this.options.path)
49- url = path.join('/', encodeURIComponent(this.options.repo.id),
50- 'raw', this.options.rev, this.options.path.join('/'), href)
51- else
52- return text
53- return '<img src="' + escapeHTML(url) + '" alt="' + text + '"' +
54- (title ? ' title="' + title + '"' : '') + '/>'
55-}
56-
57-blockRenderer.mention = function (preceding, id) {
58- // prevent broken name mention
59- if (id[0] == '@' && !ref.isFeed(id))
60- return (preceding||'') + escapeHTML(id)
61-
62- return marked.Renderer.prototype.mention.call(this, preceding, id)
63-}
64-
65-function getExtension(filename) {
66- return (/\.([^.]+)$/.exec(filename) || [,filename])[1]
67-}
68-
69-function highlight(code, lang) {
70- try {
71- return lang
72- ? Highlight.highlight(lang, code).value
73- : Highlight.highlightAuto(code).value
74- } catch(e) {
75- if (/^Unknown language/.test(e.message))
76- return escapeHTML(code)
77- throw e
78- }
79-}
80-
81-marked.setOptions({
82- gfm: true,
83- mentions: true,
84- tables: true,
85- breaks: true,
86- pedantic: false,
87- sanitize: true,
88- smartLists: true,
89- smartypants: false,
90- highlight: highlight,
91- renderer: blockRenderer
92-})
93-
94-// hack to make git link mentions work
95-var mdRules = new marked.InlineLexer(1, marked.defaults).rules
96-mdRules.mention =
97- /^(\s)?([@%&][A-Za-z0-9\._\-+=\/]*[A-Za-z0-9_\-+=\/]|[0-9a-f]{40})/
98-mdRules.text = /^[\s\S]+?(?=[\\<!\[_*`]| {2,}\n| [@%&]|[0-9a-f]{40}|$)/
99-
100-function markdown(text, options, cb) {
101- if (!text) return ''
102- if (typeof text != 'string') text = String(text)
103- if (!options) options = {}
104- else if (options.id) options = {repo: options}
105- if (!options.rev) options.rev = 'HEAD'
106- if (!options.path) options.path = []
107-
108- return marked(text, options, cb)
109-}
110-
11127 function ParamError(msg) {
11228 var err = Error.call(this, msg)
11329 err.name = ParamError.name
11430 return err
@@ -122,256 +38,8 @@
12238 if (isNaN(str)) return {host: str, port: def.port}
12339 return {host: def.host, port: str}
12440 }
12541
126-function isArray(arr) {
127- return Object.prototype.toString.call(arr) == '[object Array]'
128-}
129-
130-function encodeLink(url) {
131- if (!isArray(url)) url = [url]
132- return '/' + url.map(encodeURIComponent).join('/')
133-}
134-
135-function link(parts, text, raw, props) {
136- if (text == null) text = parts[parts.length-1]
137- if (!raw) text = escapeHTML(text)
138- return '<a href="' + encodeLink(parts) + '"' +
139- (props ? ' ' + props : '') +
140- '>' + text + '</a>'
141-}
142-
143-function linkify(text) {
144- // regex is from ssb-ref
145- return text.replace(/(@|%|&|&amp;)[A-Za-z0-9\/+]{43}=\.[\w\d]+/g, function (str) {
146- return '<a href="/' + encodeURIComponent(str) + '">' + str + '</a>'
147- })
148-}
149-
150-function timestamp(time, req) {
151- time = Number(time)
152- var d = new Date(time)
153- return '<span title="' + time + '">' +
154- d.toLocaleString(req._locale) + '</span>'
155-}
156-
157-function pre(text) {
158- return '<pre>' + escapeHTML(text) + '</pre>'
159-}
160-
161-function json(obj) {
162- return linkify(pre(JSON.stringify(obj, null, 2)))
163-}
164-
165-function escapeHTML(str) {
166- return String(str)
167- .replace(/&/g, '&amp;')
168- .replace(/</g, '&lt;')
169- .replace(/>/g, '&gt;')
170- .replace(/"/g, '&quot;')
171-}
172-
173-function table(props) {
174- return function (read) {
175- return cat([
176- pull.once('<table' + (props ? ' ' + props : '') + '>'),
177- pull(
178- read,
179- pull.map(function (row) {
180- return row ? '<tr>' + row.map(function (cell) {
181- return '<td>' + cell + '</td>'
182- }).join('') + '</tr>' : ''
183- })
184- ),
185- pull.once('</table>')
186- ])
187- }
188-}
189-
190-function ul(props) {
191- return function (read) {
192- return cat([
193- pull.once('<ul' + (props ? ' ' + props : '') + '>'),
194- pull(
195- read,
196- pull.map(function (li) {
197- return '<li>' + li + '</li>'
198- })
199- ),
200- pull.once('</ul>')
201- ])
202- }
203-}
204-
205-function nav(links, page, after) {
206- return ['<nav>'].concat(
207- links.map(function (link) {
208- var href = typeof link[0] == 'string' ? link[0] : encodeLink(link[0])
209- var props = link[2] == page ? ' class="active"' : ''
210- return '<a href="' + href + '"' + props + '>' + link[1] + '</a>'
211- }), after || '', '</nav>').join('')
212-}
213-
214-function renderNameForm(req, enabled, id, name, action, inputId, title, header) {
215- if (!inputId) inputId = action
216- return '<form class="petname" action="" method="post">' +
217- (enabled ?
218- '<input type="checkbox" class="name-checkbox" id="' + inputId + '" ' +
219- 'onfocus="this.form.name.focus()" />' +
220- '<input name="name" class="name" value="' + escapeHTML(name) + '" ' +
221- 'onkeyup="if (event.keyCode == 27) this.form.reset()" />' +
222- '<input type="hidden" name="action" value="' + action + '">' +
223- '<input type="hidden" name="id" value="' +
224- escapeHTML(id) + '">' +
225- '<label class="name-toggle" for="' + inputId + '" ' +
226- 'title="' + title + '"><i>✍</i></label> ' +
227- '<input class="btn name-btn" type="submit" value="' +
228- req._t('Rename') + '">' +
229- header :
230- header + '<br clear="all"/>'
231- ) +
232- '</form>'
233-}
234-
235-function renderPostForm(req, repo, placeholder, rows) {
236- return '<input type="radio" class="tab-radio" id="tab1" name="tab" checked="checked"/>' +
237- '<input type="radio" class="tab-radio" id="tab2" name="tab"/>' +
238- '<div id="tab-links" class="tab-links" style="display:none">' +
239- '<label for="tab1" id="write-tab-link" class="tab1-link">' +
240- req._t('post.Write') + '</label>' +
241- '<label for="tab2" id="preview-tab-link" class="tab2-link">' +
242- req._t('post.Preview') + '</label>' +
243- '</div>' +
244- '<input type="hidden" id="repo-id" value="' + repo.id + '"/>' +
245- '<div id="write-tab" class="tab1">' +
246- '<textarea id="post-text" name="text" class="wide-input"' +
247- ' rows="' + (rows||4) + '" cols="77"' +
248- (placeholder ? ' placeholder="' + placeholder + '"' : '') +
249- '></textarea>' +
250- '</div>' +
251- '<div class="preview-text tab2" id="preview-tab"></div>' +
252- '<script>' + issueCommentScript + '</script>'
253-}
254-
255-function hiddenInputs(values) {
256- return Object.keys(values).map(function (key) {
257- return '<input type="hidden"' +
258- ' name="' + escapeHTML(key) + '"' +
259- ' value="' + escapeHTML(values[key]) + '"/>'
260- }).join('')
261-}
262-
263-function readNext(fn) {
264- var next
265- return function (end, cb) {
266- if (next) return next(end, cb)
267- fn(function (err, _next) {
268- if (err) return cb(err)
269- next = _next
270- next(null, cb)
271- })
272- }
273-}
274-
275-function readOnce(fn) {
276- var ended
277- return function (end, cb) {
278- fn(function (err, data) {
279- if (err || ended) return cb(err || ended)
280- ended = true
281- cb(null, data)
282- })
283- }
284-}
285-
286-function paginate(onFirst, through, onLast, onEmpty) {
287- var ended, last, first = true, queue = []
288- return function (read) {
289- var mappedRead = through(function (end, cb) {
290- if (ended = end) return read(ended, cb)
291- if (queue.length)
292- return cb(null, queue.shift())
293- read(null, function (end, data) {
294- if (end) return cb(end)
295- last = data
296- cb(null, data)
297- })
298- })
299- return function (end, cb) {
300- var tmp
301- if (ended) return cb(ended)
302- if (ended = end) return read(ended, cb)
303- if (first)
304- return read(null, function (end, data) {
305- if (ended = end) {
306- if (end === true && onEmpty)
307- return onEmpty(cb)
308- return cb(ended)
309- }
310- first = false
311- last = data
312- queue.push(data)
313- if (onFirst)
314- onFirst(data, cb)
315- else
316- mappedRead(null, cb)
317- })
318- mappedRead(null, function (end, data) {
319- if (ended = end) {
320- if (end === true && last)
321- return onLast(last, cb)
322- }
323- cb(end, data)
324- })
325- }
326- }
327-}
328-
329-function readObjectString(obj, cb) {
330- pull(obj.read, pull.collect(function (err, bufs) {
331- if (err) return cb(err)
332- cb(null, Buffer.concat(bufs, obj.length).toString('utf8'))
333- }))
334-}
335-
336-function getRepoObjectString(repo, id, cb) {
337- if (!id) return cb(null, '')
338- repo.getObjectFromAny(id, function (err, obj) {
339- if (err) return cb(err)
340- readObjectString(obj, cb)
341- })
342-}
343-
344-function compareMsgs(a, b) {
345- return (a.value.timestamp - b.value.timestamp) || (a.key - b.key)
346-}
347-
348-function pullSort(comparator) {
349- return function (read) {
350- return readNext(function (cb) {
351- pull(read, pull.collect(function (err, items) {
352- if (err) return cb(err)
353- items.sort(comparator)
354- cb(null, pull.values(items))
355- }))
356- })
357- }
358-}
359-
360-function sortMsgs() {
361- return pullSort(compareMsgs)
362-}
363-
364-function pullReverse() {
365- return function (read) {
366- return readNext(function (cb) {
367- pull(read, pull.collect(function (err, items) {
368- cb(err, items && pull.values(items.reverse()))
369- }))
370- })
371- }
372-}
373-
37442 function tryDecodeURIComponent(str) {
37543 if (!str || (str[0] == '%' && ref.isBlobId(str)))
37644 return str
37745 try {
@@ -380,24 +48,11 @@
38048 return str
38149 }
38250 }
38351
384-function getMention(msg, id) {
385- if (msg.key == id) return msg
386- var mentions = msg.value.content.mentions
387- if (mentions) for (var i = 0; i < mentions.length; i++) {
388- var mention = mentions[i]
389- if (mention.link == id)
390- return mention
391- }
392- return null
393-}
394-
395-var hasOwnProp = Object.prototype.hasOwnProperty
396-
39752 function getContentType(filename) {
398- var ext = getExtension(filename)
399- return contentTypes[ext] || imgMimes[ext] || 'text/plain; charset=utf-8'
53 + var ext = u.getExtension(filename)
54 + return contentTypes[ext] || u.imgMimes[ext] || 'text/plain; charset=utf-8'
40055 }
40156
40257 var contentTypes = {
40358 css: 'text/css'
@@ -418,41 +73,15 @@
41873 })
41974 )
42075 }
42176
422-var issueCommentScript = '(' + function () {
423- var $ = document.getElementById.bind(document)
424- $('tab-links').style.display = 'block'
425- $('preview-tab-link').onclick = function (e) {
426- with (new XMLHttpRequest()) {
427- open('POST', '', true)
428- onload = function() {
429- $('preview-tab').innerHTML = responseText
430- }
431- send('action=markdown' +
432- '&repo=' + encodeURIComponent($('repo-id').value) +
433- '&text=' + encodeURIComponent($('post-text').value))
434- }
435- }
436-}.toString() + ')()'
437-
43877 var msgTypes = {
43978 'git-repo': true,
44079 'git-update': true,
44180 'issue': true,
44281 'pull-request': true
44382 }
44483
445-var imgMimes = {
446- png: 'image/png',
447- jpeg: 'image/jpeg',
448- jpg: 'image/jpeg',
449- gif: 'image/gif',
450- tif: 'image/tiff',
451- svg: 'image/svg+xml',
452- bmp: 'image/bmp'
453-}
454-
45584 var _httpServer
45685
45786 module.exports = {
45887 name: 'git-ssb-web',
@@ -477,9 +106,9 @@
477106 this.reconnect = reconnect
478107
479108 this.ssbAppname = config.appname || 'ssb'
480109 this.isPublic = config.public
481- this.getVotes = ssbVotes(ssb)
110 + this.getVotes = require('./lib/votes')(ssb)
482111 this.getMsg = asyncMemo(ssb.get)
483112 this.issues = Issues.init(ssb)
484113 this.pullReqs = PullRequests.init(ssb)
485114 this.getRepo = asyncMemo(function (id, cb) {
@@ -491,11 +120,15 @@
491120
492121 this.about = function (id, cb) { cb(null, {name: id}) }
493122 ssb.whoami(function (err, feed) {
494123 this.myId = feed.id
495- this.about = ssbAbout(ssb, this.myId)
124 + this.about = require('./lib/about')(ssb, this.myId)
496125 }.bind(this))
497126
127 + this.i18n = require('./lib/i18n')(path.join(__dirname, 'locale'), 'en')
128 + this.users = require('./lib/users')(this)
129 + this.repos = require('./lib/repos')(this)
130 +
498131 var webConfig = config['git-ssb-web'] || {}
499132 var addr = parseAddr(config.listenAddr, {
500133 host: webConfig.host || 'localhost',
501134 port: webConfig.port || 7718
@@ -563,9 +196,9 @@
563196 req._u = url.parse(req.url, true)
564197 var locale = req._u.query.locale ||
565198 (/locale=([^;]*)/.exec(req.headers.cookie) || [])[1]
566199 var reqLocales = req.headers['accept-language']
567- i18n.pickCatalog(reqLocales, locale, function (err, t) {
200 + this.i18n.pickCatalog(reqLocales, locale, function (err, t) {
568201 if (err) return pull(this.serveError(req, err, 500), serve(req, res))
569202 req._t = t
570203 req._locale = t.locale
571204 pull(this.handleRequest(req), serve(req, res))
@@ -589,9 +222,9 @@
589222 return this.serveBlob(req, dir)
590223 else if (ref.isMsgId(dir))
591224 return this.serveMessage(req, dir, dirs.slice(1))
592225 else if (ref.isFeedId(dir))
593- return this.serveUserPage(req, dir, dirs.slice(1))
226 + return this.users.serveUserPage(req, dir, dirs.slice(1))
594227 else if (dir == 'static')
595228 return this.serveFile(req, dirs)
596229 else if (dir == 'highlight')
597230 return this.serveFile(req, [hlCssPath].concat(dirs.slice(1)), true)
@@ -602,27 +235,27 @@
602235 G.handlePOST = function (req, dir) {
603236 var self = this
604237 if (self.isPublic)
605238 return self.serveBuffer(405, req._t('error.POSTNotAllowed'))
606- return readNext(function (cb) {
239 + return u.readNext(function (cb) {
607240 readReqForm(req, function (err, data) {
608241 if (err) return cb(null, self.serveError(req, err, 400))
609242 if (!data) return cb(null, self.serveError(req,
610243 new ParamError(req._t('error.MissingData')), 400))
611244
612245 switch (data.action) {
613246 case 'fork-prompt':
614247 return cb(null, self.serveRedirect(req,
615- encodeLink([data.id, 'fork'])))
248 + u.encodeLink([data.id, 'fork'])))
616249
617250 case 'fork':
618251 if (!data.id)
619252 return cb(null, self.serveError(req,
620253 new ParamError(req._t('error.MissingId')), 400))
621254 return ssbGit.createRepo(self.ssb, {upstream: data.id},
622255 function (err, repo) {
623256 if (err) return cb(null, self.serveError(req, err))
624- cb(null, self.serveRedirect(req, encodeLink(repo.id)))
257 + cb(null, self.serveRedirect(req, u.encodeLink(repo.id)))
625258 })
626259
627260 case 'vote':
628261 var voteValue = +data.value || 0
@@ -686,9 +319,9 @@
686319 if (mentions.length)
687320 msg.mentions = mentions
688321 return self.ssb.publish(msg, function (err, msg) {
689322 if (err) return cb(null, self.serveError(req, err))
690- cb(null, self.serveRedirect(req, encodeLink(msg.key)))
323 + cb(null, self.serveRedirect(req, u.encodeLink(msg.key)))
691324 })
692325
693326 case 'new-pull':
694327 var msg = PullRequests.schemas.new(dir, data.branch,
@@ -697,9 +330,9 @@
697330 if (mentions.length)
698331 msg.mentions = mentions
699332 return self.ssb.publish(msg, function (err, msg) {
700333 if (err) return cb(null, self.serveError(req, err))
701- cb(null, self.serveRedirect(req, encodeLink(msg.key)))
334 + cb(null, self.serveRedirect(req, u.encodeLink(msg.key)))
702335 })
703336
704337 case 'markdown':
705338 return cb(null, self.serveMarkdown(data.text, {id: data.repo}))
@@ -716,9 +349,9 @@
716349 // prevent escaping base dir
717350 if (!outside && filename.indexOf('../') === 0)
718351 return this.serveBuffer(403, req._t("error.403Forbidden"))
719352
720- return readNext(function (cb) {
353 + return u.readNext(function (cb) {
721354 fs.stat(filename, function (err, stats) {
722355 cb(null, err ?
723356 err.code == 'ENOENT' ? this.serve404(req)
724357 : this.serveBuffer(500, err.message)
@@ -756,9 +389,9 @@
756389 G.serveRedirect = function (req, path) {
757390 return this.serveBuffer(302,
758391 '<!doctype><html><head>' +
759392 '<title>' + req._t('Redirect') + '</title></head><body>' +
760- '<p><a href="' + escapeHTML(path) + '">' +
393 + '<p><a href="' + u.escape(path) + '">' +
761394 req._t('Continue') + '</a></p>' +
762395 '</body></html>', 'text/html; charset=utf-8', {Location: path})
763396 }
764397
@@ -766,24 +399,25 @@
766399 return this.serveBuffer(200, markdown(text, repo),
767400 'text/html; charset=utf-8')
768401 }
769402
770-function renderError(err, tag) {
403 +G.renderError = function (err, tag) {
771404 tag = tag || 'h3'
772405 return '<' + tag + '>' + err.name + '</' + tag + '>' +
773- '<pre>' + escapeHTML(err.stack) + '</pre>'
406 + '<pre>' + u.escape(err.stack) + '</pre>'
774407 }
775408
776409 function renderTry(read) {
410 + var self = this
777411 var ended
778412 return function (end, cb) {
779413 if (ended) return cb(ended)
780414 read(end, function (err, data) {
781415 if (err === true)
782416 cb(true)
783417 else if (err) {
784418 ended = true
785- cb(null, renderError(err, 'h3'))
419 + cb(null, self.renderError(err))
786420 } else
787421 cb(null, data)
788422 })
789423 }
@@ -791,9 +425,9 @@
791425
792426 G.serveTemplate = function (req, title, code, read) {
793427 if (read === undefined)
794428 return this.serveTemplate.bind(this, req, title, code)
795- var q = req._u.query.q && escapeHTML(req._u.query.q) || ''
429 + var q = req._u.query.q && u.escape(req._u.query.q) || ''
796430 var app = 'git ssb'
797431 var appName = this.ssbAppname
798432 if (req._t) app = req._t(app)
799433 return cat([
@@ -824,17 +458,17 @@
824458 G.serveError = function (req, err, status) {
825459 if (err.message == 'stream is closed')
826460 this.reconnect && this.reconnect()
827461 return pull(
828- pull.once(renderError(err, 'h2')),
462 + pull.once(this.renderError(err, 'h2')),
829463 this.serveTemplate(req, err.name, status || 500)
830464 )
831465 }
832466
833-function renderObjectData(obj, filename, repo, rev, path) {
834- var ext = getExtension(filename)
835- return readOnce(function (cb) {
836- readObjectString(obj, function (err, buf) {
467 +G.renderObjectData = function (obj, filename, repo, rev, path) {
468 + var ext = u.getExtension(filename)
469 + return u.readOnce(function (cb) {
470 + u.readObjectString(obj, function (err, buf) {
837471 buf = buf.toString('utf8')
838472 if (err) return cb(err)
839473 cb(null, (ext == 'md' || ext == 'markdown')
840474 ? markdown(buf, {repo: repo, rev: rev, path: path})
@@ -844,9 +478,9 @@
844478 }
845479
846480 function renderCodeTable(buf, ext) {
847481 return '<pre><table class="code">' +
848- highlight(buf, ext).split('\n').map(function (line, i) {
482 + u.highlight(buf, ext).split('\n').map(function (line, i) {
849483 i++
850484 return '<tr id="L' + i + '">' +
851485 '<td class="code-linenum">' + '<a href="#L' + i + '">' + i + '</td>' +
852486 '<td class="code-text">' + line + '</td></tr>'
@@ -873,9 +507,9 @@
873507 }),
874508 typeof filter == 'function' ? filter(opts) : filter,
875509 pull.take(20),
876510 this.addAuthorName(),
877- query.forwards && pullReverse(),
511 + query.forwards && u.pullReverse(),
878512 paginate(
879513 function (first, cb) {
880514 if (!query.lt && !query.gt) return cb(null, '')
881515 var gt = feedId ? first.value.sequence : first.value.timestamp + 1
@@ -912,12 +546,12 @@
912546
913547 G.renderFeedItem = function (req, msg, cb) {
914548 var self = this
915549 var c = msg.value.content
916- var msgLink = link([msg.key],
550 + var msgLink = u.link([msg.key],
917551 new Date(msg.value.timestamp).toLocaleString(req._locale))
918552 var author = msg.value.author
919- var authorLink = link([msg.value.author], msg.authorName)
553 + var authorLink = u.link([msg.value.author], msg.authorName)
920554 switch (c.type) {
921555 case 'git-repo':
922556 var done = multicb({ pluck: 1, spread: true })
923557 self.getRepoName(author, msg.key, done())
@@ -928,17 +562,17 @@
928562 done(function (err, repoName, upstreamName) {
929563 cb(null, '<section class="collapse">' + msgLink + '<br>' +
930564 req._t('Forked', {
931565 name: authorLink,
932- upstream: link([c.upstream], upstreamName),
933- repo: link([msg.key], repoName)
566 + upstream: u.link([c.upstream], upstreamName),
567 + repo: u.link([msg.key], repoName)
934568 }) + '</section>')
935569 })
936570 })
937571 } else {
938572 return done(function (err, repoName) {
939573 if (err) return cb(err)
940- var repoLink = link([msg.key], repoName)
574 + var repoLink = u.link([msg.key], repoName)
941575 cb(null, '<section class="collapse">' + msgLink + '<br>' +
942576 req._t('CreatedRepo', {
943577 name: authorLink,
944578 repo: repoLink
@@ -947,24 +581,25 @@
947581 }
948582 case 'git-update':
949583 return self.getRepoName(author, c.repo, function (err, repoName) {
950584 if (err) return cb(err)
951- var repoLink = link([c.repo], repoName)
585 + var repoLink = u.link([c.repo], repoName)
952586 cb(null, '<section class="collapse">' + msgLink + '<br>' +
953587 req._t('Pushed', {
954588 name: authorLink,
955589 repo: repoLink
956590 }) + '</section>')
957591 })
958592 case 'issue':
959593 case 'pull-request':
960- var issueLink = link([msg.key], c.title)
594 + var issueLink = u.link([msg.key], c.title)
961595 return self.getMsg(c.project, function (err, projectMsg) {
962- if (err) return cb(null, self.serveRepoNotFound(req, c.repo, err))
596 + if (err) return cb(null,
597 + self.repos.serveRepoNotFound(req, c.repo, err))
963598 self.getRepoName(projectMsg.author, c.project,
964599 function (err, repoName) {
965600 if (err) return cb(err)
966- var repoLink = link([c.project], repoName)
601 + var repoLink = u.link([c.project], repoName)
967602 cb(null, '<section class="collapse">' + msgLink + '<br>' +
968603 req._t('OpenedIssue', {
969604 name: authorLink,
970605 type: req._t(c.type == 'pull-request' ?
@@ -977,10 +612,10 @@
977612 case 'about':
978613 return cb(null, '<section class="collapse">' + msgLink + '<br>' +
979614 req._t('Named', {
980615 author: authorLink,
981- target: '<tt>' + escapeHTML(c.about) + '</tt>',
982- name: link([c.about], c.name)
616 + target: '<tt>' + u.escape(c.about) + '</tt>',
617 + name: u.link([c.about], c.name)
983618 }) + '</section>')
984619 case 'post':
985620 return this.pullReqs.get(c.issue, function (err, pr) {
986621 if (err) return cb(err)
@@ -988,15 +623,15 @@
988623 'pull request' : 'issue.'
989624 return cb(null, '<section class="collapse">' + msgLink + '<br>' +
990625 req._t('CommentedOn', {
991626 author: authorLink,
992- target: req._t(type) + ' ' + link([pr.id], pr.title, true)
627 + target: req._t(type) + ' ' + u.link([pr.id], pr.title, true)
993628 }) +
994629 '<blockquote>' + markdown(c.text) + '</blockquote>' +
995630 '</section>')
996631 })
997632 default:
998- return cb(null, json(msg))
633 + return cb(null, u.json(msg))
999634 }
1000635 }
1001636
1002637 /* Index */
@@ -1004,142 +639,63 @@
1004639 G.serveIndex = function (req) {
1005640 return this.serveTemplate(req)(this.renderFeed(req))
1006641 }
1007642
1008-G.serveUserPage = function (req, feedId, dirs) {
1009- switch (dirs[0]) {
1010- case undefined:
1011- case '':
1012- case 'activity':
1013- return this.serveUserActivity(req, feedId)
1014- case 'repos':
1015- return this.serveUserRepos(req, feedId)
1016- }
1017-}
643 +/* Message */
1018644
1019-G.renderUserPage = function (req, feedId, page, titleTemplate, body) {
1020- var self = this
1021- return readNext(function (cb) {
1022- self.about.getName(feedId, function (err, name) {
1023- if (err) return cb(err)
1024- var title = titleTemplate ? titleTemplate
1025- .replace(/\%{name\}/g, escapeHTML(name))
1026- : escapeHTML(name)
1027- cb(null, self.serveTemplate(req, title)(cat([
1028- pull.once('<h2>' + link([feedId], name) +
1029- '<code class="user-id">' + feedId + '</code></h2>' +
1030- nav([
1031- [[feedId], req._t('Activity'), 'activity'],
1032- [[feedId, 'repos'], req._t('Repos'), 'repos']
1033- ], page)),
1034- body
1035- ])))
1036- })
1037- })
1038-}
1039-
1040-G.serveUserActivity = function (req, feedId) {
1041- return this.renderUserPage(req, feedId, 'activity', null,
1042- this.renderFeed(req, feedId))
1043-}
1044-
1045-G.serveUserRepos = function (req, feedId) {
1046- var self = this
1047- var title = req._t('UsersRepos', {name: '%{name}'})
1048- return this.renderUserPage(req, feedId, 'repos', title, pull(
1049- cat([
1050- self.ssb.messagesByType({
1051- type: 'git-update',
1052- reverse: true
1053- }),
1054- self.ssb.messagesByType({
1055- type: 'git-repo',
1056- reverse: true
1057- })
1058- ]),
1059- pull.filter(function (msg) {
1060- return msg.value.author == feedId
1061- }),
1062- pull.unique(function (msg) {
1063- return msg.value.content.repo || msg.key
1064- }),
1065- pull.take(20),
1066- paramap(function (msg, cb) {
1067- var repoId = msg.value.content.repo || msg.key
1068- var done = multicb({ pluck: 1, spread: true })
1069- self.getRepoName(feedId, repoId, done())
1070- self.getVotes(repoId, done())
1071- done(function (err, repoName, votes) {
1072- if (err) return cb(err)
1073- cb(null, '<section class="collapse">' +
1074- '<span class="right-bar">' +
1075- '<i>✌</i> ' +
1076- link([repoId, 'digs'], votes.upvotes, true,
1077- ' title="' + req._t('Digs') + '"') +
1078- '</span>' +
1079- '<strong>' + link([repoId], repoName) + '</strong>' +
1080- '<div class="date-info">' +
1081- req._t(msg.value.content.type == 'git-update' ?
1082- 'UpdatedOnDate' : 'CreatedOnDate',
1083- {
1084- date: timestamp(msg.value.timestamp, req)
1085- }) + '</div>' +
1086- '</section>')
1087- })
1088- }, 8)
1089- ))
1090-}
1091-
1092- /* Message */
1093-
1094645 G.serveMessage = function (req, id, path) {
1095646 var self = this
1096- return readNext(function (cb) {
647 + return u.readNext(function (cb) {
1097648 self.ssb.get(id, function (err, msg) {
1098649 if (err) return cb(null, self.serveError(req, err))
1099650 var c = msg.content || {}
1100651 switch (c.type) {
1101652 case 'git-repo':
1102653 return self.getRepo(id, function (err, repo) {
1103654 if (err) return cb(null, self.serveError(req, err))
1104- cb(null, self.serveRepoPage(req, Repo(repo), path))
655 + cb(null, self.repos.serveRepoPage(req, GitRepo(repo), path))
1105656 })
1106657 case 'git-update':
1107658 return self.getRepo(c.repo, function (err, repo) {
1108- if (err) return cb(null, self.serveRepoNotFound(req, c.repo, err))
1109- cb(null, self.serveRepoUpdate(req, Repo(repo), id, msg, path))
659 + if (err) return cb(null,
660 + self.repos.serveRepoNotFound(req, c.repo, err))
661 + cb(null, self.repos.serveRepoUpdate(req,
662 + GitRepo(repo), id, msg, path))
1110663 })
1111664 case 'issue':
1112665 return self.getRepo(c.project, function (err, repo) {
1113666 if (err) return cb(null,
1114- self.serveRepoNotFound(req, c.project, err))
667 + self.repos.serveRepoNotFound(req, c.project, err))
1115668 self.issues.get(id, function (err, issue) {
1116669 if (err) return cb(null, self.serveError(req, err))
1117- cb(null, self.serveRepoIssue(req, Repo(repo), issue, path))
670 + cb(null, self.repos.issues.serveRepoIssue(req,
671 + GitRepo(repo), issue, path))
1118672 })
1119673 })
1120674 case 'pull-request':
1121675 return self.getRepo(c.repo, function (err, repo) {
1122676 if (err) return cb(null,
1123- self.serveRepoNotFound(req, c.project, err))
677 + self.repos.serveRepoNotFound(req, c.project, err))
1124678 self.pullReqs.get(id, function (err, pr) {
1125679 if (err) return cb(null, self.serveError(req, err))
1126- cb(null, self.serveRepoPullReq(req, Repo(repo), pr, path))
680 + cb(null, self.repos.pulls.serveRepoPullReq(req,
681 + GitRepo(repo), pr, path))
1127682 })
1128683 })
1129684 case 'issue-edit':
1130685 if (ref.isMsgId(c.issue)) {
1131686 return self.pullReqs.get(c.issue, function (err, issue) {
1132687 if (err) return cb(err)
1133- var serve = issue.msg.value.content.type == 'pull-request' ?
1134- self.serveRepoPullReq : self.serveRepoIssue
688 + var serve = issue.msg.value.content.type == 'pull-request'
689 + ? self.repos.pulls.serveRepoPullReq
690 + : self.repos.issues.serveRepoIssue
1135691 self.getRepo(issue.project, function (err, repo) {
1136692 if (err) {
1137693 if (!repo) return cb(null,
1138- self.serveRepoNotFound(req, c.repo, err))
694 + self.repos.serveRepoNotFound(req, c.repo, err))
1139695 return cb(null, self.serveError(req, err))
1140696 }
1141- cb(null, serve.call(self, req, Repo(repo), issue, path, id))
697 + cb(null, serve.call(self, req, GitRepo(repo), issue, path, id))
1142698 })
1143699 })
1144700 }
1145701 // fallthrough
@@ -1151,14 +707,15 @@
1151707 self.pullReqs.get(c.issue, done())
1152708 return done(function (err, repo, issue) {
1153709 if (err) {
1154710 if (!repo) return cb(null,
1155- self.serveRepoNotFound(req, c.repo, err))
711 + self.repos.serveRepoNotFound(req, c.repo, err))
1156712 return cb(null, self.serveError(req, err))
1157713 }
1158- var serve = issue.msg.value.content.type == 'pull-request' ?
1159- self.serveRepoPullReq : self.serveRepoIssue
1160- cb(null, serve.call(self, req, Repo(repo), issue, path, id))
714 + var serve = issue.msg.value.content.type == 'pull-request'
715 + ? self.repos.pulls.serveRepoPullReq
716 + : self.repos.issues.serveRepoIssue
717 + cb(null, serve.call(self, req, GitRepo(repo), issue, path, id))
1161718 })
1162719 } else if (ref.isMsgId(c.root)) {
1163720 // comment on issue from patchwork?
1164721 return self.getMsg(c.root, function (err, root) {
@@ -1172,15 +729,17 @@
1172729 case 'issue':
1173730 return self.issues.get(c.root, function (err, issue) {
1174731 if (err) return cb(null, self.serveError(req, err))
1175732 return cb(null,
1176- self.serveRepoIssue(req, Repo(repo), issue, path, id))
733 + self.repos.issues.serveRepoIssue(req,
734 + GitRepo(repo), issue, path, id))
1177735 })
1178736 case 'pull-request':
1179737 return self.pullReqs.get(c.root, function (err, pr) {
1180738 if (err) return cb(null, self.serveError(req, err))
1181739 return cb(null,
1182- self.serveRepoPullReq(req, Repo(repo), pr, path, id))
740 + self.repos.pulls.serveRepoPullReq(req,
741 + GitRepo(repo), pr, path, id))
1183742 })
1184743 }
1185744 })
1186745 })
@@ -1189,10 +748,11 @@
1189748 default:
1190749 if (ref.isMsgId(c.repo))
1191750 return self.getRepo(c.repo, function (err, repo) {
1192751 if (err) return cb(null,
1193- self.serveRepoNotFound(req, c.repo, err))
1194- cb(null, self.serveRepoSomething(req, Repo(repo), id, msg, path))
752 + self.repos.serveRepoNotFound(req, c.repo, err))
753 + cb(null, self.repos.serveRepoSomething(req,
754 + GitRepo(repo), id, msg, path))
1195755 })
1196756 else
1197757 return cb(null, self.serveGenericMessage(req, id, msg, path))
1198758 }
@@ -1201,217 +761,13 @@
1201761 }
1202762
1203763 G.serveGenericMessage = function (req, id, msg, path) {
1204764 return this.serveTemplate(req, id)(pull.once(
1205- '<section><h2>' + link([id]) + '</h2>' +
1206- json(msg) +
765 + '<section><h2>' + u.link([id]) + '</h2>' +
766 + u.json(msg) +
1207767 '</section>'))
1208768 }
1209769
1210- /* Repo */
1211-
1212-G.serveRepoPage = function (req, repo, path) {
1213- var self = this
1214- var defaultBranch = 'master'
1215- var query = req._u.query
1216-
1217- if (query.rev != null) {
1218- // Allow navigating revs using GET query param.
1219- // Replace the branch in the path with the rev query value
1220- path[0] = path[0] || 'tree'
1221- path[1] = query.rev
1222- req._u.pathname = encodeLink([repo.id].concat(path))
1223- delete req._u.query.rev
1224- delete req._u.search
1225- return self.serveRedirect(req, url.format(req._u))
1226- }
1227-
1228- // get branch
1229- return path[1] ?
1230- G_serveRepoPage2.call(self, req, repo, path) :
1231- readNext(function (cb) {
1232- // TODO: handle this in pull-git-repo or ssb-git-repo
1233- repo.getSymRef('HEAD', true, function (err, ref) {
1234- if (err) return cb(err)
1235- repo.resolveRef(ref, function (err, rev) {
1236- path[1] = rev ? ref : null
1237- cb(null, G_serveRepoPage2.call(self, req, repo, path))
1238- })
1239- })
1240- })
1241-}
1242-
1243-function G_serveRepoPage2(req, repo, path) {
1244- var branch = path[1]
1245- var filePath = path.slice(2)
1246- switch (path[0]) {
1247- case undefined:
1248- case '':
1249- return this.serveRepoTree(req, repo, branch, [])
1250- case 'activity':
1251- return this.serveRepoActivity(req, repo, branch)
1252- case 'commits':
1253- return this.serveRepoCommits(req, repo, branch)
1254- case 'commit':
1255- return this.serveRepoCommit(req, repo, path[1])
1256- case 'tag':
1257- return this.serveRepoTag(req, repo, branch)
1258- case 'tree':
1259- return this.serveRepoTree(req, repo, branch, filePath)
1260- case 'blob':
1261- return this.serveRepoBlob(req, repo, branch, filePath)
1262- case 'raw':
1263- return this.serveRepoRaw(req, repo, branch, filePath)
1264- case 'digs':
1265- return this.serveRepoDigs(req, repo)
1266- case 'fork':
1267- return this.serveRepoForkPrompt(req, repo)
1268- case 'forks':
1269- return this.serveRepoForks(req, repo)
1270- case 'issues':
1271- switch (path[1]) {
1272- case 'new':
1273- if (filePath.length == 0)
1274- return this.serveRepoNewIssue(req, repo)
1275- break
1276- default:
1277- return this.serveRepoIssues(req, repo, false)
1278- }
1279- case 'pulls':
1280- return this.serveRepoIssues(req, repo, true)
1281- case 'compare':
1282- return this.serveRepoCompare(req, repo)
1283- case 'comparing':
1284- return this.serveRepoComparing(req, repo)
1285- default:
1286- return this.serve404(req)
1287- }
1288-}
1289-
1290-G.serveRepoNotFound = function (req, id, err) {
1291- return this.serveTemplate(req, req._t('error.RepoNotFound'), 404)
1292- (pull.values([
1293- '<h2>' + req._t('error.RepoNotFound') + '</h2>',
1294- '<p>' + req._t('error.RepoNameNotFound') + '</p>',
1295- '<pre>' + escapeHTML(err.stack) + '</pre>'
1296- ]))
1297-}
1298-
1299-G.renderRepoPage = function (req, repo, page, branch, titleTemplate, body) {
1300- var self = this
1301- var gitUrl = 'ssb://' + repo.id
1302- var gitLink = '<input class="clone-url" readonly="readonly" ' +
1303- 'value="' + gitUrl + '" size="45" ' +
1304- 'onclick="this.select()"/>'
1305- var digsPath = [repo.id, 'digs']
1306-
1307- var done = multicb({ pluck: 1, spread: true })
1308- self.getRepoName(repo.feed, repo.id, done())
1309- self.about.getName(repo.feed, done())
1310- self.getVotes(repo.id, done())
1311-
1312- if (repo.upstream) {
1313- self.getRepoName(repo.upstream.feed, repo.upstream.id, done())
1314- self.about.getName(repo.upstream.feed, done())
1315- }
1316-
1317- return readNext(function (cb) {
1318- done(function (err, repoName, authorName, votes,
1319- upstreamName, upstreamAuthorName) {
1320- if (err) return cb(null, self.serveError(req, err))
1321- var upvoted = votes.upvoters[self.myId] > 0
1322- var upstreamLink = !repo.upstream ? '' :
1323- link([repo.upstream])
1324- var title = titleTemplate ? titleTemplate
1325- .replace(/%\{repo\}/g, repoName)
1326- .replace(/%\{author\}/g, authorName)
1327- : authorName + '/' + repoName
1328- var isPublic = self.isPublic
1329- cb(null, self.serveTemplate(req, title)(cat([
1330- pull.once(
1331- '<div class="repo-title">' +
1332- '<form class="right-bar" action="" method="post">' +
1333- '<button class="btn" name="action" value="vote" ' +
1334- (isPublic ? 'disabled="disabled"' : ' type="submit"') + '>' +
1335- '<i>✌</i> ' + req._t(!isPublic && upvoted ? 'Undig' : 'Dig') +
1336- '</button>' +
1337- (isPublic ? '' : '<input type="hidden" name="value" value="' +
1338- (upvoted ? '0' : '1') + '">' +
1339- '<input type="hidden" name="id" value="' +
1340- escapeHTML(repo.id) + '">') + ' ' +
1341- '<strong>' + link(digsPath, votes.upvotes) + '</strong> ' +
1342- (isPublic ? '' : '<button class="btn" type="submit" ' +
1343- ' name="action" value="fork-prompt">' +
1344- '<i>⑂</i> ' + req._t('Fork') +
1345- '</button>') + ' ' +
1346- link([repo.id, 'forks'], '+', false, ' title="' +
1347- req._t('Forks') + '"') +
1348- '</form>' +
1349- renderNameForm(req, !isPublic, repo.id, repoName, 'repo-name',
1350- null, req._t('repo.Rename'),
1351- '<h2 class="bgslash">' + link([repo.feed], authorName) + ' / ' +
1352- link([repo.id], repoName) + '</h2>') +
1353- '</div>' +
1354- (repo.upstream ? '<small class="bgslash">' + req._t('ForkedFrom', {
1355- repo: link([repo.upstream.feed], upstreamAuthorName) + '/' +
1356- link([repo.upstream.id], upstreamName)
1357- }) + '</small>' : '') +
1358- nav([
1359- [[repo.id], req._t('Code'), 'code'],
1360- [[repo.id, 'activity'], req._t('Activity'), 'activity'],
1361- [[repo.id, 'commits', branch||''], req._t('Commits'), 'commits'],
1362- [[repo.id, 'issues'], req._t('Issues'), 'issues'],
1363- [[repo.id, 'pulls'], req._t('PullRequests'), 'pulls']
1364- ], page, gitLink)),
1365- body
1366- ])))
1367- })
1368- })
1369-}
1370-
1371-G.serveEmptyRepo = function (req, repo) {
1372- if (repo.feed != this.myId)
1373- return this.renderRepoPage(req, repo, 'code', null, null, pull.once(
1374- '<section>' +
1375- '<h3>' + req._t('EmptyRepo') + '</h3>' +
1376- '</section>'))
1377-
1378- var gitUrl = 'ssb://' + repo.id
1379- return this.renderRepoPage(req, repo, 'code', null, null, pull.once(
1380- '<section>' +
1381- '<h3>' + req._t('initRepo.GettingStarted') + '</h3>' +
1382- '<h4>' + req._t('initRepo.CreateNew') + '</h4><pre>' +
1383- 'touch ' + req._t('initRepo.README') + '.md\n' +
1384- 'git init\n' +
1385- 'git add ' + req._t('initRepo.README') + '.md\n' +
1386- 'git commit -m "' + req._t('initRepo.InitialCommit') + '"\n' +
1387- 'git remote add origin ' + gitUrl + '\n' +
1388- 'git push -u origin master</pre>\n' +
1389- '<h4>' + req._t('initRepo.PushExisting') + '</h4>\n' +
1390- '<pre>git remote add origin ' + gitUrl + '\n' +
1391- 'git push -u origin master</pre>' +
1392- '</section>'))
1393-}
1394-
1395-G.serveRepoTree = function (req, repo, rev, path) {
1396- if (!rev) return this.serveEmptyRepo(req, repo)
1397- var type = repo.isCommitHash(rev) ? 'Tree' : 'Branch'
1398- var title = (path.length ? path.join('/') + ' · ' : '') +
1399- '%{author}/%{repo}' +
1400- (repo.head == 'refs/heads/' + rev ? '' : '@' + rev)
1401- return this.renderRepoPage(req, repo, 'code', rev, title, cat([
1402- pull.once('<section><form action="" method="get">' +
1403- '<h3>' + req._t(type) + ': ' + rev + ' '),
1404- revMenu(req, repo, rev),
1405- pull.once('</h3></form>'),
1406- type == 'Branch' && renderRepoLatest(req, repo, rev),
1407- pull.once('</section><section>'),
1408- renderRepoTree(req, repo, rev, path),
1409- pull.once('</section>'),
1410- renderRepoReadme(req, repo, rev, path)
1411- ]))
1412-}
1413-
1414770 /* Search */
1415771
1416772 G.serveSearch = function (req) {
1417773 var self = this
@@ -1459,584 +815,18 @@
1459815 })
1460816 )
1461817 }
1462818
1463-/* Repo activity */
1464-
1465-G.serveRepoActivity = function (req, repo, branch) {
1466- var self = this
1467- var title = req._t('Activity') + ' · %{author}/%{repo}'
1468- return self.renderRepoPage(req, repo, 'activity', branch, title, cat([
1469- pull.once('<h3>' + req._t('Activity') + '</h3>'),
1470- pull(
1471- self.ssb.links({
1472- dest: repo.id,
1473- source: repo.feed,
1474- rel: 'repo',
1475- values: true,
1476- reverse: true
1477- }),
1478- pull.map(renderRepoUpdate.bind(self, req, repo))
1479- ),
1480- readOnce(function (cb) {
1481- var done = multicb({ pluck: 1, spread: true })
1482- self.about.getName(repo.feed, done())
1483- self.getMsg(repo.id, done())
1484- done(function (err, authorName, msg) {
1485- if (err) return cb(err)
1486- self.renderFeedItem(req, {
1487- key: repo.id,
1488- value: msg,
1489- authorName: authorName
1490- }, cb)
1491- })
1492- })
1493- ]))
1494-}
1495-
1496-function renderRepoUpdate(req, repo, msg, full) {
1497- var c = msg.value.content
1498-
1499- if (c.type != 'git-update') {
1500- return ''
1501- // return renderFeedItem(msg, cb)
1502- // TODO: render post, issue, pull-request
1503- }
1504-
1505- var branches = []
1506- var tags = []
1507- if (c.refs) for (var name in c.refs) {
1508- var m = name.match(/^refs\/(heads|tags)\/(.*)$/) || [,, name]
1509- ;(m[1] == 'tags' ? tags : branches)
1510- .push({name: m[2], value: c.refs[name]})
1511- }
1512- var numObjects = c.objects ? Object.keys(c.objects).length : 0
1513-
1514- var dateStr = new Date(msg.value.timestamp).toLocaleString(req._locale)
1515- return '<section class="collapse">' +
1516- link([msg.key], dateStr) + '<br>' +
1517- branches.map(function (update) {
1518- if (!update.value) {
1519- return '<s>' + escapeHTML(update.name) + '</s><br/>'
1520- } else {
1521- var commitLink = link([repo.id, 'commit', update.value])
1522- var branchLink = link([repo.id, 'tree', update.name])
1523- return branchLink + ' &rarr; <tt>' + commitLink + '</tt><br/>'
1524- }
1525- }).join('') +
1526- tags.map(function (update) {
1527- return update.value
1528- ? link([repo.id, 'tag', update.value], update.name)
1529- : '<s>' + escapeHTML(update.name) + '</s>'
1530- }).join(', ') +
1531- '</section>'
1532-}
1533-
1534-/* Repo commits */
1535-
1536-G.serveRepoCommits = function (req, repo, branch) {
1537- var query = req._u.query
1538- var title = req._t('Commits') + ' · %{author}/%{repo}'
1539- return this.renderRepoPage(req, repo, 'commits', branch, title, cat([
1540- pull.once('<h3>' + req._t('Commits') + '</h3>'),
1541- pull(
1542- repo.readLog(query.start || branch),
1543- pull.take(20),
1544- paramap(repo.getCommitParsed.bind(repo), 8),
1545- paginate(
1546- !query.start ? '' : function (first, cb) {
1547- cb(null, '&hellip;')
1548- },
1549- pull.map(renderCommit.bind(this, req, repo)),
1550- function (commit, cb) {
1551- cb(null, commit.parents && commit.parents[0] ?
1552- '<a href="?start=' + commit.id + '">' +
1553- req._t('Older') + '</a>' : '')
1554- }
1555- )
1556- )
1557- ]))
1558-}
1559-
1560-function renderCommit(req, repo, commit) {
1561- var commitPath = [repo.id, 'commit', commit.id]
1562- var treePath = [repo.id, 'tree', commit.id]
1563- return '<section class="collapse">' +
1564- '<strong>' + link(commitPath, commit.title) + '</strong><br>' +
1565- '<tt>' + commit.id + '</tt> ' +
1566- link(treePath, req._t('Tree')) + '<br>' +
1567- escapeHTML(commit.author.name) + ' &middot; ' +
1568- commit.author.date.toLocaleString(req._locale) +
1569- (commit.separateAuthor ? '<br>' + req._t('CommittedOn', {
1570- name: escapeHTML(commit.committer.name),
1571- date: commit.committer.date.toLocaleString(req._locale)
1572- }) : '') +
1573- '</section>'
1574-}
1575-
1576-/* Branch menu */
1577-
1578-function formatRevOptions(currentName) {
1579- return function (name) {
1580- var htmlName = escapeHTML(name)
1581- return '<option value="' + htmlName + '"' +
1582- (name == currentName ? ' selected="selected"' : '') +
1583- '>' + htmlName + '</option>'
1584- }
1585-}
1586-
1587-function formatRevType(req, type) {
1588- return (
1589- type == 'heads' ? req._t('Branches') :
1590- type == 'tags' ? req._t('Tags') :
1591- type)
1592-}
1593-
1594-function revMenu(req, repo, currentName) {
1595- return readOnce(function (cb) {
1596- repo.getRefNames(function (err, refs) {
1597- if (err) return cb(err)
1598- cb(null, '<select name="rev" onchange="this.form.submit()">' +
1599- Object.keys(refs).map(function (group) {
1600- return '<optgroup label="' + formatRevType(req, group) + '">' +
1601- refs[group].map(formatRevOptions(currentName)).join('') +
1602- '</optgroup>'
1603- }).join('') +
1604- '</select><noscript> ' +
1605- '<input type="submit" value="' + req._t('Go') + '"/></noscript>')
1606- })
1607- })
1608-}
1609-
1610-function branchMenu(repo, name, currentName) {
1611- return cat([
1612- pull.once('<select name="' + name + '">'),
1613- pull(
1614- repo.refs(),
1615- pull.map(function (ref) {
1616- var m = ref.name.match(/^refs\/([^\/]*)\/(.*)$/) || [,, ref.name]
1617- return m[1] == 'heads' && m[2]
1618- }),
1619- pull.filter(Boolean),
1620- pullSort(),
1621- pull.map(formatRevOptions(currentName))
1622- ),
1623- pull.once('</select>')
1624- ])
1625-}
1626-
1627-/* Repo tree */
1628-
1629-function renderRepoLatest(req, repo, rev) {
1630- return readOnce(function (cb) {
1631- repo.getCommitParsed(rev, function (err, commit) {
1632- if (err) return cb(err)
1633- var commitPath = [repo.id, 'commit', commit.id]
1634- cb(null,
1635- req._t('Latest') + ': ' +
1636- '<strong>' + link(commitPath, commit.title) + '</strong><br/>' +
1637- '<tt>' + commit.id + '</tt><br/> ' +
1638- req._t('CommittedOn', {
1639- name: escapeHTML(commit.committer.name),
1640- date: commit.committer.date.toLocaleString(req._locale)
1641- }) +
1642- (commit.separateAuthor ? '<br/>' + req._t('AuthoredOn', {
1643- name: escapeHTML(commit.author.name),
1644- date: commit.author.date.toLocaleString(req._locale)
1645- }) : ''))
1646- })
1647- })
1648-}
1649-
1650-// breadcrumbs
1651-function linkPath(basePath, path) {
1652- path = path.slice()
1653- var last = path.pop()
1654- return path.map(function (dir, i) {
1655- return link(basePath.concat(path.slice(0, i+1)), dir)
1656- }).concat(last).join(' / ')
1657-}
1658-
1659-function renderRepoTree(req, repo, rev, path) {
1660- var pathLinks = path.length === 0 ? '' :
1661- ': ' + linkPath([repo.id, 'tree'], [rev].concat(path))
1662- return cat([
1663- pull.once('<h3>' + req._t('Files') + pathLinks + '</h3>'),
1664- pull(
1665- repo.readDir(rev, path),
1666- pull.map(function (file) {
1667- var type = (file.mode === 040000) ? 'tree' :
1668- (file.mode === 0160000) ? 'commit' : 'blob'
1669- if (type == 'commit')
1670- return [
1671- '<span title="' + req._t('gitCommitLink') + '">🖈</span>',
1672- '<span title="' + escapeHTML(file.id) + '">' +
1673- escapeHTML(file.name) + '</span>']
1674- var filePath = [repo.id, type, rev].concat(path, file.name)
1675- return ['<i>' + (type == 'tree' ? '📁' : '📄') + '</i>',
1676- link(filePath, file.name)]
1677- }),
1678- table('class="files"')
1679- )
1680- ])
1681-}
1682-
1683-/* Repo readme */
1684-
1685-function renderRepoReadme(req, repo, branch, path) {
1686- return readNext(function (cb) {
1687- pull(
1688- repo.readDir(branch, path),
1689- pull.filter(function (file) {
1690- return /readme(\.|$)/i.test(file.name)
1691- }),
1692- pull.take(1),
1693- pull.collect(function (err, files) {
1694- if (err) return cb(null, pull.empty())
1695- var file = files[0]
1696- if (!file)
1697- return cb(null, pull.once(path.length ? '' :
1698- '<p>' + req._t('NoReadme') + '</p>'))
1699- repo.getObjectFromAny(file.id, function (err, obj) {
1700- if (err) return cb(err)
1701- cb(null, cat([
1702- pull.once('<section><h4><a name="readme">' +
1703- escapeHTML(file.name) + '</a></h4><hr/>'),
1704- renderObjectData(obj, file.name, repo, branch, path),
1705- pull.once('</section>')
1706- ]))
1707- })
1708- })
1709- )
1710- })
1711-}
1712-
1713-/* Repo commit */
1714-
1715-G.serveRepoCommit = function (req, repo, rev) {
1716- var self = this
1717- return readNext(function (cb) {
1718- repo.getCommitParsed(rev, function (err, commit) {
1719- if (err) return cb(err)
1720- var commitPath = [repo.id, 'commit', commit.id]
1721- var treePath = [repo.id, 'tree', commit.id]
1722- var title = escapeHTML(commit.title) + ' · ' +
1723- '%{author}/%{repo}@' + commit.id.substr(0, 8)
1724- cb(null, self.renderRepoPage(req, repo, null, rev, title, cat([
1725- pull.once(
1726- '<h3>' + link(commitPath,
1727- req._t('CommitRev', {rev: rev})) + '</h3>' +
1728- '<section class="collapse">' +
1729- '<div class="right-bar">' +
1730- link(treePath, req._t('BrowseFiles')) +
1731- '</div>' +
1732- '<h4>' + linkify(escapeHTML(commit.title)) + '</h4>' +
1733- (commit.body ? linkify(pre(commit.body)) : '') +
1734- (commit.separateAuthor ? req._t('AuthoredOn', {
1735- name: escapeHTML(commit.author.name),
1736- date: commit.author.date.toLocaleString(req._locale)
1737- }) + '<br/>' : '') +
1738- req._t('CommittedOn', {
1739- name: escapeHTML(commit.committer.name),
1740- date: commit.committer.date.toLocaleString(req._locale)
1741- }) + '<br/>' +
1742- commit.parents.map(function (id) {
1743- return req._t('Parent') + ': ' +
1744- link([repo.id, 'commit', id], id)
1745- }).join('<br>') +
1746- '</section>' +
1747- '<section><h3>' + req._t('FilesChanged') + '</h3>'),
1748- // TODO: show diff from all parents (merge commits)
1749- renderDiffStat(req, [repo, repo], [commit.parents[0], commit.id]),
1750- pull.once('</section>')
1751- ])))
1752- })
1753- })
1754-}
1755-
1756-/* Repo tag */
1757-
1758-G.serveRepoTag = function (req, repo, rev) {
1759- var self = this
1760- return readNext(function (cb) {
1761- repo.getTagParsed(rev, function (err, tag) {
1762- if (err) return cb(err)
1763- var title = req._t('TagName', {
1764- tag: escapeHTML(tag.tag)
1765- }) + ' · %{author}/%{repo}'
1766- var body = (tag.title + '\n\n' +
1767- tag.body.replace(/-----BEGIN PGP SIGNATURE-----\n[^.]*?\n-----END PGP SIGNATURE-----\s*$/, '')).trim()
1768- cb(null, self.renderRepoPage(req, repo, 'tags', tag.object, title,
1769- pull.once(
1770- '<section class="collapse">' +
1771- '<h3>' + link([repo.id, 'tag', rev], tag.tag) + '</h3>' +
1772- req._t('TaggedOn', {
1773- name: escapeHTML(tag.tagger.name),
1774- date: tag.tagger.date.toLocaleString(req._locale)
1775- }) + '<br/>' +
1776- link([repo.id, tag.type, tag.object]) +
1777- linkify(pre(body)) +
1778- '</section>')))
1779- })
1780- })
1781-}
1782-
1783-
1784-/* Diff stat */
1785-
1786-function renderDiffStat(req, repos, treeIds) {
1787- if (treeIds.length == 0) treeIds = [null]
1788- var id = treeIds[0]
1789- var lastI = treeIds.length - 1
1790- var oldTree = treeIds[0]
1791- var changedFiles = []
1792- return cat([
1793- pull(
1794- Repo.diffTrees(repos, treeIds, true),
1795- pull.map(function (item) {
1796- var filename = escapeHTML(item.filename = item.path.join('/'))
1797- var oldId = item.id && item.id[0]
1798- var newId = item.id && item.id[lastI]
1799- var oldMode = item.mode && item.mode[0]
1800- var newMode = item.mode && item.mode[lastI]
1801- var action =
1802- !oldId && newId ? req._t('action.added') :
1803- oldId && !newId ? req._t('action.deleted') :
1804- oldMode != newMode ? req._t('action.changedMode', {
1805- old: oldMode.toString(8),
1806- new: newMode.toString(8)
1807- }) : req._t('changed')
1808- if (item.id)
1809- changedFiles.push(item)
1810- var blobsPath = item.id[1]
1811- ? [repos[1].id, 'blob', treeIds[1]]
1812- : [repos[0].id, 'blob', treeIds[0]]
1813- var rawsPath = item.id[1]
1814- ? [repos[1].id, 'raw', treeIds[1]]
1815- : [repos[0].id, 'raw', treeIds[0]]
1816- item.blobPath = blobsPath.concat(item.path)
1817- item.rawPath = rawsPath.concat(item.path)
1818- var fileHref = item.id ?
1819- '#' + encodeURIComponent(item.path.join('/')) :
1820- encodeLink(item.blobPath)
1821- return ['<a href="' + fileHref + '">' + filename + '</a>', action]
1822- }),
1823- table()
1824- ),
1825- pull(
1826- pull.values(changedFiles),
1827- paramap(function (item, cb) {
1828- var extension = getExtension(item.filename)
1829- if (extension in imgMimes) {
1830- var filename = escapeHTML(item.filename)
1831- return cb(null,
1832- '<pre><table class="code">' +
1833- '<tr><th id="' + escapeHTML(item.filename) + '">' +
1834- filename + '</th></tr>' +
1835- '<tr><td><img src="' + encodeLink(item.rawPath) + '"' +
1836- ' alt="' + filename + '"/></td></tr>' +
1837- '</table></pre>')
1838- }
1839- var done = multicb({ pluck: 1, spread: true })
1840- getRepoObjectString(repos[0], item.id[0], done())
1841- getRepoObjectString(repos[1], item.id[lastI], done())
1842- done(function (err, strOld, strNew) {
1843- if (err) return cb(err)
1844- cb(null, htmlLineDiff(req, item.filename, item.filename,
1845- strOld, strNew,
1846- encodeLink(item.blobPath)))
1847- })
1848- }, 4)
1849- )
1850- ])
1851-}
1852-
1853-function htmlLineDiff(req, filename, anchor, oldStr, newStr, blobHref) {
1854- var diff = JsDiff.structuredPatch('', '', oldStr, newStr)
1855- var groups = diff.hunks.map(function (hunk) {
1856- var oldLine = hunk.oldStart
1857- var newLine = hunk.newStart
1858- var header = '<tr class="diff-hunk-header"><td colspan=2></td><td>' +
1859- '@@ -' + oldLine + ',' + hunk.oldLines + ' ' +
1860- '+' + newLine + ',' + hunk.newLines + ' @@' +
1861- '</td></tr>'
1862- return [header].concat(hunk.lines.map(function (line) {
1863- var s = line[0]
1864- if (s == '\\') return
1865- var html = highlight(line, getExtension(filename))
1866- var trClass = s == '+' ? 'diff-new' : s == '-' ? 'diff-old' : ''
1867- var lineNums = [s == '+' ? '' : oldLine++, s == '-' ? '' : newLine++]
1868- var id = [filename].concat(lineNums).join('-')
1869- return '<tr id="' + escapeHTML(id) + '" class="' + trClass + '">' +
1870- lineNums.map(function (num) {
1871- return '<td class="code-linenum">' +
1872- (num ? '<a href="#' + encodeURIComponent(id) + '">' +
1873- num + '</a>' : '') + '</td>'
1874- }).join('') +
1875- '<td class="code-text">' + html + '</td></tr>'
1876- }))
1877- })
1878- return '<pre><table class="code">' +
1879- '<tr><th colspan=3 id="' + escapeHTML(anchor) + '">' + filename +
1880- '<span class="right-bar">' +
1881- '<a href="' + blobHref + '">' + req._t('View') + '</a> ' +
1882- '</span></th></tr>' +
1883- [].concat.apply([], groups).join('') +
1884- '</table></pre>'
1885-}
1886-
1887-/* An unknown message linking to a repo */
1888-
1889-G.serveRepoSomething = function (req, repo, id, msg, path) {
1890- return this.renderRepoPage(req, repo, null, null, null,
1891- pull.once('<section><h3>' + link([id]) + '</h3>' +
1892- json(msg) + '</section>'))
1893-}
1894-
1895-/* Repo update */
1896-
1897-function objsArr(objs) {
1898- return Array.isArray(objs) ? objs :
1899- Object.keys(objs).map(function (sha1) {
1900- var obj = Object.create(objs[sha1])
1901- obj.sha1 = sha1
1902- return obj
1903- })
1904-}
1905-
1906-G.serveRepoUpdate = function (req, repo, id, msg, path) {
1907- var self = this
1908- var raw = req._u.query.raw != null
1909- var title = req._t('Update') + ' · %{author}/%{repo}'
1910-
1911- if (raw)
1912- return self.renderRepoPage(req, repo, 'activity', null, title, pull.once(
1913- '<a href="?" class="raw-link header-align">' +
1914- req._t('Info') + '</a>' +
1915- '<h3>' + req._t('Update') + '</h3>' +
1916- '<section class="collapse">' +
1917- json({key: id, value: msg}) + '</section>'))
1918-
1919- // convert packs to old single-object style
1920- if (msg.content.indexes) {
1921- for (var i = 0; i < msg.content.indexes.length; i++) {
1922- msg.content.packs[i] = {
1923- pack: {link: msg.content.packs[i].link},
1924- idx: msg.content.indexes[i]
1925- }
1926- }
1927- }
1928-
1929- var commits = cat([
1930- msg.content.objects && pull(
1931- pull.values(msg.content.objects),
1932- pull.filter(function (obj) { return obj.type == 'commit' }),
1933- paramap(function (obj, cb) {
1934- self.getBlob(req, obj.link || obj.key, function (err, readObject) {
1935- if (err) return cb(err)
1936- Repo.getCommitParsed({read: readObject}, cb)
1937- })
1938- }, 8)
1939- ),
1940- msg.content.packs && pull(
1941- pull.values(msg.content.packs),
1942- paramap(function (pack, cb) {
1943- var done = multicb({ pluck: 1, spread: true })
1944- self.getBlob(req, pack.pack.link, done())
1945- self.getBlob(req, pack.idx.link, done())
1946- done(function (err, readPack, readIdx) {
1947- if (err) return cb(renderError(err))
1948- cb(null, gitPack.decodeWithIndex(repo, readPack, readIdx))
1949- })
1950- }, 4),
1951- pull.flatten(),
1952- pull.asyncMap(function (obj, cb) {
1953- if (obj.type == 'commit')
1954- Repo.getCommitParsed(obj, cb)
1955- else
1956- pull(obj.read, pull.drain(null, cb))
1957- }),
1958- pull.filter()
1959- )
1960- ])
1961-
1962- return self.renderRepoPage(req, repo, 'activity', null, title, cat([
1963- pull.once('<a href="?raw" class="raw-link header-align">' +
1964- req._t('Data') + '</a>' +
1965- '<h3>' + req._t('Update') + '</h3>' +
1966- renderRepoUpdate(req, repo, {key: id, value: msg}, true)),
1967- (msg.content.objects || msg.content.packs) &&
1968- pull.once('<h3>' + req._t('Commits') + '</h3>'),
1969- pull(commits, pull.map(function (commit) {
1970- return renderCommit(req, repo, commit)
1971- }))
1972- ]))
1973-}
1974-
1975-/* Blob */
1976-
1977-G.serveRepoBlob = function (req, repo, rev, path) {
1978- var self = this
1979- return readNext(function (cb) {
1980- repo.getFile(rev, path, function (err, object) {
1981- if (err) return cb(null, self.serveBlobNotFound(req, repo.id, err))
1982- var type = repo.isCommitHash(rev) ? 'Tree' : 'Branch'
1983- var pathLinks = path.length === 0 ? '' :
1984- ': ' + linkPath([repo.id, 'tree'], [rev].concat(path))
1985- var rawFilePath = [repo.id, 'raw', rev].concat(path)
1986- var dirPath = path.slice(0, path.length-1)
1987- var filename = path[path.length-1]
1988- var extension = getExtension(filename)
1989- var title = (path.length ? path.join('/') + ' · ' : '') +
1990- '%{author}/%{repo}' +
1991- (repo.head == 'refs/heads/' + rev ? '' : '@' + rev)
1992- cb(null, self.renderRepoPage(req, repo, 'code', rev, title, cat([
1993- pull.once('<section><form action="" method="get">' +
1994- '<h3>' + req._t(type) + ': ' + rev + ' '),
1995- revMenu(req, repo, rev),
1996- pull.once('</h3></form>'),
1997- type == 'Branch' && renderRepoLatest(req, repo, rev),
1998- pull.once('</section><section class="collapse">' +
1999- '<h3>' + req._t('Files') + pathLinks + '</h3>' +
2000- '<div>' + object.length + ' bytes' +
2001- '<span class="raw-link">' +
2002- link(rawFilePath, req._t('Raw')) + '</span>' +
2003- '</div></section>' +
2004- '<section>'),
2005- extension in imgMimes
2006- ? pull.once('<img src="' + encodeLink(rawFilePath) +
2007- '" alt="' + escapeHTML(filename) + '" />')
2008- : renderObjectData(object, filename, repo, rev, dirPath),
2009- pull.once('</section>')
2010- ])))
2011- })
2012- })
2013-}
2014-
2015819 G.serveBlobNotFound = function (req, repoId, err) {
2016820 return this.serveTemplate(req, req._t('error.BlobNotFound'), 404)(pull.once(
2017821 '<h2>' + req._t('error.BlobNotFound') + '</h2>' +
2018822 '<p>' + req._t('error.BlobNotFoundInRepo', {
2019- repo: link([repoId])
823 + repo: u.link([repoId])
2020824 }) + '</p>' +
2021- '<pre>' + escapeHTML(err.stack) + '</pre>'
825 + '<pre>' + u.escape(err.stack) + '</pre>'
2022826 ))
2023827 }
2024828
2025-/* Raw blob */
2026-
2027-G.serveRepoRaw = function (req, repo, branch, path) {
2028- return readNext(function (cb) {
2029- repo.getFile(branch, path, function (err, object) {
2030- if (err) return cb(null,
2031- this.serveBuffer(404, req._t('error.BlobNotFound')))
2032- var extension = getExtension(path[path.length-1])
2033- var contentType = imgMimes[extension]
2034- cb(null, pull(object.read, this.serveRaw(object.length, contentType)))
2035- })
2036- })
2037-}
2038-
2039829 G.serveRaw = function (length, contentType) {
2040830 var headers = {
2041831 'Content-Type': contentType || 'text/plain; charset=utf-8',
2042832 'Cache-Control': 'max-age=31536000'
@@ -2058,721 +848,12 @@
2058848 }
2059849
2060850 G.serveBlob = function (req, key) {
2061851 var self = this
2062- return readNext(function (cb) {
852 + return u.readNext(function (cb) {
2063853 self.getBlob(req, key, function (err, read) {
2064854 if (err) cb(null, self.serveError(req, err))
2065855 else if (!read) cb(null, self.serve404(req))
2066856 else cb(null, self.serveRaw()(read))
2067857 })
2068858 })
2069859 }
2070-
2071-/* Digs */
2072-
2073-G.serveRepoDigs = function (req, repo) {
2074- var self = this
2075- return readNext(function (cb) {
2076- var title = req._t('Digs') + ' · %{author}/%{repo}'
2077- self.getVotes(repo.id, function (err, votes) {
2078- cb(null, self.renderRepoPage(req, repo, null, null, title, cat([
2079- pull.once('<section><h3>' + req._t('Digs') + '</h3>' +
2080- '<div>' + req._t('Total') + ': ' + votes.upvotes + '</div>'),
2081- pull(
2082- pull.values(Object.keys(votes.upvoters)),
2083- paramap(function (feedId, cb) {
2084- self.about.getName(feedId, function (err, name) {
2085- if (err) return cb(err)
2086- cb(null, link([feedId], name))
2087- })
2088- }, 8),
2089- ul()
2090- ),
2091- pull.once('</section>')
2092- ])))
2093- })
2094- })
2095-}
2096-
2097-/* Forks */
2098-
2099-G.getForks = function (repo, includeSelf) {
2100- var self = this
2101- return pull(
2102- cat([
2103- includeSelf && readOnce(function (cb) {
2104- self.getMsg(repo.id, function (err, value) {
2105- cb(err, value && {key: repo.id, value: value})
2106- })
2107- }),
2108- this.ssb.links({
2109- dest: repo.id,
2110- values: true,
2111- rel: 'upstream'
2112- })
2113- ]),
2114- pull.filter(function (msg) {
2115- return msg.value.content && msg.value.content.type == 'git-repo'
2116- }),
2117- paramap(function (msg, cb) {
2118- self.getRepoFullName(msg.value.author, msg.key,
2119- function (err, repoName, authorName) {
2120- if (err) return cb(err)
2121- cb(null, {
2122- key: msg.key,
2123- value: msg.value,
2124- repoName: repoName,
2125- authorName: authorName
2126- })
2127- })
2128- }, 8)
2129- )
2130-}
2131-
2132-G.serveRepoForks = function (req, repo) {
2133- var hasForks
2134- var title = req._t('Forks') + ' · %{author}/%{repo}'
2135- return this.renderRepoPage(req, repo, null, null, title, cat([
2136- pull.once('<h3>' + req._t('Forks') + '</h3>'),
2137- pull(
2138- this.getForks(repo),
2139- pull.map(function (msg) {
2140- hasForks = true
2141- return '<section class="collapse">' +
2142- link([msg.value.author], msg.authorName) + ' / ' +
2143- link([msg.key], msg.repoName) +
2144- '<span class="right-bar">' +
2145- timestamp(msg.value.timestamp, req) +
2146- '</span></section>'
2147- })
2148- ),
2149- readOnce(function (cb) {
2150- cb(null, hasForks ? '' : req._t('NoForks'))
2151- })
2152- ]))
2153-}
2154-
2155-G.serveRepoForkPrompt = function (req, repo) {
2156- var title = req._t('Fork') + ' · %{author}/%{repo}'
2157- return this.renderRepoPage(req, repo, null, null, title, pull.once(
2158- '<form action="" method="post" onreset="history.back()">' +
2159- '<h3>' + req._t('ForkRepoPrompt') + '</h3>' +
2160- '<p>' + hiddenInputs({ id: repo.id }) +
2161- '<button class="btn open" type="submit" name="action" value="fork">' +
2162- req._t('Fork') +
2163- '</button>' +
2164- ' <button class="btn" type="reset">' +
2165- req._t('Cancel') + '</button>' +
2166- '</p></form>'
2167- ))
2168-}
2169-
2170- /* Issues */
2171-
2172-G.serveRepoIssues = function (req, repo, isPRs) {
2173- var self = this
2174- var count = 0
2175- var state = req._u.query.state || 'open'
2176- var newPath = isPRs ? [repo.id, 'compare'] : [repo.id, 'issues', 'new']
2177- var title = req._t('Issues') + ' · %{author}/%{repo}'
2178- var page = isPRs ? 'pulls' : 'issues'
2179- return self.renderRepoPage(req, repo, page, null, title, cat([
2180- pull.once(
2181- (self.isPublic ? '' :
2182- '<form class="right-bar" method="get"' +
2183- ' action="' + encodeLink(newPath) + '">' +
2184- '<button class="btn">&plus; ' +
2185- req._t(isPRs ? 'pullRequest.New' : 'issue.New') +
2186- '</button>' +
2187- '</form>') +
2188- '<h3>' + req._t(isPRs ? 'PullRequests' : 'Issues') + '</h3>' +
2189- nav([
2190- ['?', req._t('issues.Open'), 'open'],
2191- ['?state=closed', req._t('issues.Closed'), 'closed'],
2192- ['?state=all', req._t('issues.All'), 'all']
2193- ], state)),
2194- pull(
2195- (isPRs ? self.pullReqs : self.issues).list({
2196- repo: repo.id,
2197- project: repo.id,
2198- open: {open: true, closed: false}[state]
2199- }),
2200- pull.map(function (issue) {
2201- count++
2202- var state = (issue.open ? 'open' : 'closed')
2203- var stateStr = req._t(issue.open ?
2204- 'issue.state.Open' : 'issue.state.Closed')
2205- return '<section class="collapse">' +
2206- '<i class="issue-state issue-state-' + state + '"' +
2207- ' title="' + stateStr + '">◼</i> ' +
2208- '<a href="' + encodeLink(issue.id) + '">' +
2209- escapeHTML(issue.title) +
2210- '<span class="right-bar">' +
2211- new Date(issue.created_at).toLocaleString(req._locale) +
2212- '</span>' +
2213- '</a>' +
2214- '</section>'
2215- })
2216- ),
2217- readOnce(function (cb) {
2218- cb(null, count > 0 ? '' :
2219- '<p>' + req._t(isPRs ? 'NoPullRequests' : 'NoIssues') + '</p>')
2220- })
2221- ]))
2222-}
2223-
2224-/* New Issue */
2225-
2226-G.serveRepoNewIssue = function (req, repo, issueId, path) {
2227- var title = req._t('issue.New') + ' · %{author}/%{repo}'
2228- return this.renderRepoPage(req, repo, 'issues', null, title, pull.once(
2229- '<h3>' + req._t('issue.New') + '</h3>' +
2230- '<section><form action="" method="post">' +
2231- '<input type="hidden" name="action" value="new-issue">' +
2232- '<p><input class="wide-input" name="title" placeholder="' +
2233- req._t('issue.Title') + '" size="77" /></p>' +
2234- renderPostForm(req, repo, req._t('Description'), 8) +
2235- '<button type="submit" class="btn">' + req._t('Create') + '</button>' +
2236- '</form></section>'))
2237-}
2238-
2239-/* Issue */
2240-
2241-G.serveRepoIssue = function (req, repo, issue, path, postId) {
2242- var self = this
2243- var isAuthor = (self.myId == issue.author) || (self.myId == repo.feed)
2244- var newestMsg = {key: issue.id, value: {timestamp: issue.created_at}}
2245- var title = escapeHTML(issue.title) + ' · %{author}/%{repo}'
2246- return self.renderRepoPage(req, repo, 'issues', null, title, cat([
2247- pull.once(
2248- renderNameForm(req, !self.isPublic, issue.id, issue.title, 'issue-title',
2249- null, req._t('issue.Rename'),
2250- '<h3>' + link([issue.id], issue.title) + '</h3>') +
2251- '<code>' + issue.id + '</code>' +
2252- '<section class="collapse">' +
2253- (issue.open
2254- ? '<strong class="issue-status open">' +
2255- req._t('issue.state.Open') + '</strong>'
2256- : '<strong class="issue-status closed">' +
2257- req._t('issue.state.Closed') + '</strong>')),
2258- readOnce(function (cb) {
2259- self.about.getName(issue.author, function (err, authorName) {
2260- if (err) return cb(err)
2261- var authorLink = link([issue.author], authorName)
2262- cb(null, req._t('issue.Opened',
2263- {name: authorLink, datetime: timestamp(issue.created_at, req)}))
2264- })
2265- }),
2266- pull.once('<hr/>' + markdown(issue.text, repo) + '</section>'),
2267- // render posts and edits
2268- pull(
2269- self.ssb.links({
2270- dest: issue.id,
2271- values: true
2272- }),
2273- pull.unique('key'),
2274- self.addAuthorName(),
2275- sortMsgs(),
2276- pull.through(function (msg) {
2277- if (msg.value.timestamp > newestMsg.value.timestamp)
2278- newestMsg = msg
2279- }),
2280- pull.map(self.renderIssueActivityMsg.bind(self, req, repo, issue,
2281- req._t('issue.'), postId))
2282- ),
2283- self.isPublic ? pull.empty() : readOnce(function (cb) {
2284- cb(null, renderIssueCommentForm(req, issue, repo, newestMsg.key,
2285- isAuthor, req._t('issue.')))
2286- })
2287- ]))
2288-}
2289-
2290-G.renderIssueActivityMsg = function (req, repo, issue, type, postId, msg) {
2291- var authorLink = link([msg.value.author], msg.authorName)
2292- var msgHref = encodeLink(msg.key) + '#' + encodeURIComponent(msg.key)
2293- var msgTimeLink = '<a href="' + msgHref + '"' +
2294- ' name="' + escapeHTML(msg.key) + '">' +
2295- new Date(msg.value.timestamp).toLocaleString(req._locale) + '</a>'
2296- var c = msg.value.content
2297- switch (c.type) {
2298- case 'post':
2299- if (c.root == issue.id) {
2300- var changed = this.issues.isStatusChanged(msg, issue)
2301- return '<section class="collapse">' +
2302- (msg.key == postId ? '<div class="highlight">' : '') +
2303- '<tt class="right-bar item-id">' + msg.key + '</tt> ' +
2304- (changed == null ? authorLink : req._t(
2305- changed ? 'issue.Reopened' : 'issue.Closed',
2306- {name: authorLink, type: type})) +
2307- ' &middot; ' + msgTimeLink +
2308- (msg.key == postId ? '</div>' : '') +
2309- markdown(c.text, repo) +
2310- '</section>'
2311- } else {
2312- var text = c.text || (c.type + ' ' + msg.key)
2313- return '<section class="collapse mention-preview">' +
2314- req._t('issue.MentionedIn', {
2315- name: authorLink,
2316- type: type,
2317- post: '<a href="/' + msg.key + '#' + msg.key + '">' +
2318- String(text).substr(0, 140) + '</a>'
2319- }) + '</section>'
2320- }
2321- case 'issue':
2322- case 'pull-request':
2323- return '<section class="collapse mention-preview">' +
2324- req._t('issue.MentionedIn', {
2325- name: authorLink,
2326- type: type,
2327- post: link([msg.key], String(c.title || msg.key).substr(0, 140))
2328- }) + '</section>'
2329- case 'issue-edit':
2330- return '<section class="collapse">' +
2331- (msg.key == postId ? '<div class="highlight">' : '') +
2332- (c.title == null ? '' : req._t('issue.Renamed', {
2333- author: authorLink,
2334- type: type,
2335- name: '<q>' + escapeHTML(c.title) + '</q>'
2336- })) + ' &middot; ' + msgTimeLink +
2337- (msg.key == postId ? '</div>' : '') +
2338- '</section>'
2339- case 'git-update':
2340- var mention = this.issues.getMention(msg, issue)
2341- if (mention) {
2342- var commitLink = link([repo.id, 'commit', mention.object],
2343- mention.label || mention.object)
2344- return '<section class="collapse">' +
2345- req._t(mention.open ? 'issue.Reopened' : 'issue.Closed', {
2346- name: authorLink,
2347- type: type
2348- }) + ' &middot; ' + msgTimeLink + '<br/>' +
2349- commitLink +
2350- '</section>'
2351- } else if ((mention = getMention(msg, issue.id))) {
2352- var commitLink = link(mention.object ?
2353- [repo.id, 'commit', mention.object] : [msg.key],
2354- mention.label || mention.object || msg.key)
2355- return '<section class="collapse">' +
2356- req._t('issue.Mentioned', {
2357- name: authorLink,
2358- type: type
2359- }) + ' &middot; ' + msgTimeLink + '<br/>' +
2360- commitLink +
2361- '</section>'
2362- } else {
2363- // fallthrough
2364- }
2365-
2366- default:
2367- return '<section class="collapse">' +
2368- authorLink +
2369- ' &middot; ' + msgTimeLink +
2370- json(c) +
2371- '</section>'
2372- }
2373-}
2374-
2375- function renderIssueCommentForm(req, issue, repo, branch, isAuthor, type) {
2376- return '<section><form action="" method="post">' +
2377- '<input type="hidden" name="action" value="comment">' +
2378- '<input type="hidden" name="id" value="' + issue.id + '">' +
2379- '<input type="hidden" name="issue" value="' + issue.id + '">' +
2380- '<input type="hidden" name="repo" value="' + repo.id + '">' +
2381- '<input type="hidden" name="branch" value="' + branch + '">' +
2382- renderPostForm(req, repo) +
2383- '<input type="submit" class="btn open" value="' +
2384- req._t('issue.Comment') + '" />' +
2385- (isAuthor ?
2386- '<input type="submit" class="btn"' +
2387- ' name="' + (issue.open ? 'close' : 'open') + '"' +
2388- ' value="' + req._t(
2389- issue.open ? 'issue.Close' : 'issue.Reopen', {type: type}
2390- ) + '"/>' : '') +
2391- '</form></section>'
2392- }
2393-
2394- /* Pull Request */
2395-
2396-G.serveRepoPullReq = function (req, repo, pr, path, postId) {
2397- var self = this
2398- var headRepo, authorLink
2399- var page = path[0] || 'activity'
2400- var title = escapeHTML(pr.title) + ' · %{author}/%{repo}'
2401- return self.renderRepoPage(req, repo, 'pulls', null, title, cat([
2402- pull.once('<div class="pull-request">' +
2403- renderNameForm(req, !self.isPublic, pr.id, pr.title, 'issue-title', null,
2404- req._t('pullRequest.Rename'),
2405- '<h3>' + link([pr.id], pr.title) + '</h3>') +
2406- '<code>' + pr.id + '</code>'),
2407- readOnce(function (cb) {
2408- var done = multicb({ pluck: 1, spread: true })
2409- var gotHeadRepo = done()
2410- self.about.getName(pr.author, done())
2411- var sameRepo = (pr.headRepo == pr.baseRepo)
2412- self.getRepo(pr.headRepo, function (err, headRepo) {
2413- if (err) return cb(err)
2414- self.getRepoName(headRepo.feed, headRepo.id, done())
2415- self.about.getName(headRepo.feed, done())
2416- gotHeadRepo(null, Repo(headRepo))
2417- })
2418-
2419- done(function (err, _headRepo, issueAuthorName,
2420- headRepoName, headRepoAuthorName) {
2421- if (err) return cb(err)
2422- headRepo = _headRepo
2423- authorLink = link([pr.author], issueAuthorName)
2424- var repoLink = link([pr.headRepo], headRepoName)
2425- var headRepoAuthorLink = link([headRepo.feed], headRepoAuthorName)
2426- var headRepoLink = link([headRepo.id], headRepoName)
2427- var headBranchLink = link([headRepo.id, 'tree', pr.headBranch])
2428- var baseBranchLink = link([repo.id, 'tree', pr.baseBranch])
2429- cb(null, '<section class="collapse">' +
2430- '<strong class="issue-status ' +
2431- (pr.open ? 'open' : 'closed') + '">' +
2432- req._t(pr.open ? 'issue.state.Open' : 'issue.state.Closed') +
2433- '</strong> ' +
2434- req._t('pullRequest.WantToMerge', {
2435- name: authorLink,
2436- base: '<code>' + baseBranchLink + '</code>',
2437- head: (sameRepo ?
2438- '<code>' + headBranchLink + '</code>' :
2439- '<code class="bgslash">' +
2440- headRepoAuthorLink + ' / ' +
2441- headRepoLink + ' / ' +
2442- headBranchLink + '</code>')
2443- }) + '</section>')
2444- })
2445- }),
2446- pull.once(
2447- nav([
2448- [[pr.id], req._t('Discussion'), 'activity'],
2449- [[pr.id, 'commits'], req._t('Commits'), 'commits'],
2450- [[pr.id, 'files'], req._t('Files'), 'files']
2451- ], page)),
2452- readNext(function (cb) {
2453- if (page == 'commits')
2454- self.renderPullReqCommits(req, pr, repo, headRepo, cb)
2455- else if (page == 'files')
2456- self.renderPullReqFiles(req, pr, repo, headRepo, cb)
2457- else cb(null,
2458- self.renderPullReqActivity(req, pr, repo, headRepo, authorLink, postId))
2459- })
2460- ]))
2461-}
2462-
2463-G.renderPullReqCommits = function (req, pr, baseRepo, headRepo, cb) {
2464- var self = this
2465- self.pullReqs.getRevs(pr.id, function (err, revs) {
2466- if (err) return cb(null, renderError(err))
2467- cb(null, cat([
2468- pull.once('<section>'),
2469- self.renderCommitLog(req, baseRepo, revs.base, headRepo, revs.head),
2470- pull.once('</section>')
2471- ]))
2472- })
2473-}
2474-
2475-G.renderPullReqFiles = function (req, pr, baseRepo, headRepo, cb) {
2476- this.pullReqs.getRevs(pr.id, function (err, revs) {
2477- if (err) return cb(null, renderError(err))
2478- cb(null, cat([
2479- pull.once('<section>'),
2480- renderDiffStat(req,
2481- [baseRepo, headRepo], [revs.base, revs.head]),
2482- pull.once('</section>')
2483- ]))
2484- })
2485-}
2486-
2487-G.renderPullReqActivity = function (req, pr, repo, headRepo, authorLink, postId) {
2488- var self = this
2489- var msgTimeLink = link([pr.id],
2490- new Date(pr.created_at).toLocaleString(req._locale))
2491- var newestMsg = {key: pr.id, value: {timestamp: pr.created_at}}
2492- var isAuthor = (self.myId == pr.author) || (self.myId == repo.feed)
2493- return cat([
2494- readOnce(function (cb) {
2495- cb(null,
2496- '<section class="collapse">' +
2497- authorLink + ' &middot; ' + msgTimeLink +
2498- markdown(pr.text, repo) + '</section>')
2499- }),
2500- // render posts, edits, and updates
2501- pull(
2502- many([
2503- self.ssb.links({
2504- dest: pr.id,
2505- values: true
2506- }),
2507- readNext(function (cb) {
2508- cb(null, pull(
2509- self.ssb.links({
2510- dest: headRepo.id,
2511- source: headRepo.feed,
2512- rel: 'repo',
2513- values: true,
2514- reverse: true
2515- }),
2516- pull.take(function (link) {
2517- return link.value.timestamp > pr.created_at
2518- }),
2519- pull.filter(function (link) {
2520- return link.value.content.type == 'git-update'
2521- && ('refs/heads/' + pr.headBranch) in link.value.content.refs
2522- })
2523- ))
2524- })
2525- ]),
2526- self.addAuthorName(),
2527- pull.unique('key'),
2528- pull.through(function (msg) {
2529- if (msg.value.timestamp > newestMsg.value.timestamp)
2530- newestMsg = msg
2531- }),
2532- sortMsgs(),
2533- pull.map(function (item) {
2534- if (item.value.content.type == 'git-update')
2535- return self.renderBranchUpdate(req, pr, item)
2536- return self.renderIssueActivityMsg(req, repo, pr,
2537- req._t('pull request'), postId, item)
2538- })
2539- ),
2540- !self.isPublic && isAuthor && pr.open && pull.once(
2541- '<section class="merge-instructions">' +
2542- '<input type="checkbox" class="toggle" id="merge-instructions"/>' +
2543- '<h4><label for="merge-instructions" class="toggle-link"><a>' +
2544- req._t('mergeInstructions.MergeViaCmdLine') +
2545- '</a></label></h4>' +
2546- '<div class="contents">' +
2547- '<p>' + req._t('mergeInstructions.CheckOut') + '</p>' +
2548- '<pre>' +
2549- 'git fetch ssb://' + escapeHTML(pr.headRepo) + ' ' +
2550- escapeHTML(pr.headBranch) + '\n' +
2551- 'git checkout -b ' + escapeHTML(pr.headBranch) + ' FETCH_HEAD' +
2552- '</pre>' +
2553- '<p>' + req._t('mergeInstructions.MergeAndPush') + '</p>' +
2554- '<pre>' +
2555- 'git checkout ' + escapeHTML(pr.baseBranch) + '\n' +
2556- 'git merge ' + escapeHTML(pr.headBranch) + '\n' +
2557- 'git push ssb ' + escapeHTML(pr.baseBranch) +
2558- '</pre>' +
2559- '</div></section>'),
2560- !self.isPublic && readOnce(function (cb) {
2561- cb(null, renderIssueCommentForm(req, pr, repo, newestMsg.key, isAuthor,
2562- req._t('pull request')))
2563- })
2564- ])
2565-}
2566-
2567-G.renderBranchUpdate = function (req, pr, msg) {
2568- var authorLink = link([msg.value.author], msg.authorName)
2569- var msgLink = link([msg.key],
2570- new Date(msg.value.timestamp).toLocaleString(req._locale))
2571- var rev = msg.value.content.refs['refs/heads/' + pr.headBranch]
2572- if (!rev)
2573- return '<section class="collapse">' +
2574- req._t('NameDeletedBranch', {
2575- name: authorLink,
2576- branch: '<code>' + pr.headBranch + '</code>'
2577- }) + ' &middot; ' + msgLink +
2578- '</section>'
2579-
2580- var revLink = link([pr.headRepo, 'commit', rev], rev.substr(0, 8))
2581- return '<section class="collapse">' +
2582- req._t('NameUpdatedBranch', {
2583- name: authorLink,
2584- rev: '<code>' + revLink + '</code>'
2585- }) + ' &middot; ' + msgLink +
2586- '</section>'
2587-}
2588-
2589-/* Compare changes */
2590-
2591-G.serveRepoCompare = function (req, repo) {
2592- var self = this
2593- var query = req._u.query
2594- var base
2595- var count = 0
2596- var title = req._t('CompareChanges') + ' · %{author}/%{repo}'
2597-
2598- return self.renderRepoPage(req, repo, 'pulls', null, title, cat([
2599- pull.once('<h3>' + req._t('CompareChanges') + '</h3>' +
2600- '<form action="' + encodeLink(repo.id) + '/comparing" method="get">' +
2601- '<section>'),
2602- pull.once(req._t('BaseBranch') + ': '),
2603- readNext(function (cb) {
2604- if (query.base) gotBase(null, query.base)
2605- else repo.getSymRef('HEAD', true, gotBase)
2606- function gotBase(err, ref) {
2607- if (err) return cb(err)
2608- cb(null, branchMenu(repo, 'base', base = ref || 'HEAD'))
2609- }
2610- }),
2611- pull.once('<br/>' + req._t('ComparisonRepoBranch') + ':'),
2612- pull(
2613- self.getForks(repo, true),
2614- pull.asyncMap(function (msg, cb) {
2615- self.getRepo(msg.key, function (err, repo) {
2616- if (err) return cb(err)
2617- cb(null, {
2618- msg: msg,
2619- repo: repo
2620- })
2621- })
2622- }),
2623- pull.map(renderFork),
2624- pull.flatten()
2625- ),
2626- pull.once('</section>'),
2627- readOnce(function (cb) {
2628- cb(null, count == 0 ? req._t('NoBranches') :
2629- '<button type="submit" class="btn">' +
2630- req._t('Compare') + '</button>')
2631- }),
2632- pull.once('</form>')
2633- ]))
2634-
2635- function renderFork(fork) {
2636- return pull(
2637- fork.repo.refs(),
2638- pull.map(function (ref) {
2639- var m = /^refs\/([^\/]*)\/(.*)$/.exec(ref.name) || [,ref.name]
2640- return {
2641- type: m[1],
2642- name: m[2],
2643- value: ref.value
2644- }
2645- }),
2646- pull.filter(function (ref) {
2647- return ref.type == 'heads'
2648- && !(ref.name == base && fork.msg.key == repo.id)
2649- }),
2650- pull.map(function (ref) {
2651- var branchLink = link([fork.msg.key, 'tree', ref.name], ref.name)
2652- var authorLink = link([fork.msg.value.author], fork.msg.authorName)
2653- var repoLink = link([fork.msg.key], fork.msg.repoName)
2654- var value = fork.msg.key + ':' + ref.name
2655- count++
2656- return '<div class="bgslash">' +
2657- '<input type="radio" name="head"' +
2658- ' value="' + escapeHTML(value) + '"' +
2659- (query.head == value ? ' checked="checked"' : '') + '> ' +
2660- authorLink + ' / ' + repoLink + ' / ' + branchLink + '</div>'
2661- })
2662- )
2663- }
2664-}
2665-
2666-G.serveRepoComparing = function (req, repo) {
2667- var self = this
2668- var query = req._u.query
2669- var baseBranch = query.base
2670- var s = (query.head || '').split(':')
2671-
2672- if (!s || !baseBranch)
2673- return self.serveRedirect(req, encodeLink([repo.id, 'compare']))
2674-
2675- var headRepoId = s[0]
2676- var headBranch = s[1]
2677- var baseLink = link([repo.id, 'tree', baseBranch])
2678- var headBranchLink = link([headRepoId, 'tree', headBranch])
2679- var backHref = encodeLink([repo.id, 'compare']) + req._u.search
2680- var title = req._t(query.expand ? 'OpenPullRequest': 'ComparingChanges')
2681- var pageTitle = title + ' · %{author}/%{repo}'
2682-
2683- return self.renderRepoPage(req, repo, 'pulls', null, pageTitle, cat([
2684- pull.once('<h3>' + title + '</h3>'),
2685- readNext(function (cb) {
2686- self.getRepo(headRepoId, function (err, headRepo) {
2687- if (err) return cb(err)
2688- self.getRepoFullName(headRepo.feed, headRepo.id,
2689- function (err, repoName, authorName) {
2690- if (err) return cb(err)
2691- cb(null, renderRepoInfo(Repo(headRepo), repoName, authorName))
2692- }
2693- )
2694- })
2695- })
2696- ]))
2697-
2698- function renderRepoInfo(headRepo, headRepoName, headRepoAuthorName) {
2699- var authorLink = link([headRepo.feed], headRepoAuthorName)
2700- var repoLink = link([headRepoId], headRepoName)
2701- return cat([
2702- pull.once('<section>' +
2703- req._t('Base') + ': ' + baseLink + '<br/>' +
2704- req._t('Head') + ': ' +
2705- '<span class="bgslash">' + authorLink + ' / ' + repoLink +
2706- ' / ' + headBranchLink + '</span>' +
2707- '</section>' +
2708- (query.expand ? '<section><form method="post" action="">' +
2709- hiddenInputs({
2710- action: 'new-pull',
2711- branch: baseBranch,
2712- head_repo: headRepoId,
2713- head_branch: headBranch
2714- }) +
2715- '<input class="wide-input" name="title"' +
2716- ' placeholder="' + req._t('Title') + '" size="77"/>' +
2717- renderPostForm(req, repo, req._t('Description'), 8) +
2718- '<button type="submit" class="btn open">' +
2719- req._t('Create') + '</button>' +
2720- '</form></section>'
2721- : '<section><form method="get" action="">' +
2722- hiddenInputs({
2723- base: baseBranch,
2724- head: query.head
2725- }) +
2726- '<button class="btn open" type="submit" name="expand" value="1">' +
2727- '<i>⎇</i> ' + req._t('CreatePullRequest') + '</button> ' +
2728- '<a href="' + backHref + '">' + req._t('Back') + '</a>' +
2729- '</form></section>') +
2730- '<div id="commits"></div>' +
2731- '<div class="tab-links">' +
2732- '<a href="#" id="files-link">' + req._t('FilesChanged') + '</a> ' +
2733- '<a href="#commits" id="commits-link">' +
2734- req._t('Commits') + '</a>' +
2735- '</div>' +
2736- '<section id="files-tab">'),
2737- renderDiffStat(req, [repo, headRepo], [baseBranch, headBranch]),
2738- pull.once('</section>' +
2739- '<section id="commits-tab">'),
2740- self.renderCommitLog(req, repo, baseBranch, headRepo, headBranch),
2741- pull.once('</section>')
2742- ])
2743- }
2744-}
2745-
2746-G.renderCommitLog = function (req, baseRepo, baseBranch, headRepo, headBranch) {
2747- return cat([
2748- pull.once('<table class="compare-commits">'),
2749- readNext(function (cb) {
2750- baseRepo.resolveRef(baseBranch, function (err, baseBranchRev) {
2751- if (err) return cb(err)
2752- var currentDay
2753- return cb(null, pull(
2754- headRepo.readLog(headBranch),
2755- pull.take(function (rev) { return rev != baseBranchRev }),
2756- pullReverse(),
2757- paramap(headRepo.getCommitParsed.bind(headRepo), 8),
2758- pull.map(function (commit) {
2759- var commitPath = [headRepo.id, 'commit', commit.id]
2760- var commitIdShort = '<tt>' + commit.id.substr(0, 8) + '</tt>'
2761- var day = Math.floor(commit.author.date / 86400000)
2762- var dateRow = day == currentDay ? '' :
2763- '<tr><th colspan=3 class="date-info">' +
2764- commit.author.date.toLocaleDateString(req._locale) +
2765- '</th><tr>'
2766- currentDay = day
2767- return dateRow + '<tr>' +
2768- '<td>' + escapeHTML(commit.author.name) + '</td>' +
2769- '<td>' + link(commitPath, commit.title) + '</td>' +
2770- '<td>' + link(commitPath, commitIdShort, true) + '</td>' +
2771- '</tr>'
2772- })
2773- ))
2774- })
2775- }),
2776- pull.once('</table>')
2777- ])
2778-}
about.jsView
@@ -1,111 +1,0 @@
1-/* ssb-about
2- * factored out of ssb-notifier
3- *
4- * TODO:
5- * - publish as own module
6- * - handle live updates and reconnecting
7- * - deprecate when ssb-names is used in scuttlebot
8- */
9-
10-var pull = require('pull-stream')
11-var cat = require('pull-cat')
12-var asyncMemo = require('asyncmemo')
13-
14-module.exports = function (sbot, id) {
15- var getAbout = asyncMemo(getAboutFull, sbot, id)
16-
17- getAbout.getName = function (id, cb) {
18- getAbout(id, function (err, about) {
19- cb(err, about && about.name)
20- })
21- }
22-
23- getAbout.getImage = function (id, cb) {
24- getAbout(id, function (err, about) {
25- cb(err, about && about.image)
26- })
27- }
28-
29- return getAbout
30-}
31-
32-function truncate(str, len) {
33- str = String(str)
34- return str.length < len ? str : str.substr(0, len-1) + '…'
35-}
36-
37-// Get About info (name and icon) for a feed.
38-function getAboutFull(sbot, source, dest, cb) {
39- var info = {}
40- var target = dest.target || dest
41- var owner = dest.owner || dest
42-
43- pull(
44- cat([
45- // First get About info that we gave them.
46- sbot.links({
47- source: source,
48- dest: target,
49- rel: 'about',
50- values: true,
51- reverse: true
52- }),
53- // If that isn't enough, then get About info that they gave themselves.
54- sbot.links({
55- source: owner,
56- dest: target,
57- rel: 'about',
58- values: true,
59- reverse: true
60- }),
61- ]),
62- pull.filter(function (msg) {
63- return msg && msg.value.content
64- }),
65- pull.drain(function (msg) {
66- if (info.name && info.image)
67- return gotIt()
68- var c = msg.value.content
69- if (!info.name && c.name)
70- info.name = c.name
71- if (!info.image && c.image)
72- info.image = c.image.link
73- }, gotIt)
74- )
75-
76- function gotIt(err) {
77- if (!cb) {
78- if (err && err !== true)
79- console.error(err)
80- return
81- }
82- var _cb = cb
83- cb = null
84- if (err && err !== true) return _cb(err)
85- if (!info.name) info.name = truncate(target, 20)
86- _cb(null, info)
87- }
88-
89- // Keep updated as changes are made
90- pull(
91- sbot.links({
92- dest: target,
93- rel: 'about',
94- live: true,
95- values: true,
96- gte: Date.now()
97- }),
98- pull.drain(function (msg) {
99- var c = msg.value.content
100- if (msg.value.author == source || msg.value.author == owner) {
101- // TODO: give about from source (self) priority over about from owner
102- if (c.name)
103- info.name = c.name
104- if (c.image)
105- info.image = c.image
106- }
107- }, function (err) {
108- if (err) console.error(err)
109- })
110- )
111-}
i18n.jsView
@@ -1,58 +1,0 @@
1-var Polyglot = require('node-polyglot')
2-var path = require('path')
3-var fs = require('fs')
4-var asyncMemo = require('asyncmemo')
5-
6-var i18n = module.exports = {
7- dir: path.join(__dirname, 'locale'),
8- fallback: 'en',
9-
10- getCatalog: asyncMemo(function (locale, cb) {
11- if (!locale) return cb()
12- var filename = path.join(i18n.dir, locale.replace(/\//g, '') + '.json')
13- fs.access(filename, fs.R_OK, function (err) {
14- if (err) return cb()
15- fs.readFile(filename, onRead)
16- })
17- function onRead(err, data) {
18- if (err) return cb(err)
19- var phrases
20- try { phrases = JSON.parse(data) }
21- catch(e) { return cb(e) }
22- var polyglot = new Polyglot({locale: locale, phrases: phrases})
23- var t = polyglot.t.bind(polyglot)
24- t.locale = polyglot.currentLocale
25- cb(null, t)
26- }
27- }),
28-
29- pickCatalog: function (acceptLocales, locale, cb) {
30- i18n.getCatalog(locale, function (err, phrases) {
31- if (err || phrases) return cb(err, phrases)
32- var locales = String(acceptLocales).split(/, */).map(function (item) {
33- return item.split(';')[0]
34- })
35- i18n.pickCatalog2(locales.concat(
36- process.env.LANG && process.env.LANG.replace(/[._].*/, ''),
37- i18n.fallback
38- ).reverse(), cb)
39- })
40- },
41-
42- pickCatalog2: function (locales, cb) {
43- if (!locales.length) return cb(null, new Error('No locale'))
44- i18n.getCatalog(locales.pop(), function (err, phrases) {
45- if (err || phrases) return cb(err, phrases)
46- i18n.pickCatalog2(locales, cb)
47- })
48- },
49-
50- listLocales: function (cb) {
51- fs.readdir(dir, function (err, files) {
52- if (err) return cb(err)
53- cb(null, files.map(function (filename) {
54- return filename.replace(/\.json$/, '')
55- }))
56- })
57- }
58-}
lib/about.jsView
@@ -1,0 +1,111 @@
1 +/* ssb-about
2 + * factored out of ssb-notifier
3 + *
4 + * TODO:
5 + * - publish as own module
6 + * - handle live updates and reconnecting
7 + * - deprecate when ssb-names is used in scuttlebot
8 + */
9 +
10 +var pull = require('pull-stream')
11 +var cat = require('pull-cat')
12 +var asyncMemo = require('asyncmemo')
13 +
14 +module.exports = function (sbot, id) {
15 + var getAbout = asyncMemo(getAboutFull, sbot, id)
16 +
17 + getAbout.getName = function (id, cb) {
18 + getAbout(id, function (err, about) {
19 + cb(err, about && about.name)
20 + })
21 + }
22 +
23 + getAbout.getImage = function (id, cb) {
24 + getAbout(id, function (err, about) {
25 + cb(err, about && about.image)
26 + })
27 + }
28 +
29 + return getAbout
30 +}
31 +
32 +function truncate(str, len) {
33 + str = String(str)
34 + return str.length < len ? str : str.substr(0, len-1) + '…'
35 +}
36 +
37 +// Get About info (name and icon) for a feed.
38 +function getAboutFull(sbot, source, dest, cb) {
39 + var info = {}
40 + var target = dest.target || dest
41 + var owner = dest.owner || dest
42 +
43 + pull(
44 + cat([
45 + // First get About info that we gave them.
46 + sbot.links({
47 + source: source,
48 + dest: target,
49 + rel: 'about',
50 + values: true,
51 + reverse: true
52 + }),
53 + // If that isn't enough, then get About info that they gave themselves.
54 + sbot.links({
55 + source: owner,
56 + dest: target,
57 + rel: 'about',
58 + values: true,
59 + reverse: true
60 + }),
61 + ]),
62 + pull.filter(function (msg) {
63 + return msg && msg.value.content
64 + }),
65 + pull.drain(function (msg) {
66 + if (info.name && info.image)
67 + return gotIt()
68 + var c = msg.value.content
69 + if (!info.name && c.name)
70 + info.name = c.name
71 + if (!info.image && c.image)
72 + info.image = c.image.link
73 + }, gotIt)
74 + )
75 +
76 + function gotIt(err) {
77 + if (!cb) {
78 + if (err && err !== true)
79 + console.error(err)
80 + return
81 + }
82 + var _cb = cb
83 + cb = null
84 + if (err && err !== true) return _cb(err)
85 + if (!info.name) info.name = truncate(target, 20)
86 + _cb(null, info)
87 + }
88 +
89 + // Keep updated as changes are made
90 + pull(
91 + sbot.links({
92 + dest: target,
93 + rel: 'about',
94 + live: true,
95 + values: true,
96 + gte: Date.now()
97 + }),
98 + pull.drain(function (msg) {
99 + var c = msg.value.content
100 + if (msg.value.author == source || msg.value.author == owner) {
101 + // TODO: give about from source (self) priority over about from owner
102 + if (c.name)
103 + info.name = c.name
104 + if (c.image)
105 + info.image = c.image
106 + }
107 + }, function (err) {
108 + if (err) console.error(err)
109 + })
110 + )
111 +}
lib/i18n.jsView
@@ -1,0 +1,67 @@
1 +var Polyglot = require('node-polyglot')
2 +var path = require('path')
3 +var fs = require('fs')
4 +var asyncMemo = require('asyncmemo')
5 +
6 +module.exports = function (localesDir, fallback) {
7 + return new I18n(localesDir, fallback)
8 +}
9 +
10 +function I18n(dir, fallback) {
11 + this.dir = dir
12 + this.fallback = fallback
13 +}
14 +
15 +I18n.prototype = {
16 + constructor: I18n,
17 +
18 + getCatalog: asyncMemo(function (locale, cb) {
19 + var self = this
20 + if (!locale) return cb.call(self)
21 + var filename = path.join(this.dir, locale.replace(/\//g, '') + '.json')
22 + fs.access(filename, fs.R_OK, function (err) {
23 + if (err) return cb.call(self)
24 + fs.readFile(filename, onRead)
25 + })
26 + function onRead(err, data) {
27 + if (err) return cb.call(self, err)
28 + var phrases
29 + try { phrases = JSON.parse(data) }
30 + catch(e) { return cb.call(self, e) }
31 + var polyglot = new Polyglot({locale: locale, phrases: phrases})
32 + var t = polyglot.t.bind(polyglot)
33 + t.locale = polyglot.currentLocale
34 + cb.call(self, null, t)
35 + }
36 + }),
37 +
38 + pickCatalog: function (acceptLocales, locale, cb) {
39 + this.getCatalog(locale, function (err, phrases) {
40 + if (err || phrases) return cb(err, phrases)
41 + var locales = String(acceptLocales).split(/, */).map(function (item) {
42 + return item.split(';')[0]
43 + })
44 + this.pickCatalog2(locales.concat(
45 + process.env.LANG && process.env.LANG.replace(/[._].*/, ''),
46 + this.fallback
47 + ).reverse(), cb)
48 + })
49 + },
50 +
51 + pickCatalog2: function (locales, cb) {
52 + if (!locales.length) return cb(null, new Error('No locale'))
53 + this.getCatalog(locales.pop(), function (err, phrases) {
54 + if (err || phrases) return cb(err, phrases)
55 + this.pickCatalog2(locales, cb)
56 + })
57 + },
58 +
59 + listLocales: function (cb) {
60 + fs.readdir(dir, function (err, files) {
61 + if (err) return cb(err)
62 + cb(null, files.map(function (filename) {
63 + return filename.replace(/\.json$/, '')
64 + }))
65 + })
66 + }
67 +}
lib/markdown.jsView
@@ -1,0 +1,68 @@
1 +var path = require('path')
2 +var marked = require('ssb-marked')
3 +var ref = require('ssb-ref')
4 +var u = require('./util')
5 +
6 +// render links to git objects and ssb objects
7 +var blockRenderer = new marked.Renderer()
8 +blockRenderer.urltransform = function (url) {
9 + if (ref.isLink(url))
10 + return u.encodeLink(url)
11 + if (/^[0-9a-f]{40}$/.test(url) && this.options.repo)
12 + return u.encodeLink([this.options.repo.id, 'commit', url])
13 + return url
14 +}
15 +
16 +blockRenderer.image = function (href, title, text) {
17 + href = href.replace(/^&amp;/, '&')
18 + var url
19 + if (ref.isBlobId(href))
20 + url = u.encodeLink(href)
21 + else if (/^https?:\/\//.test(href))
22 + url = href
23 + else if (this.options.repo && this.options.rev && this.options.path)
24 + url = path.join('/', encodeURIComponent(this.options.repo.id),
25 + 'raw', this.options.rev, this.options.path.join('/'), href)
26 + else
27 + return text
28 + return '<img src="' + u.escape(url) + '" alt="' + text + '"' +
29 + (title ? ' title="' + title + '"' : '') + '/>'
30 +}
31 +
32 +blockRenderer.mention = function (preceding, id) {
33 + // prevent broken name mention
34 + if (id[0] == '@' && !ref.isFeed(id))
35 + return (preceding||'') + u.escape(id)
36 +
37 + return marked.Renderer.prototype.mention.call(this, preceding, id)
38 +}
39 +
40 +marked.setOptions({
41 + gfm: true,
42 + mentions: true,
43 + tables: true,
44 + breaks: true,
45 + pedantic: false,
46 + sanitize: true,
47 + smartLists: true,
48 + smartypants: false,
49 + highlight: u.highlight,
50 + renderer: blockRenderer
51 +})
52 +
53 +// hack to make git link mentions work
54 +var mdRules = new marked.InlineLexer(1, marked.defaults).rules
55 +mdRules.mention =
56 + /^(\s)?([@%&][A-Za-z0-9\._\-+=\/]*[A-Za-z0-9_\-+=\/]|[0-9a-f]{40})/
57 +mdRules.text = /^[\s\S]+?(?=[\\<!\[_*`]| {2,}\n| [@%&]|[0-9a-f]{40}|$)/
58 +
59 +module.exports = function (text, options, cb) {
60 + if (!text) return ''
61 + if (typeof text != 'string') text = String(text)
62 + if (!options) options = {}
63 + else if (options.id) options = {repo: options}
64 + if (!options.rev) options.rev = 'HEAD'
65 + if (!options.path) options.path = []
66 +
67 + return marked(text, options, cb)
68 +}
lib/paginate.jsView
@@ -1,0 +1,43 @@
1 +module.exports = function (onFirst, through, onLast, onEmpty) {
2 + var ended, last, first = true, queue = []
3 + return function (read) {
4 + var mappedRead = through(function (end, cb) {
5 + if (ended = end) return read(ended, cb)
6 + if (queue.length)
7 + return cb(null, queue.shift())
8 + read(null, function (end, data) {
9 + if (end) return cb(end)
10 + last = data
11 + cb(null, data)
12 + })
13 + })
14 + return function (end, cb) {
15 + var tmp
16 + if (ended) return cb(ended)
17 + if (ended = end) return read(ended, cb)
18 + if (first)
19 + return read(null, function (end, data) {
20 + if (ended = end) {
21 + if (end === true && onEmpty)
22 + return onEmpty(cb)
23 + return cb(ended)
24 + }
25 + first = false
26 + last = data
27 + queue.push(data)
28 + if (onFirst)
29 + onFirst(data, cb)
30 + else
31 + mappedRead(null, cb)
32 + })
33 + mappedRead(null, function (end, data) {
34 + if (ended = end) {
35 + if (end === true && last)
36 + return onLast(last, cb)
37 + }
38 + cb(end, data)
39 + })
40 + }
41 + }
42 +}
43 +
lib/repos/index.jsView
@@ -1,0 +1,938 @@
1 +var url = require('url')
2 +var pull = require('pull-stream')
3 +var cat = require('pull-cat')
4 +var paramap = require('pull-paramap')
5 +var multicb = require('multicb')
6 +var JsDiff = require('diff')
7 +var GitRepo = require('pull-git-repo')
8 +var gitPack = require('pull-git-pack')
9 +var u = require('../util')
10 +var paginate = require('../paginate')
11 +var markdown = require('../markdown')
12 +var forms = require('../forms')
13 +
14 +module.exports = function (web) {
15 + return new RepoRoutes(web)
16 +}
17 +
18 +function RepoRoutes(web) {
19 + this.web = web
20 + this.issues = require('./issues')(this, web)
21 + this.pulls = require('./pulls')(this, web)
22 +}
23 +
24 +var R = RepoRoutes.prototype
25 +
26 +function getRepoObjectString(repo, id, cb) {
27 + if (!id) return cb(null, '')
28 + repo.getObjectFromAny(id, function (err, obj) {
29 + if (err) return cb(err)
30 + u.readObjectString(obj, cb)
31 + })
32 +}
33 +
34 +function table(props) {
35 + return function (read) {
36 + return cat([
37 + pull.once('<table' + (props ? ' ' + props : '') + '>'),
38 + pull(
39 + read,
40 + pull.map(function (row) {
41 + return row ? '<tr>' + row.map(function (cell) {
42 + return '<td>' + cell + '</td>'
43 + }).join('') + '</tr>' : ''
44 + })
45 + ),
46 + pull.once('</table>')
47 + ])
48 + }
49 +}
50 +
51 +function ul(props) {
52 + return function (read) {
53 + return cat([
54 + pull.once('<ul' + (props ? ' ' + props : '') + '>'),
55 + pull(read, pull.map(function (li) { return '<li>' + li + '</li>' })),
56 + pull.once('</ul>')
57 + ])
58 + }
59 +}
60 +
61 +function hiddenInputs(values) {
62 + return Object.keys(values).map(function (key) {
63 + return '<input type="hidden"' +
64 + ' name="' + u.escape(key) + '"' +
65 + ' value="' + u.escape(values[key]) + '"/>'
66 + }).join('')
67 +}
68 +
69 +/* Repo */
70 +
71 +R.serveRepoPage = function (req, repo, path) {
72 + var self = this
73 + var defaultBranch = 'master'
74 + var query = req._u.query
75 +
76 + if (query.rev != null) {
77 + // Allow navigating revs using GET query param.
78 + // Replace the branch in the path with the rev query value
79 + path[0] = path[0] || 'tree'
80 + path[1] = query.rev
81 + req._u.pathname = u.encodeLink([repo.id].concat(path))
82 + delete req._u.query.rev
83 + delete req._u.search
84 + return self.web.serveRedirect(req, url.format(req._u))
85 + }
86 +
87 + // get branch
88 + return path[1] ?
89 + R_serveRepoPage2.call(self, req, repo, path) :
90 + u.readNext(function (cb) {
91 + // TODO: handle this in pull-git-repo or ssb-git-repo
92 + repo.getSymRef('HEAD', true, function (err, ref) {
93 + if (err) return cb(err)
94 + repo.resolveRef(ref, function (err, rev) {
95 + path[1] = rev ? ref : null
96 + cb(null, R_serveRepoPage2.call(self, req, repo, path))
97 + })
98 + })
99 + })
100 +}
101 +
102 +function R_serveRepoPage2(req, repo, path) {
103 + var branch = path[1]
104 + var filePath = path.slice(2)
105 + switch (path[0]) {
106 + case undefined:
107 + case '':
108 + return this.serveRepoTree(req, repo, branch, [])
109 + case 'activity':
110 + return this.serveRepoActivity(req, repo, branch)
111 + case 'commits':
112 + return this.serveRepoCommits(req, repo, branch)
113 + case 'commit':
114 + return this.serveRepoCommit(req, repo, path[1])
115 + case 'tag':
116 + return this.serveRepoTag(req, repo, branch)
117 + case 'tree':
118 + return this.serveRepoTree(req, repo, branch, filePath)
119 + case 'blob':
120 + return this.serveRepoBlob(req, repo, branch, filePath)
121 + case 'raw':
122 + return this.serveRepoRaw(req, repo, branch, filePath)
123 + case 'digs':
124 + return this.serveRepoDigs(req, repo)
125 + case 'fork':
126 + return this.serveRepoForkPrompt(req, repo)
127 + case 'forks':
128 + return this.serveRepoForks(req, repo)
129 + case 'issues':
130 + switch (path[1]) {
131 + case 'new':
132 + if (filePath.length == 0)
133 + return this.issues.serveRepoNewIssue(req, repo)
134 + break
135 + default:
136 + return this.issues.serveRepoIssues(req, repo, false)
137 + }
138 + case 'pulls':
139 + return this.issues.serveRepoIssues(req, repo, true)
140 + case 'compare':
141 + return this.pulls.serveRepoCompare(req, repo)
142 + case 'comparing':
143 + return this.pulls.serveRepoComparing(req, repo)
144 + default:
145 + return this.web.serve404(req)
146 + }
147 +}
148 +
149 +R.serveRepoNotFound = function (req, id, err) {
150 + return this.web.serveTemplate(req, req._t('error.RepoNotFound'), 404)
151 + (pull.values([
152 + '<h2>' + req._t('error.RepoNotFound') + '</h2>',
153 + '<p>' + req._t('error.RepoNameNotFound') + '</p>',
154 + '<pre>' + u.escape(err.stack) + '</pre>'
155 + ]))
156 +}
157 +
158 +R.renderRepoPage = function (req, repo, page, branch, titleTemplate, body) {
159 + var self = this
160 + var gitUrl = 'ssb://' + repo.id
161 + var gitLink = '<input class="clone-url" readonly="readonly" ' +
162 + 'value="' + gitUrl + '" size="45" ' +
163 + 'onclick="this.select()"/>'
164 + var digsPath = [repo.id, 'digs']
165 +
166 + var done = multicb({ pluck: 1, spread: true })
167 + self.web.getRepoName(repo.feed, repo.id, done())
168 + self.web.about.getName(repo.feed, done())
169 + self.web.getVotes(repo.id, done())
170 +
171 + if (repo.upstream) {
172 + self.web.getRepoName(repo.upstream.feed, repo.upstream.id, done())
173 + self.web.about.getName(repo.upstream.feed, done())
174 + }
175 +
176 + return u.readNext(function (cb) {
177 + done(function (err, repoName, authorName, votes,
178 + upstreamName, upstreamAuthorName) {
179 + if (err) return cb(null, self.web.serveError(req, err))
180 + var upvoted = votes.upvoters[self.web.myId] > 0
181 + var upstreamLink = !repo.upstream ? '' :
182 + u.link([repo.upstream])
183 + var title = titleTemplate ? titleTemplate
184 + .replace(/%\{repo\}/g, repoName)
185 + .replace(/%\{author\}/g, authorName)
186 + : authorName + '/' + repoName
187 + var isPublic = self.web.isPublic
188 + cb(null, self.web.serveTemplate(req, title)(cat([
189 + pull.once(
190 + '<div class="repo-title">' +
191 + '<form class="right-bar" action="" method="post">' +
192 + '<button class="btn" name="action" value="vote" ' +
193 + (isPublic ? 'disabled="disabled"' : ' type="submit"') + '>' +
194 + '<i>✌</i> ' + req._t(!isPublic && upvoted ? 'Undig' : 'Dig') +
195 + '</button>' +
196 + (isPublic ? '' : '<input type="hidden" name="value" value="' +
197 + (upvoted ? '0' : '1') + '">' +
198 + '<input type="hidden" name="id" value="' +
199 + u.escape(repo.id) + '">') + ' ' +
200 + '<strong>' + u.link(digsPath, votes.upvotes) + '</strong> ' +
201 + (isPublic ? '' : '<button class="btn" type="submit" ' +
202 + ' name="action" value="fork-prompt">' +
203 + '<i>⑂</i> ' + req._t('Fork') +
204 + '</button>') + ' ' +
205 + u.link([repo.id, 'forks'], '+', false, ' title="' +
206 + req._t('Forks') + '"') +
207 + '</form>' +
208 + forms.name(req, !isPublic, repo.id, repoName, 'repo-name',
209 + null, req._t('repo.Rename'),
210 + '<h2 class="bgslash">' + u.link([repo.feed], authorName) + ' / ' +
211 + u.link([repo.id], repoName) + '</h2>') +
212 + '</div>' +
213 + (repo.upstream ? '<small class="bgslash">' + req._t('ForkedFrom', {
214 + repo: u.link([repo.upstream.feed], upstreamAuthorName) + '/' +
215 + u.link([repo.upstream.id], upstreamName)
216 + }) + '</small>' : '') +
217 + u.nav([
218 + [[repo.id], req._t('Code'), 'code'],
219 + [[repo.id, 'activity'], req._t('Activity'), 'activity'],
220 + [[repo.id, 'commits', branch||''], req._t('Commits'), 'commits'],
221 + [[repo.id, 'issues'], req._t('Issues'), 'issues'],
222 + [[repo.id, 'pulls'], req._t('PullRequests'), 'pulls']
223 + ], page, gitLink)),
224 + body
225 + ])))
226 + })
227 + })
228 +}
229 +
230 +R.serveEmptyRepo = function (req, repo) {
231 + if (repo.feed != this.web.myId)
232 + return this.renderRepoPage(req, repo, 'code', null, null, pull.once(
233 + '<section>' +
234 + '<h3>' + req._t('EmptyRepo') + '</h3>' +
235 + '</section>'))
236 +
237 + var gitUrl = 'ssb://' + repo.id
238 + return this.renderRepoPage(req, repo, 'code', null, null, pull.once(
239 + '<section>' +
240 + '<h3>' + req._t('initRepo.GettingStarted') + '</h3>' +
241 + '<h4>' + req._t('initRepo.CreateNew') + '</h4><pre>' +
242 + 'touch ' + req._t('initRepo.README') + '.md\n' +
243 + 'git init\n' +
244 + 'git add ' + req._t('initRepo.README') + '.md\n' +
245 + 'git commit -m "' + req._t('initRepo.InitialCommit') + '"\n' +
246 + 'git remote add origin ' + gitUrl + '\n' +
247 + 'git push -u origin master</pre>\n' +
248 + '<h4>' + req._t('initRepo.PushExisting') + '</h4>\n' +
249 + '<pre>git remote add origin ' + gitUrl + '\n' +
250 + 'git push -u origin master</pre>' +
251 + '</section>'))
252 +}
253 +
254 +R.serveRepoTree = function (req, repo, rev, path) {
255 + if (!rev) return this.serveEmptyRepo(req, repo)
256 + var type = repo.isCommitHash(rev) ? 'Tree' : 'Branch'
257 + var title = (path.length ? path.join('/') + ' · ' : '') +
258 + '%{author}/%{repo}' +
259 + (repo.head == 'refs/heads/' + rev ? '' : '@' + rev)
260 + return this.renderRepoPage(req, repo, 'code', rev, title, cat([
261 + pull.once('<section><form action="" method="get">' +
262 + '<h3>' + req._t(type) + ': ' + rev + ' '),
263 + revMenu(req, repo, rev),
264 + pull.once('</h3></form>'),
265 + type == 'Branch' && renderRepoLatest(req, repo, rev),
266 + pull.once('</section><section>'),
267 + renderRepoTree(req, repo, rev, path),
268 + pull.once('</section>'),
269 + this.renderRepoReadme(req, repo, rev, path)
270 + ]))
271 +}
272 +
273 +/* Repo activity */
274 +
275 +R.serveRepoActivity = function (req, repo, branch) {
276 + var self = this
277 + var title = req._t('Activity') + ' · %{author}/%{repo}'
278 + return self.renderRepoPage(req, repo, 'activity', branch, title, cat([
279 + pull.once('<h3>' + req._t('Activity') + '</h3>'),
280 + pull(
281 + self.web.ssb.links({
282 + dest: repo.id,
283 + source: repo.feed,
284 + rel: 'repo',
285 + values: true,
286 + reverse: true
287 + }),
288 + pull.map(renderRepoUpdate.bind(self, req, repo))
289 + ),
290 + u.readOnce(function (cb) {
291 + var done = multicb({ pluck: 1, spread: true })
292 + self.web.about.getName(repo.feed, done())
293 + self.web.getMsg(repo.id, done())
294 + done(function (err, authorName, msg) {
295 + if (err) return cb(err)
296 + self.web.renderFeedItem(req, {
297 + key: repo.id,
298 + value: msg,
299 + authorName: authorName
300 + }, cb)
301 + })
302 + })
303 + ]))
304 +}
305 +
306 +function renderRepoUpdate(req, repo, msg, full) {
307 + var c = msg.value.content
308 +
309 + if (c.type != 'git-update') {
310 + return ''
311 + // return renderFeedItem(msg, cb)
312 + // TODO: render post, issue, pull-request
313 + }
314 +
315 + var branches = []
316 + var tags = []
317 + if (c.refs) for (var name in c.refs) {
318 + var m = name.match(/^refs\/(heads|tags)\/(.*)$/) || [,, name]
319 + ;(m[1] == 'tags' ? tags : branches)
320 + .push({name: m[2], value: c.refs[name]})
321 + }
322 + var numObjects = c.objects ? Object.keys(c.objects).length : 0
323 +
324 + var dateStr = new Date(msg.value.timestamp).toLocaleString(req._locale)
325 + return '<section class="collapse">' +
326 + u.link([msg.key], dateStr) + '<br>' +
327 + branches.map(function (update) {
328 + if (!update.value) {
329 + return '<s>' + u.escape(update.name) + '</s><br/>'
330 + } else {
331 + var commitLink = u.link([repo.id, 'commit', update.value])
332 + var branchLink = u.link([repo.id, 'tree', update.name])
333 + return branchLink + ' &rarr; <tt>' + commitLink + '</tt><br/>'
334 + }
335 + }).join('') +
336 + tags.map(function (update) {
337 + return update.value
338 + ? u.link([repo.id, 'tag', update.value], update.name)
339 + : '<s>' + u.escape(update.name) + '</s>'
340 + }).join(', ') +
341 + '</section>'
342 +}
343 +
344 +/* Repo commits */
345 +
346 +R.serveRepoCommits = function (req, repo, branch) {
347 + var query = req._u.query
348 + var title = req._t('Commits') + ' · %{author}/%{repo}'
349 + return this.renderRepoPage(req, repo, 'commits', branch, title, cat([
350 + pull.once('<h3>' + req._t('Commits') + '</h3>'),
351 + pull(
352 + repo.readLog(query.start || branch),
353 + pull.take(20),
354 + paramap(repo.getCommitParsed.bind(repo), 8),
355 + paginate(
356 + !query.start ? '' : function (first, cb) {
357 + cb(null, '&hellip;')
358 + },
359 + pull.map(renderCommit.bind(this, req, repo)),
360 + function (commit, cb) {
361 + cb(null, commit.parents && commit.parents[0] ?
362 + '<a href="?start=' + commit.id + '">' +
363 + req._t('Older') + '</a>' : '')
364 + }
365 + )
366 + )
367 + ]))
368 +}
369 +
370 +function renderCommit(req, repo, commit) {
371 + var commitPath = [repo.id, 'commit', commit.id]
372 + var treePath = [repo.id, 'tree', commit.id]
373 + return '<section class="collapse">' +
374 + '<strong>' + u.link(commitPath, commit.title) + '</strong><br>' +
375 + '<tt>' + commit.id + '</tt> ' +
376 + u.link(treePath, req._t('Tree')) + '<br>' +
377 + u.escape(commit.author.name) + ' &middot; ' +
378 + commit.author.date.toLocaleString(req._locale) +
379 + (commit.separateAuthor ? '<br>' + req._t('CommittedOn', {
380 + name: u.escape(commit.committer.name),
381 + date: commit.committer.date.toLocaleString(req._locale)
382 + }) : '') +
383 + '</section>'
384 +}
385 +
386 +/* Branch menu */
387 +
388 +function formatRevOptions(currentName) {
389 + return function (name) {
390 + var htmlName = u.escape(name)
391 + return '<option value="' + htmlName + '"' +
392 + (name == currentName ? ' selected="selected"' : '') +
393 + '>' + htmlName + '</option>'
394 + }
395 +}
396 +
397 +function formatRevType(req, type) {
398 + return (
399 + type == 'heads' ? req._t('Branches') :
400 + type == 'tags' ? req._t('Tags') :
401 + type)
402 +}
403 +
404 +function revMenu(req, repo, currentName) {
405 + return u.readOnce(function (cb) {
406 + repo.getRefNames(function (err, refs) {
407 + if (err) return cb(err)
408 + cb(null, '<select name="rev" onchange="this.form.submit()">' +
409 + Object.keys(refs).map(function (group) {
410 + return '<optgroup label="' + formatRevType(req, group) + '">' +
411 + refs[group].map(formatRevOptions(currentName)).join('') +
412 + '</optgroup>'
413 + }).join('') +
414 + '</select><noscript> ' +
415 + '<input type="submit" value="' + req._t('Go') + '"/></noscript>')
416 + })
417 + })
418 +}
419 +
420 +function branchMenu(repo, name, currentName) {
421 + return cat([
422 + pull.once('<select name="' + name + '">'),
423 + pull(
424 + repo.refs(),
425 + pull.map(function (ref) {
426 + var m = ref.name.match(/^refs\/([^\/]*)\/(.*)$/) || [,, ref.name]
427 + return m[1] == 'heads' && m[2]
428 + }),
429 + pull.filter(Boolean),
430 + u.pullSort(),
431 + pull.map(formatRevOptions(currentName))
432 + ),
433 + pull.once('</select>')
434 + ])
435 +}
436 +
437 +/* Repo tree */
438 +
439 +function renderRepoLatest(req, repo, rev) {
440 + return u.readOnce(function (cb) {
441 + repo.getCommitParsed(rev, function (err, commit) {
442 + if (err) return cb(err)
443 + var commitPath = [repo.id, 'commit', commit.id]
444 + cb(null,
445 + req._t('Latest') + ': ' +
446 + '<strong>' + u.link(commitPath, commit.title) + '</strong><br/>' +
447 + '<tt>' + commit.id + '</tt><br/> ' +
448 + req._t('CommittedOn', {
449 + name: u.escape(commit.committer.name),
450 + date: commit.committer.date.toLocaleString(req._locale)
451 + }) +
452 + (commit.separateAuthor ? '<br/>' + req._t('AuthoredOn', {
453 + name: u.escape(commit.author.name),
454 + date: commit.author.date.toLocaleString(req._locale)
455 + }) : ''))
456 + })
457 + })
458 +}
459 +
460 +// breadcrumbs
461 +function linkPath(basePath, path) {
462 + path = path.slice()
463 + var last = path.pop()
464 + return path.map(function (dir, i) {
465 + return u.link(basePath.concat(path.slice(0, i+1)), dir)
466 + }).concat(last).join(' / ')
467 +}
468 +
469 +function renderRepoTree(req, repo, rev, path) {
470 + var pathLinks = path.length === 0 ? '' :
471 + ': ' + linkPath([repo.id, 'tree'], [rev].concat(path))
472 + return cat([
473 + pull.once('<h3>' + req._t('Files') + pathLinks + '</h3>'),
474 + pull(
475 + repo.readDir(rev, path),
476 + pull.map(function (file) {
477 + var type = (file.mode === 040000) ? 'tree' :
478 + (file.mode === 0160000) ? 'commit' : 'blob'
479 + if (type == 'commit')
480 + return [
481 + '<span title="' + req._t('gitCommitLink') + '">🖈</span>',
482 + '<span title="' + u.escape(file.id) + '">' +
483 + u.escape(file.name) + '</span>']
484 + var filePath = [repo.id, type, rev].concat(path, file.name)
485 + return ['<i>' + (type == 'tree' ? '📁' : '📄') + '</i>',
486 + u.link(filePath, file.name)]
487 + }),
488 + table('class="files"')
489 + )
490 + ])
491 +}
492 +
493 +/* Repo readme */
494 +
495 +R.renderRepoReadme = function (req, repo, branch, path) {
496 + var self = this
497 + return u.readNext(function (cb) {
498 + pull(
499 + repo.readDir(branch, path),
500 + pull.filter(function (file) {
501 + return /readme(\.|$)/i.test(file.name)
502 + }),
503 + pull.take(1),
504 + pull.collect(function (err, files) {
505 + if (err) return cb(null, pull.empty())
506 + var file = files[0]
507 + if (!file)
508 + return cb(null, pull.once(path.length ? '' :
509 + '<p>' + req._t('NoReadme') + '</p>'))
510 + repo.getObjectFromAny(file.id, function (err, obj) {
511 + if (err) return cb(err)
512 + cb(null, cat([
513 + pull.once('<section><h4><a name="readme">' +
514 + u.escape(file.name) + '</a></h4><hr/>'),
515 + self.web.renderObjectData(obj, file.name, repo, branch, path),
516 + pull.once('</section>')
517 + ]))
518 + })
519 + })
520 + )
521 + })
522 +}
523 +
524 +/* Repo commit */
525 +
526 +R.serveRepoCommit = function (req, repo, rev) {
527 + var self = this
528 + return u.readNext(function (cb) {
529 + repo.getCommitParsed(rev, function (err, commit) {
530 + if (err) return cb(err)
531 + var commitPath = [repo.id, 'commit', commit.id]
532 + var treePath = [repo.id, 'tree', commit.id]
533 + var title = u.escape(commit.title) + ' · ' +
534 + '%{author}/%{repo}@' + commit.id.substr(0, 8)
535 + cb(null, self.renderRepoPage(req, repo, null, rev, title, cat([
536 + pull.once(
537 + '<h3>' + u.link(commitPath,
538 + req._t('CommitRev', {rev: rev})) + '</h3>' +
539 + '<section class="collapse">' +
540 + '<div class="right-bar">' +
541 + u.link(treePath, req._t('BrowseFiles')) +
542 + '</div>' +
543 + '<h4>' + u.linkify(u.escape(commit.title)) + '</h4>' +
544 + (commit.body ? u.linkify(u.pre(commit.body)) : '') +
545 + (commit.separateAuthor ? req._t('AuthoredOn', {
546 + name: u.escape(commit.author.name),
547 + date: commit.author.date.toLocaleString(req._locale)
548 + }) + '<br/>' : '') +
549 + req._t('CommittedOn', {
550 + name: u.escape(commit.committer.name),
551 + date: commit.committer.date.toLocaleString(req._locale)
552 + }) + '<br/>' +
553 + commit.parents.map(function (id) {
554 + return req._t('Parent') + ': ' +
555 + u.link([repo.id, 'commit', id], id)
556 + }).join('<br>') +
557 + '</section>' +
558 + '<section><h3>' + req._t('FilesChanged') + '</h3>'),
559 + // TODO: show diff from all parents (merge commits)
560 + self.renderDiffStat(req, [repo, repo], [commit.parents[0], commit.id]),
561 + pull.once('</section>')
562 + ])))
563 + })
564 + })
565 +}
566 +
567 +/* Repo tag */
568 +
569 +R.serveRepoTag = function (req, repo, rev) {
570 + var self = this
571 + return u.readNext(function (cb) {
572 + repo.getTagParsed(rev, function (err, tag) {
573 + if (err) return cb(err)
574 + var title = req._t('TagName', {
575 + tag: u.escape(tag.tag)
576 + }) + ' · %{author}/%{repo}'
577 + var body = (tag.title + '\n\n' +
578 + tag.body.replace(/-----BEGIN PGP SIGNATURE-----\n[^.]*?\n-----END PGP SIGNATURE-----\s*$/, '')).trim()
579 + cb(null, self.renderRepoPage(req, repo, 'tags', tag.object, title,
580 + pull.once(
581 + '<section class="collapse">' +
582 + '<h3>' + u.link([repo.id, 'tag', rev], tag.tag) + '</h3>' +
583 + req._t('TaggedOn', {
584 + name: u.escape(tag.tagger.name),
585 + date: tag.tagger.date.toLocaleString(req._locale)
586 + }) + '<br/>' +
587 + u.link([repo.id, tag.type, tag.object]) +
588 + u.linkify(u.pre(body)) +
589 + '</section>')))
590 + })
591 + })
592 +}
593 +
594 +
595 +/* Diff stat */
596 +
597 +R.renderDiffStat = function (req, repos, treeIds) {
598 + if (treeIds.length == 0) treeIds = [null]
599 + var id = treeIds[0]
600 + var lastI = treeIds.length - 1
601 + var oldTree = treeIds[0]
602 + var changedFiles = []
603 + return cat([
604 + pull(
605 + GitRepo.diffTrees(repos, treeIds, true),
606 + pull.map(function (item) {
607 + var filename = u.escape(item.filename = item.path.join('/'))
608 + var oldId = item.id && item.id[0]
609 + var newId = item.id && item.id[lastI]
610 + var oldMode = item.mode && item.mode[0]
611 + var newMode = item.mode && item.mode[lastI]
612 + var action =
613 + !oldId && newId ? req._t('action.added') :
614 + oldId && !newId ? req._t('action.deleted') :
615 + oldMode != newMode ? req._t('action.changedMode', {
616 + old: oldMode.toString(8),
617 + new: newMode.toString(8)
618 + }) : req._t('changed')
619 + if (item.id)
620 + changedFiles.push(item)
621 + var blobsPath = item.id[1]
622 + ? [repos[1].id, 'blob', treeIds[1]]
623 + : [repos[0].id, 'blob', treeIds[0]]
624 + var rawsPath = item.id[1]
625 + ? [repos[1].id, 'raw', treeIds[1]]
626 + : [repos[0].id, 'raw', treeIds[0]]
627 + item.blobPath = blobsPath.concat(item.path)
628 + item.rawPath = rawsPath.concat(item.path)
629 + var fileHref = item.id ?
630 + '#' + encodeURIComponent(item.path.join('/')) :
631 + u.encodeLink(item.blobPath)
632 + return ['<a href="' + fileHref + '">' + filename + '</a>', action]
633 + }),
634 + table()
635 + ),
636 + pull(
637 + pull.values(changedFiles),
638 + paramap(function (item, cb) {
639 + var extension = u.getExtension(item.filename)
640 + if (extension in u.imgMimes) {
641 + var filename = u.escape(item.filename)
642 + return cb(null,
643 + '<pre><table class="code">' +
644 + '<tr><th id="' + u.escape(item.filename) + '">' +
645 + filename + '</th></tr>' +
646 + '<tr><td><img src="' + u.encodeLink(item.rawPath) + '"' +
647 + ' alt="' + filename + '"/></td></tr>' +
648 + '</table></pre>')
649 + }
650 + var done = multicb({ pluck: 1, spread: true })
651 + getRepoObjectString(repos[0], item.id[0], done())
652 + getRepoObjectString(repos[1], item.id[lastI], done())
653 + done(function (err, strOld, strNew) {
654 + if (err) return cb(err)
655 + cb(null, htmlLineDiff(req, item.filename, item.filename,
656 + strOld, strNew,
657 + u.encodeLink(item.blobPath)))
658 + })
659 + }, 4)
660 + )
661 + ])
662 +}
663 +
664 +function htmlLineDiff(req, filename, anchor, oldStr, newStr, blobHref) {
665 + var diff = JsDiff.structuredPatch('', '', oldStr, newStr)
666 + var groups = diff.hunks.map(function (hunk) {
667 + var oldLine = hunk.oldStart
668 + var newLine = hunk.newStart
669 + var header = '<tr class="diff-hunk-header"><td colspan=2></td><td>' +
670 + '@@ -' + oldLine + ',' + hunk.oldLines + ' ' +
671 + '+' + newLine + ',' + hunk.newLines + ' @@' +
672 + '</td></tr>'
673 + return [header].concat(hunk.lines.map(function (line) {
674 + var s = line[0]
675 + if (s == '\\') return
676 + var html = u.highlight(line, u.getExtension(filename))
677 + var trClass = s == '+' ? 'diff-new' : s == '-' ? 'diff-old' : ''
678 + var lineNums = [s == '+' ? '' : oldLine++, s == '-' ? '' : newLine++]
679 + var id = [filename].concat(lineNums).join('-')
680 + return '<tr id="' + u.escape(id) + '" class="' + trClass + '">' +
681 + lineNums.map(function (num) {
682 + return '<td class="code-linenum">' +
683 + (num ? '<a href="#' + encodeURIComponent(id) + '">' +
684 + num + '</a>' : '') + '</td>'
685 + }).join('') +
686 + '<td class="code-text">' + html + '</td></tr>'
687 + }))
688 + })
689 + return '<pre><table class="code">' +
690 + '<tr><th colspan=3 id="' + u.escape(anchor) + '">' + filename +
691 + '<span class="right-bar">' +
692 + '<a href="' + blobHref + '">' + req._t('View') + '</a> ' +
693 + '</span></th></tr>' +
694 + [].concat.apply([], groups).join('') +
695 + '</table></pre>'
696 +}
697 +
698 +/* An unknown message linking to a repo */
699 +
700 +R.serveRepoSomething = function (req, repo, id, msg, path) {
701 + return this.renderRepoPage(req, repo, null, null, null,
702 + pull.once('<section><h3>' + u.link([id]) + '</h3>' +
703 + u.json(msg) + '</section>'))
704 +}
705 +
706 +/* Repo update */
707 +
708 +function objsArr(objs) {
709 + return Array.isArray(objs) ? objs :
710 + Object.keys(objs).map(function (sha1) {
711 + var obj = Object.create(objs[sha1])
712 + obj.sha1 = sha1
713 + return obj
714 + })
715 +}
716 +
717 +R.serveRepoUpdate = function (req, repo, id, msg, path) {
718 + var self = this
719 + var raw = req._u.query.raw != null
720 + var title = req._t('Update') + ' · %{author}/%{repo}'
721 +
722 + if (raw)
723 + return self.renderRepoPage(req, repo, 'activity', null, title, pull.once(
724 + '<a href="?" class="raw-link header-align">' +
725 + req._t('Info') + '</a>' +
726 + '<h3>' + req._t('Update') + '</h3>' +
727 + '<section class="collapse">' +
728 + u.json({key: id, value: msg}) + '</section>'))
729 +
730 + // convert packs to old single-object style
731 + if (msg.content.indexes) {
732 + for (var i = 0; i < msg.content.indexes.length; i++) {
733 + msg.content.packs[i] = {
734 + pack: {link: msg.content.packs[i].link},
735 + idx: msg.content.indexes[i]
736 + }
737 + }
738 + }
739 +
740 + var commits = cat([
741 + msg.content.objects && pull(
742 + pull.values(msg.content.objects),
743 + pull.filter(function (obj) { return obj.type == 'commit' }),
744 + paramap(function (obj, cb) {
745 + self.web.getBlob(req, obj.link || obj.key, function (err, readObject) {
746 + if (err) return cb(err)
747 + GitRepo.getCommitParsed({read: readObject}, cb)
748 + })
749 + }, 8)
750 + ),
751 + msg.content.packs && pull(
752 + pull.values(msg.content.packs),
753 + paramap(function (pack, cb) {
754 + var done = multicb({ pluck: 1, spread: true })
755 + self.web.getBlob(req, pack.pack.link, done())
756 + self.web.getBlob(req, pack.idx.link, done())
757 + done(function (err, readPack, readIdx) {
758 + if (err) return cb(self.web.renderError(err))
759 + cb(null, gitPack.decodeWithIndex(repo, readPack, readIdx))
760 + })
761 + }, 4),
762 + pull.flatten(),
763 + pull.asyncMap(function (obj, cb) {
764 + if (obj.type == 'commit')
765 + GitRepo.getCommitParsed(obj, cb)
766 + else
767 + pull(obj.read, pull.drain(null, cb))
768 + }),
769 + pull.filter()
770 + )
771 + ])
772 +
773 + return self.renderRepoPage(req, repo, 'activity', null, title, cat([
774 + pull.once('<a href="?raw" class="raw-link header-align">' +
775 + req._t('Data') + '</a>' +
776 + '<h3>' + req._t('Update') + '</h3>' +
777 + renderRepoUpdate(req, repo, {key: id, value: msg}, true)),
778 + (msg.content.objects || msg.content.packs) &&
779 + pull.once('<h3>' + req._t('Commits') + '</h3>'),
780 + pull(commits, pull.map(function (commit) {
781 + return renderCommit(req, repo, commit)
782 + }))
783 + ]))
784 +}
785 +
786 +/* Blob */
787 +
788 +R.serveRepoBlob = function (req, repo, rev, path) {
789 + var self = this
790 + return u.readNext(function (cb) {
791 + repo.getFile(rev, path, function (err, object) {
792 + if (err) return cb(null, self.web.serveBlobNotFound(req, repo.id, err))
793 + var type = repo.isCommitHash(rev) ? 'Tree' : 'Branch'
794 + var pathLinks = path.length === 0 ? '' :
795 + ': ' + linkPath([repo.id, 'tree'], [rev].concat(path))
796 + var rawFilePath = [repo.id, 'raw', rev].concat(path)
797 + var dirPath = path.slice(0, path.length-1)
798 + var filename = path[path.length-1]
799 + var extension = u.getExtension(filename)
800 + var title = (path.length ? path.join('/') + ' · ' : '') +
801 + '%{author}/%{repo}' +
802 + (repo.head == 'refs/heads/' + rev ? '' : '@' + rev)
803 + cb(null, self.renderRepoPage(req, repo, 'code', rev, title, cat([
804 + pull.once('<section><form action="" method="get">' +
805 + '<h3>' + req._t(type) + ': ' + rev + ' '),
806 + revMenu(req, repo, rev),
807 + pull.once('</h3></form>'),
808 + type == 'Branch' && renderRepoLatest(req, repo, rev),
809 + pull.once('</section><section class="collapse">' +
810 + '<h3>' + req._t('Files') + pathLinks + '</h3>' +
811 + '<div>' + object.length + ' bytes' +
812 + '<span class="raw-link">' +
813 + u.link(rawFilePath, req._t('Raw')) + '</span>' +
814 + '</div></section>' +
815 + '<section>'),
816 + extension in u.imgMimes
817 + ? pull.once('<img src="' + u.encodeLink(rawFilePath) +
818 + '" alt="' + u.escape(filename) + '" />')
819 + : self.web.renderObjectData(object, filename, repo, rev, dirPath),
820 + pull.once('</section>')
821 + ])))
822 + })
823 + })
824 +}
825 +
826 +/* Raw blob */
827 +
828 +R.serveRepoRaw = function (req, repo, branch, path) {
829 + var self = this
830 + return u.readNext(function (cb) {
831 + repo.getFile(branch, path, function (err, object) {
832 + if (err) return cb(null,
833 + this.web.serveBuffer(404, req._t('error.BlobNotFound')))
834 + var extension = u.getExtension(path[path.length-1])
835 + var contentType = u.imgMimes[extension]
836 + cb(null, pull(object.read, self.web.serveRaw(object.length, contentType)))
837 + })
838 + })
839 +}
840 +
841 +/* Digs */
842 +
843 +R.serveRepoDigs = function (req, repo) {
844 + var self = this
845 + return u.readNext(function (cb) {
846 + var title = req._t('Digs') + ' · %{author}/%{repo}'
847 + self.web.getVotes(repo.id, function (err, votes) {
848 + cb(null, self.renderRepoPage(req, repo, null, null, title, cat([
849 + pull.once('<section><h3>' + req._t('Digs') + '</h3>' +
850 + '<div>' + req._t('Total') + ': ' + votes.upvotes + '</div>'),
851 + pull(
852 + pull.values(Object.keys(votes.upvoters)),
853 + paramap(function (feedId, cb) {
854 + self.web.about.getName(feedId, function (err, name) {
855 + if (err) return cb(err)
856 + cb(null, u.link([feedId], name))
857 + })
858 + }, 8),
859 + ul()
860 + ),
861 + pull.once('</section>')
862 + ])))
863 + })
864 + })
865 +}
866 +
867 +/* Forks */
868 +
869 +R.getForks = function (repo, includeSelf) {
870 + var self = this
871 + return pull(
872 + cat([
873 + includeSelf && u.readOnce(function (cb) {
874 + self.web.getMsg(repo.id, function (err, value) {
875 + cb(err, value && {key: repo.id, value: value})
876 + })
877 + }),
878 + self.web.ssb.links({
879 + dest: repo.id,
880 + values: true,
881 + rel: 'upstream'
882 + })
883 + ]),
884 + pull.filter(function (msg) {
885 + return msg.value.content && msg.value.content.type == 'git-repo'
886 + }),
887 + paramap(function (msg, cb) {
888 + self.web.getRepoFullName(msg.value.author, msg.key,
889 + function (err, repoName, authorName) {
890 + if (err) return cb(err)
891 + cb(null, {
892 + key: msg.key,
893 + value: msg.value,
894 + repoName: repoName,
895 + authorName: authorName
896 + })
897 + })
898 + }, 8)
899 + )
900 +}
901 +
902 +R.serveRepoForks = function (req, repo) {
903 + var hasForks
904 + var title = req._t('Forks') + ' · %{author}/%{repo}'
905 + return this.renderRepoPage(req, repo, null, null, title, cat([
906 + pull.once('<h3>' + req._t('Forks') + '</h3>'),
907 + pull(
908 + this.getForks(repo),
909 + pull.map(function (msg) {
910 + hasForks = true
911 + return '<section class="collapse">' +
912 + u.link([msg.value.author], msg.authorName) + ' / ' +
913 + u.link([msg.key], msg.repoName) +
914 + '<span class="right-bar">' +
915 + u.timestamp(msg.value.timestamp, req) +
916 + '</span></section>'
917 + })
918 + ),
919 + u.readOnce(function (cb) {
920 + cb(null, hasForks ? '' : req._t('NoForks'))
921 + })
922 + ]))
923 +}
924 +
925 +R.serveRepoForkPrompt = function (req, repo) {
926 + var title = req._t('Fork') + ' · %{author}/%{repo}'
927 + return this.renderRepoPage(req, repo, null, null, title, pull.once(
928 + '<form action="" method="post" onreset="history.back()">' +
929 + '<h3>' + req._t('ForkRepoPrompt') + '</h3>' +
930 + '<p>' + hiddenInputs({ id: repo.id }) +
931 + '<button class="btn open" type="submit" name="action" value="fork">' +
932 + req._t('Fork') +
933 + '</button>' +
934 + ' <button class="btn" type="reset">' +
935 + req._t('Cancel') + '</button>' +
936 + '</p></form>'
937 + ))
938 +}
lib/repos/issues.jsView
@@ -1,0 +1,233 @@
1 +var pull = require('pull-stream')
2 +var cat = require('pull-cat')
3 +var u = require('../util')
4 +var markdown = require('../markdown')
5 +var forms = require('../forms')
6 +
7 +module.exports = function (repoRoutes, web) {
8 + return new RepoIssueRoutes(repoRoutes, web)
9 +}
10 +
11 +function RepoIssueRoutes(repoRoutes, web) {
12 + this.repo = repoRoutes
13 + this.web = web
14 +}
15 +
16 +var I = RepoIssueRoutes.prototype
17 +
18 +function getMention(msg, id) {
19 + if (msg.key == id) return msg
20 + var mentions = msg.value.content.mentions
21 + if (mentions) for (var i = 0; i < mentions.length; i++) {
22 + var mention = mentions[i]
23 + if (mention.link == id)
24 + return mention
25 + }
26 + return null
27 +}
28 +
29 +/* Issues */
30 +
31 +I.serveRepoIssues = function (req, repo, isPRs) {
32 + var self = this
33 + var count = 0
34 + var state = req._u.query.state || 'open'
35 + var newPath = isPRs ? [repo.id, 'compare'] : [repo.id, 'issues', 'new']
36 + var title = req._t('Issues') + ' · %{author}/%{repo}'
37 + var page = isPRs ? 'pulls' : 'issues'
38 + return self.repo.renderRepoPage(req, repo, page, null, title, cat([
39 + pull.once(
40 + (self.web.isPublic ? '' :
41 + '<form class="right-bar" method="get"' +
42 + ' action="' + u.encodeLink(newPath) + '">' +
43 + '<button class="btn">&plus; ' +
44 + req._t(isPRs ? 'pullRequest.New' : 'issue.New') +
45 + '</button>' +
46 + '</form>') +
47 + '<h3>' + req._t(isPRs ? 'PullRequests' : 'Issues') + '</h3>' +
48 + u.nav([
49 + ['?', req._t('issues.Open'), 'open'],
50 + ['?state=closed', req._t('issues.Closed'), 'closed'],
51 + ['?state=all', req._t('issues.All'), 'all']
52 + ], state)),
53 + pull(
54 + (isPRs ? self.web.pullReqs : self.web.issues).list({
55 + repo: repo.id,
56 + project: repo.id,
57 + open: {open: true, closed: false}[state]
58 + }),
59 + pull.map(function (issue) {
60 + count++
61 + var state = (issue.open ? 'open' : 'closed')
62 + var stateStr = req._t(issue.open ?
63 + 'issue.state.Open' : 'issue.state.Closed')
64 + return '<section class="collapse">' +
65 + '<i class="issue-state issue-state-' + state + '"' +
66 + ' title="' + stateStr + '">◼</i> ' +
67 + '<a href="' + u.encodeLink(issue.id) + '">' +
68 + u.escape(issue.title) +
69 + '<span class="right-bar">' +
70 + new Date(issue.created_at).toLocaleString(req._locale) +
71 + '</span>' +
72 + '</a>' +
73 + '</section>'
74 + })
75 + ),
76 + u.readOnce(function (cb) {
77 + cb(null, count > 0 ? '' :
78 + '<p>' + req._t(isPRs ? 'NoPullRequests' : 'NoIssues') + '</p>')
79 + })
80 + ]))
81 +}
82 +
83 +/* New Issue */
84 +
85 +I.serveRepoNewIssue = function (req, repo, issueId, path) {
86 + var title = req._t('issue.New') + ' · %{author}/%{repo}'
87 + return this.repo.renderRepoPage(req, repo, 'issues', null, title, pull.once(
88 + '<h3>' + req._t('issue.New') + '</h3>' +
89 + '<section><form action="" method="post">' +
90 + '<input type="hidden" name="action" value="new-issue">' +
91 + '<p><input class="wide-input" name="title" placeholder="' +
92 + req._t('issue.Title') + '" size="77" /></p>' +
93 + forms.post(req, repo, req._t('Description'), 8) +
94 + '<button type="submit" class="btn">' + req._t('Create') + '</button>' +
95 + '</form></section>'))
96 +}
97 +
98 +/* Issue */
99 +
100 +I.serveRepoIssue = function (req, repo, issue, path, postId) {
101 + var self = this
102 + var isAuthor = (self.web.myId == issue.author)
103 + || (self.web.myId == repo.feed)
104 + var newestMsg = {key: issue.id, value: {timestamp: issue.created_at}}
105 + var title = u.escape(issue.title) + ' · %{author}/%{repo}'
106 + return self.repo.renderRepoPage(req, repo, 'issues', null, title, cat([
107 + pull.once(
108 + forms.name(req, !self.web.isPublic, issue.id, issue.title,
109 + 'issue-title', null, req._t('issue.Rename'),
110 + '<h3>' + u.link([issue.id], issue.title) + '</h3>') +
111 + '<code>' + issue.id + '</code>' +
112 + '<section class="collapse">' +
113 + (issue.open
114 + ? '<strong class="issue-status open">' +
115 + req._t('issue.state.Open') + '</strong>'
116 + : '<strong class="issue-status closed">' +
117 + req._t('issue.state.Closed') + '</strong>')),
118 + u.readOnce(function (cb) {
119 + self.web.about.getName(issue.author, function (err, authorName) {
120 + if (err) return cb(err)
121 + var authorLink = u.link([issue.author], authorName)
122 + cb(null, req._t('issue.Opened',
123 + {name: authorLink, datetime: u.timestamp(issue.created_at, req)}))
124 + })
125 + }),
126 + pull.once('<hr/>' + markdown(issue.text, repo) + '</section>'),
127 + // render posts and edits
128 + pull(
129 + self.web.ssb.links({
130 + dest: issue.id,
131 + values: true
132 + }),
133 + pull.unique('key'),
134 + self.web.addAuthorName(),
135 + u.sortMsgs(),
136 + pull.through(function (msg) {
137 + if (msg.value.timestamp > newestMsg.value.timestamp)
138 + newestMsg = msg
139 + }),
140 + pull.map(self.renderIssueActivityMsg.bind(self, req, repo, issue,
141 + req._t('issue.'), postId))
142 + ),
143 + self.web.isPublic ? pull.empty() : u.readOnce(function (cb) {
144 + cb(null, forms.issueComment(req, issue, repo,
145 + newestMsg.key, isAuthor, req._t('issue.')))
146 + })
147 + ]))
148 +}
149 +
150 +I.renderIssueActivityMsg = function (req, repo, issue, type, postId, msg) {
151 + var authorLink = u.link([msg.value.author], msg.authorName)
152 + var msgHref = u.encodeLink(msg.key) + '#' + encodeURIComponent(msg.key)
153 + var msgTimeLink = '<a href="' + msgHref + '"' +
154 + ' name="' + u.escape(msg.key) + '">' +
155 + new Date(msg.value.timestamp).toLocaleString(req._locale) + '</a>'
156 + var c = msg.value.content
157 + switch (c.type) {
158 + case 'post':
159 + if (c.root == issue.id) {
160 + var changed = this.web.issues.isStatusChanged(msg, issue)
161 + return '<section class="collapse">' +
162 + (msg.key == postId ? '<div class="highlight">' : '') +
163 + '<tt class="right-bar item-id">' + msg.key + '</tt> ' +
164 + (changed == null ? authorLink : req._t(
165 + changed ? 'issue.Reopened' : 'issue.Closed',
166 + {name: authorLink, type: type})) +
167 + ' &middot; ' + msgTimeLink +
168 + (msg.key == postId ? '</div>' : '') +
169 + markdown(c.text, repo) +
170 + '</section>'
171 + } else {
172 + var text = c.text || (c.type + ' ' + msg.key)
173 + return '<section class="collapse mention-preview">' +
174 + req._t('issue.MentionedIn', {
175 + name: authorLink,
176 + type: type,
177 + post: '<a href="/' + msg.key + '#' + msg.key + '">' +
178 + String(text).substr(0, 140) + '</a>'
179 + }) + '</section>'
180 + }
181 + case 'issue':
182 + case 'pull-request':
183 + return '<section class="collapse mention-preview">' +
184 + req._t('issue.MentionedIn', {
185 + name: authorLink,
186 + type: type,
187 + post: u.link([msg.key], String(c.title || msg.key).substr(0, 140))
188 + }) + '</section>'
189 + case 'issue-edit':
190 + return '<section class="collapse">' +
191 + (msg.key == postId ? '<div class="highlight">' : '') +
192 + (c.title == null ? '' : req._t('issue.Renamed', {
193 + author: authorLink,
194 + type: type,
195 + name: '<q>' + u.escape(c.title) + '</q>'
196 + })) + ' &middot; ' + msgTimeLink +
197 + (msg.key == postId ? '</div>' : '') +
198 + '</section>'
199 + case 'git-update':
200 + var mention = this.web.issues.getMention(msg, issue)
201 + if (mention) {
202 + var commitLink = u.link([repo.id, 'commit', mention.object],
203 + mention.label || mention.object)
204 + return '<section class="collapse">' +
205 + req._t(mention.open ? 'issue.Reopened' : 'issue.Closed', {
206 + name: authorLink,
207 + type: type
208 + }) + ' &middot; ' + msgTimeLink + '<br/>' +
209 + commitLink +
210 + '</section>'
211 + } else if ((mention = getMention(msg, issue.id))) {
212 + var commitLink = u.link(mention.object ?
213 + [repo.id, 'commit', mention.object] : [msg.key],
214 + mention.label || mention.object || msg.key)
215 + return '<section class="collapse">' +
216 + req._t('issue.Mentioned', {
217 + name: authorLink,
218 + type: type
219 + }) + ' &middot; ' + msgTimeLink + '<br/>' +
220 + commitLink +
221 + '</section>'
222 + } else {
223 + // fallthrough
224 + }
225 +
226 + default:
227 + return '<section class="collapse">' +
228 + authorLink +
229 + ' &middot; ' + msgTimeLink +
230 + u.json(c) +
231 + '</section>'
232 + }
233 +}
lib/repos/pulls.jsView
@@ -1,0 +1,408 @@
1 +var pull = require('pull-stream')
2 +var paramap = require('pull-paramap')
3 +var cat = require('pull-cat')
4 +var many = require('pull-many')
5 +var multicb = require('multicb')
6 +var GitRepo = require('pull-git-repo')
7 +var u = require('../../lib/util')
8 +var markdown = require('../../lib/markdown')
9 +var forms = require('../../lib/forms')
10 +
11 +module.exports = function (repoRoutes, web) {
12 + return new RepoPullReqRoutes(repoRoutes, web)
13 +}
14 +
15 +function RepoPullReqRoutes(repoRoutes, web) {
16 + this.repo = repoRoutes
17 + this.web = web
18 +}
19 +
20 +var P = RepoPullReqRoutes.prototype
21 +
22 +/* Pull Request */
23 +
24 +P.serveRepoPullReq = function (req, repo, pr, path, postId) {
25 + var self = this
26 + var headRepo, authorLink
27 + var page = path[0] || 'activity'
28 + var title = u.escape(pr.title) + ' · %{author}/%{repo}'
29 + return self.repo.renderRepoPage(req, repo, 'pulls', null, title, cat([
30 + pull.once('<div class="pull-request">' +
31 + forms.name(req, !self.web.isPublic, pr.id, pr.title,
32 + 'issue-title', null, req._t('pullRequest.Rename'),
33 + '<h3>' + u.link([pr.id], pr.title) + '</h3>') +
34 + '<code>' + pr.id + '</code>'),
35 + u.readOnce(function (cb) {
36 + var done = multicb({ pluck: 1, spread: true })
37 + var gotHeadRepo = done()
38 + self.web.about.getName(pr.author, done())
39 + var sameRepo = (pr.headRepo == pr.baseRepo)
40 + self.web.getRepo(pr.headRepo, function (err, headRepo) {
41 + if (err) return cb(err)
42 + self.web.getRepoName(headRepo.feed, headRepo.id, done())
43 + self.web.about.getName(headRepo.feed, done())
44 + gotHeadRepo(null, GitRepo(headRepo))
45 + })
46 +
47 + done(function (err, _headRepo, issueAuthorName,
48 + headRepoName, headRepoAuthorName) {
49 + if (err) return cb(err)
50 + headRepo = _headRepo
51 + authorLink = u.link([pr.author], issueAuthorName)
52 + var repoLink = u.link([pr.headRepo], headRepoName)
53 + var headRepoAuthorLink = u.link([headRepo.feed], headRepoAuthorName)
54 + var headRepoLink = u.link([headRepo.id], headRepoName)
55 + var headBranchLink = u.link([headRepo.id, 'tree', pr.headBranch])
56 + var baseBranchLink = u.link([repo.id, 'tree', pr.baseBranch])
57 + cb(null, '<section class="collapse">' +
58 + '<strong class="issue-status ' +
59 + (pr.open ? 'open' : 'closed') + '">' +
60 + req._t(pr.open ? 'issue.state.Open' : 'issue.state.Closed') +
61 + '</strong> ' +
62 + req._t('pullRequest.WantToMerge', {
63 + name: authorLink,
64 + base: '<code>' + baseBranchLink + '</code>',
65 + head: (sameRepo ?
66 + '<code>' + headBranchLink + '</code>' :
67 + '<code class="bgslash">' +
68 + headRepoAuthorLink + ' / ' +
69 + headRepoLink + ' / ' +
70 + headBranchLink + '</code>')
71 + }) + '</section>')
72 + })
73 + }),
74 + pull.once(
75 + u.nav([
76 + [[pr.id], req._t('Discussion'), 'activity'],
77 + [[pr.id, 'commits'], req._t('Commits'), 'commits'],
78 + [[pr.id, 'files'], req._t('Files'), 'files']
79 + ], page)),
80 + u.readNext(function (cb) {
81 + if (page == 'commits')
82 + self.renderPullReqCommits(req, pr, repo, headRepo, cb)
83 + else if (page == 'files')
84 + self.renderPullReqFiles(req, pr, repo, headRepo, cb)
85 + else cb(null,
86 + self.renderPullReqActivity(req, pr, repo, headRepo, authorLink, postId))
87 + })
88 + ]))
89 +}
90 +
91 +P.renderPullReqCommits = function (req, pr, baseRepo, headRepo, cb) {
92 + var self = this
93 + self.web.pullReqs.getRevs(pr.id, function (err, revs) {
94 + if (err) return cb(null, self.web.renderError(err))
95 + cb(null, cat([
96 + pull.once('<section>'),
97 + self.renderCommitLog(req, baseRepo, revs.base, headRepo, revs.head),
98 + pull.once('</section>')
99 + ]))
100 + })
101 +}
102 +
103 +P.renderPullReqFiles = function (req, pr, baseRepo, headRepo, cb) {
104 + var self = this
105 + self.web.pullReqs.getRevs(pr.id, function (err, revs) {
106 + if (err) return cb(null, self.web.renderError(err))
107 + cb(null, cat([
108 + pull.once('<section>'),
109 + self.repo.renderDiffStat(req,
110 + [baseRepo, headRepo], [revs.base, revs.head]),
111 + pull.once('</section>')
112 + ]))
113 + })
114 +}
115 +
116 +P.renderPullReqActivity = function (req, pr, repo, headRepo, authorLink, postId) {
117 + var self = this
118 + var msgTimeLink = u.link([pr.id],
119 + new Date(pr.created_at).toLocaleString(req._locale))
120 + var newestMsg = {key: pr.id, value: {timestamp: pr.created_at}}
121 + var isAuthor = (self.web.myId == pr.author) || (self.web.myId == repo.feed)
122 + return cat([
123 + u.readOnce(function (cb) {
124 + cb(null,
125 + '<section class="collapse">' +
126 + authorLink + ' &middot; ' + msgTimeLink +
127 + markdown(pr.text, repo) + '</section>')
128 + }),
129 + // render posts, edits, and updates
130 + pull(
131 + many([
132 + self.web.ssb.links({
133 + dest: pr.id,
134 + values: true
135 + }),
136 + u.readNext(function (cb) {
137 + cb(null, pull(
138 + self.web.ssb.links({
139 + dest: headRepo.id,
140 + source: headRepo.feed,
141 + rel: 'repo',
142 + values: true,
143 + reverse: true
144 + }),
145 + pull.take(function (link) {
146 + return link.value.timestamp > pr.created_at
147 + }),
148 + pull.filter(function (link) {
149 + return link.value.content.type == 'git-update'
150 + && ('refs/heads/' + pr.headBranch) in link.value.content.refs
151 + })
152 + ))
153 + })
154 + ]),
155 + self.web.addAuthorName(),
156 + pull.unique('key'),
157 + pull.through(function (msg) {
158 + if (msg.value.timestamp > newestMsg.value.timestamp)
159 + newestMsg = msg
160 + }),
161 + u.sortMsgs(),
162 + pull.map(function (item) {
163 + if (item.value.content.type == 'git-update')
164 + return self.renderBranchUpdate(req, pr, item)
165 + return self.repo.issues.renderIssueActivityMsg(req, repo, pr,
166 + req._t('pull request'), postId, item)
167 + })
168 + ),
169 + !self.web.isPublic && isAuthor && pr.open && pull.once(
170 + '<section class="merge-instructions">' +
171 + '<input type="checkbox" class="toggle" id="merge-instructions"/>' +
172 + '<h4><label for="merge-instructions" class="toggle-link"><a>' +
173 + req._t('mergeInstructions.MergeViaCmdLine') +
174 + '</a></label></h4>' +
175 + '<div class="contents">' +
176 + '<p>' + req._t('mergeInstructions.CheckOut') + '</p>' +
177 + '<pre>' +
178 + 'git fetch ssb://' + u.escape(pr.headRepo) + ' ' +
179 + u.escape(pr.headBranch) + '\n' +
180 + 'git checkout -b ' + u.escape(pr.headBranch) + ' FETCH_HEAD' +
181 + '</pre>' +
182 + '<p>' + req._t('mergeInstructions.MergeAndPush') + '</p>' +
183 + '<pre>' +
184 + 'git checkout ' + u.escape(pr.baseBranch) + '\n' +
185 + 'git merge ' + u.escape(pr.headBranch) + '\n' +
186 + 'git push ssb ' + u.escape(pr.baseBranch) +
187 + '</pre>' +
188 + '</div></section>'),
189 + !self.web.isPublic && u.readOnce(function (cb) {
190 + cb(null, forms.issueComment(req, pr, repo, newestMsg.key,
191 + isAuthor, req._t('pull request')))
192 + })
193 + ])
194 +}
195 +
196 +P.renderBranchUpdate = function (req, pr, msg) {
197 + var authorLink = u.link([msg.value.author], msg.authorName)
198 + var msgLink = u.link([msg.key],
199 + new Date(msg.value.timestamp).toLocaleString(req._locale))
200 + var rev = msg.value.content.refs['refs/heads/' + pr.headBranch]
201 + if (!rev)
202 + return '<section class="collapse">' +
203 + req._t('NameDeletedBranch', {
204 + name: authorLink,
205 + branch: '<code>' + pr.headBranch + '</code>'
206 + }) + ' &middot; ' + msgLink +
207 + '</section>'
208 +
209 + var revLink = u.link([pr.headRepo, 'commit', rev], rev.substr(0, 8))
210 + return '<section class="collapse">' +
211 + req._t('NameUpdatedBranch', {
212 + name: authorLink,
213 + rev: '<code>' + revLink + '</code>'
214 + }) + ' &middot; ' + msgLink +
215 + '</section>'
216 +}
217 +
218 +/* Compare changes */
219 +
220 +P.serveRepoCompare = function (req, repo) {
221 + var self = this
222 + var query = req._u.query
223 + var base
224 + var count = 0
225 + var title = req._t('CompareChanges') + ' · %{author}/%{repo}'
226 +
227 + return self.repo.renderRepoPage(req, repo, 'pulls', null, title, cat([
228 + pull.once('<h3>' + req._t('CompareChanges') + '</h3>' +
229 + '<form action="' + u.encodeLink(repo.id) + '/comparing" method="get">' +
230 + '<section>'),
231 + pull.once(req._t('BaseBranch') + ': '),
232 + u.readNext(function (cb) {
233 + if (query.base) gotBase(null, query.base)
234 + else repo.getSymRef('HEAD', true, gotBase)
235 + function gotBase(err, ref) {
236 + if (err) return cb(err)
237 + cb(null, branchMenu(repo, 'base', base = ref || 'HEAD'))
238 + }
239 + }),
240 + pull.once('<br/>' + req._t('ComparisonRepoBranch') + ':'),
241 + pull(
242 + self.repo.getForks(repo, true),
243 + pull.asyncMap(function (msg, cb) {
244 + self.web.getRepo(msg.key, function (err, repo) {
245 + if (err) return cb(err)
246 + cb(null, {
247 + msg: msg,
248 + repo: repo
249 + })
250 + })
251 + }),
252 + pull.map(renderFork),
253 + pull.flatten()
254 + ),
255 + pull.once('</section>'),
256 + u.readOnce(function (cb) {
257 + cb(null, count == 0 ? req._t('NoBranches') :
258 + '<button type="submit" class="btn">' +
259 + req._t('Compare') + '</button>')
260 + }),
261 + pull.once('</form>')
262 + ]))
263 +
264 + function renderFork(fork) {
265 + return pull(
266 + fork.repo.refs(),
267 + pull.map(function (ref) {
268 + var m = /^refs\/([^\/]*)\/(.*)$/.exec(ref.name) || [,ref.name]
269 + return {
270 + type: m[1],
271 + name: m[2],
272 + value: ref.value
273 + }
274 + }),
275 + pull.filter(function (ref) {
276 + return ref.type == 'heads'
277 + && !(ref.name == base && fork.msg.key == repo.id)
278 + }),
279 + pull.map(function (ref) {
280 + var branchLink = u.link([fork.msg.key, 'tree', ref.name], ref.name)
281 + var authorLink = u.link([fork.msg.value.author], fork.msg.authorName)
282 + var repoLink = u.link([fork.msg.key], fork.msg.repoName)
283 + var value = fork.msg.key + ':' + ref.name
284 + count++
285 + return '<div class="bgslash">' +
286 + '<input type="radio" name="head"' +
287 + ' value="' + u.escape(value) + '"' +
288 + (query.head == value ? ' checked="checked"' : '') + '> ' +
289 + authorLink + ' / ' + repoLink + ' / ' + branchLink + '</div>'
290 + })
291 + )
292 + }
293 +}
294 +
295 +P.serveRepoComparing = function (req, repo) {
296 + var self = this
297 + var query = req._u.query
298 + var baseBranch = query.base
299 + var s = (query.head || '').split(':')
300 +
301 + if (!s || !baseBranch)
302 + return self.web.serveRedirect(req, u.encodeLink([repo.id, 'compare']))
303 +
304 + var headRepoId = s[0]
305 + var headBranch = s[1]
306 + var baseLink = u.link([repo.id, 'tree', baseBranch])
307 + var headBranchLink = u.link([headRepoId, 'tree', headBranch])
308 + var backHref = u.encodeLink([repo.id, 'compare']) + req._u.search
309 + var title = req._t(query.expand ? 'OpenPullRequest': 'ComparingChanges')
310 + var pageTitle = title + ' · %{author}/%{repo}'
311 +
312 + return self.repo.renderRepoPage(req, repo, 'pulls', null, pageTitle, cat([
313 + pull.once('<h3>' + title + '</h3>'),
314 + u.readNext(function (cb) {
315 + self.web.getRepo(headRepoId, function (err, headRepo) {
316 + if (err) return cb(err)
317 + self.web.getRepoFullName(headRepo.feed, headRepo.id,
318 + function (err, repoName, authorName) {
319 + if (err) return cb(err)
320 + cb(null, renderRepoInfo(GitRepo(headRepo), repoName, authorName))
321 + }
322 + )
323 + })
324 + })
325 + ]))
326 +
327 + function renderRepoInfo(headRepo, headRepoName, headRepoAuthorName) {
328 + var authorLink = u.link([headRepo.feed], headRepoAuthorName)
329 + var repoLink = u.link([headRepoId], headRepoName)
330 + return cat([
331 + pull.once('<section>' +
332 + req._t('Base') + ': ' + baseLink + '<br/>' +
333 + req._t('Head') + ': ' +
334 + '<span class="bgslash">' + authorLink + ' / ' + repoLink +
335 + ' / ' + headBranchLink + '</span>' +
336 + '</section>' +
337 + (query.expand ? '<section><form method="post" action="">' +
338 + hiddenInputs({
339 + action: 'new-pull',
340 + branch: baseBranch,
341 + head_repo: headRepoId,
342 + head_branch: headBranch
343 + }) +
344 + '<input class="wide-input" name="title"' +
345 + ' placeholder="' + req._t('Title') + '" size="77"/>' +
346 + forms.post(req, repo, req._t('Description'), 8) +
347 + '<button type="submit" class="btn open">' +
348 + req._t('Create') + '</button>' +
349 + '</form></section>'
350 + : '<section><form method="get" action="">' +
351 + hiddenInputs({
352 + base: baseBranch,
353 + head: query.head
354 + }) +
355 + '<button class="btn open" type="submit" name="expand" value="1">' +
356 + '<i>⎇</i> ' + req._t('CreatePullRequest') + '</button> ' +
357 + '<a href="' + backHref + '">' + req._t('Back') + '</a>' +
358 + '</form></section>') +
359 + '<div id="commits"></div>' +
360 + '<div class="tab-links">' +
361 + '<a href="#" id="files-link">' + req._t('FilesChanged') + '</a> ' +
362 + '<a href="#commits" id="commits-link">' +
363 + req._t('Commits') + '</a>' +
364 + '</div>' +
365 + '<section id="files-tab">'),
366 + self.repo.renderDiffStat(req, [repo, headRepo],
367 + [baseBranch, headBranch]),
368 + pull.once('</section>' +
369 + '<section id="commits-tab">'),
370 + self.renderCommitLog(req, repo, baseBranch, headRepo, headBranch),
371 + pull.once('</section>')
372 + ])
373 + }
374 +}
375 +
376 +P.renderCommitLog = function (req, baseRepo, baseBranch, headRepo, headBranch) {
377 + return cat([
378 + pull.once('<table class="compare-commits">'),
379 + u.readNext(function (cb) {
380 + baseRepo.resolveRef(baseBranch, function (err, baseBranchRev) {
381 + if (err) return cb(err)
382 + var currentDay
383 + return cb(null, pull(
384 + headRepo.readLog(headBranch),
385 + pull.take(function (rev) { return rev != baseBranchRev }),
386 + u.pullReverse(),
387 + paramap(headRepo.getCommitParsed.bind(headRepo), 8),
388 + pull.map(function (commit) {
389 + var commitPath = [headRepo.id, 'commit', commit.id]
390 + var commitIdShort = '<tt>' + commit.id.substr(0, 8) + '</tt>'
391 + var day = Math.floor(commit.author.date / 86400000)
392 + var dateRow = day == currentDay ? '' :
393 + '<tr><th colspan=3 class="date-info">' +
394 + commit.author.date.toLocaleDateString(req._locale) +
395 + '</th><tr>'
396 + currentDay = day
397 + return dateRow + '<tr>' +
398 + '<td>' + u.escape(commit.author.name) + '</td>' +
399 + '<td>' + u.link(commitPath, commit.title) + '</td>' +
400 + '<td>' + u.link(commitPath, commitIdShort, true) + '</td>' +
401 + '</tr>'
402 + })
403 + ))
404 + })
405 + }),
406 + pull.once('</table>')
407 + ])
408 +}
lib/users.jsView
@@ -1,0 +1,99 @@
1 +var pull = require('pull-stream')
2 +var cat = require('pull-cat')
3 +var paramap = require('pull-paramap')
4 +var multicb = require('multicb')
5 +var u = require('./util')
6 +
7 +module.exports = function (web) {
8 + return new UserRoutes(web)
9 +}
10 +
11 +function UserRoutes(web) {
12 + this.web = web
13 +}
14 +
15 +var U = UserRoutes.prototype
16 +
17 +U.serveUserPage = function (req, feedId, dirs) {
18 + switch (dirs[0]) {
19 + case undefined:
20 + case '':
21 + case 'activity':
22 + return this.serveUserActivity(req, feedId)
23 + case 'repos':
24 + return this.serveUserRepos(req, feedId)
25 + }
26 +}
27 +
28 +U.renderUserPage = function (req, feedId, page, titleTemplate, body) {
29 + var self = this
30 + return u.readNext(function (cb) {
31 + self.web.about.getName(feedId, function (err, name) {
32 + if (err) return cb(err)
33 + var title = titleTemplate ? titleTemplate
34 + .replace(/\%{name\}/g, u.escape(name))
35 + : u.escape(name)
36 + cb(null, self.web.serveTemplate(req, title)(cat([
37 + pull.once('<h2>' + u.link([feedId], name) +
38 + '<code class="user-id">' + feedId + '</code></h2>' +
39 + u.nav([
40 + [[feedId], req._t('Activity'), 'activity'],
41 + [[feedId, 'repos'], req._t('Repos'), 'repos']
42 + ], page)),
43 + body
44 + ])))
45 + })
46 + })
47 +}
48 +
49 +U.serveUserActivity = function (req, feedId) {
50 + return this.renderUserPage(req, feedId, 'activity', null,
51 + this.web.renderFeed(req, feedId))
52 +}
53 +
54 +U.serveUserRepos = function (req, feedId) {
55 + var self = this
56 + var title = req._t('UsersRepos', {name: '%{name}'})
57 + return self.renderUserPage(req, feedId, 'repos', title, pull(
58 + cat([
59 + self.web.ssb.messagesByType({
60 + type: 'git-update',
61 + reverse: true
62 + }),
63 + self.web.ssb.messagesByType({
64 + type: 'git-repo',
65 + reverse: true
66 + })
67 + ]),
68 + pull.filter(function (msg) {
69 + return msg.value.author == feedId
70 + }),
71 + pull.unique(function (msg) {
72 + return msg.value.content.repo || msg.key
73 + }),
74 + pull.take(20),
75 + paramap(function (msg, cb) {
76 + var repoId = msg.value.content.repo || msg.key
77 + var done = multicb({ pluck: 1, spread: true })
78 + self.web.getRepoName(feedId, repoId, done())
79 + self.web.getVotes(repoId, done())
80 + done(function (err, repoName, votes) {
81 + if (err) return cb(err)
82 + cb(null, '<section class="collapse">' +
83 + '<span class="right-bar">' +
84 + '<i>✌</i> ' +
85 + u.link([repoId, 'digs'], votes.upvotes, true,
86 + ' title="' + req._t('Digs') + '"') +
87 + '</span>' +
88 + '<strong>' + u.link([repoId], repoName) + '</strong>' +
89 + '<div class="date-info">' +
90 + req._t(msg.value.content.type == 'git-update' ?
91 + 'UpdatedOnDate' : 'CreatedOnDate',
92 + {
93 + date: u.timestamp(msg.value.timestamp, req)
94 + }) + '</div>' +
95 + '</section>')
96 + })
97 + }, 8)
98 + ))
99 +}
lib/util.jsView
@@ -1,0 +1,141 @@
1 +var pull = require('pull-stream')
2 +var Highlight = require('highlight.js')
3 +var u = exports
4 +
5 +u.imgMimes = {
6 + png: 'image/png',
7 + jpeg: 'image/jpeg',
8 + jpg: 'image/jpeg',
9 + gif: 'image/gif',
10 + tif: 'image/tiff',
11 + svg: 'image/svg+xml',
12 + bmp: 'image/bmp'
13 +}
14 +
15 +u.getExtension = function(filename) {
16 + return (/\.([^.]+)$/.exec(filename) || [,filename])[1]
17 +}
18 +
19 +u.readNext = function (fn) {
20 + var next
21 + return function (end, cb) {
22 + if (next) return next(end, cb)
23 + fn(function (err, _next) {
24 + if (err) return cb(err)
25 + next = _next
26 + next(null, cb)
27 + })
28 + }
29 +}
30 +
31 +u.readOnce = function (fn) {
32 + var ended
33 + return function (end, cb) {
34 + fn(function (err, data) {
35 + if (err || ended) return cb(err || ended)
36 + ended = true
37 + cb(null, data)
38 + })
39 + }
40 +}
41 +
42 +u.escape = function (str) {
43 + return String(str)
44 + .replace(/&/g, '&amp;')
45 + .replace(/</g, '&lt;')
46 + .replace(/>/g, '&gt;')
47 + .replace(/"/g, '&quot;')
48 +}
49 +
50 +u.encodeLink = function (url) {
51 + if (!Array.isArray(url)) url = [url]
52 + return '/' + url.map(encodeURIComponent).join('/')
53 +}
54 +
55 +u.link = function (parts, text, raw, props) {
56 + if (text == null) text = parts[parts.length-1]
57 + if (!raw) text = u.escape(text)
58 + return '<a href="' + u.encodeLink(parts) + '"' +
59 + (props ? ' ' + props : '') +
60 + '>' + text + '</a>'
61 +}
62 +
63 +u.timestamp = function (time, req) {
64 + time = Number(time)
65 + var d = new Date(time)
66 + return '<span title="' + time + '">' +
67 + d.toLocaleString(req._locale) + '</span>'
68 +}
69 +
70 +u.nav = function (links, page, after) {
71 + return ['<nav>'].concat(
72 + links.map(function (link) {
73 + var href = typeof link[0] == 'string' ? link[0] : u.encodeLink(link[0])
74 + var props = link[2] == page ? ' class="active"' : ''
75 + return '<a href="' + href + '"' + props + '>' + link[1] + '</a>'
76 + }), after || '', '</nav>').join('')
77 +}
78 +
79 +u.highlight = function(code, lang) {
80 + try {
81 + return lang
82 + ? Highlight.highlight(lang, code).value
83 + : Highlight.highlightAuto(code).value
84 + } catch(e) {
85 + if (/^Unknown language/.test(e.message))
86 + return u.escape(code)
87 + throw e
88 + }
89 +}
90 +
91 +u.pre = function (text) {
92 + return '<pre>' + u.escape(text) + '</pre>'
93 +}
94 +
95 +u.json = function (obj) {
96 + return linkify(u.pre(JSON.stringify(obj, null, 2)))
97 +}
98 +
99 +u.linkify = function (text) {
100 + // regex is from ssb-ref
101 + return text.replace(/(@|%|&|&amp;)[A-Za-z0-9\/+]{43}=\.[\w\d]+/g, function (str) {
102 + return '<a href="/' + encodeURIComponent(str) + '">' + str + '</a>'
103 + })
104 +}
105 +
106 +u.readObjectString = function (obj, cb) {
107 + pull(obj.read, pull.collect(function (err, bufs) {
108 + if (err) return cb(err)
109 + cb(null, Buffer.concat(bufs, obj.length).toString('utf8'))
110 + }))
111 +}
112 +
113 +u.pullReverse = function () {
114 + return function (read) {
115 + return u.readNext(function (cb) {
116 + pull(read, pull.collect(function (err, items) {
117 + cb(err, items && pull.values(items.reverse()))
118 + }))
119 + })
120 + }
121 +}
122 +
123 +function compareMsgs(a, b) {
124 + return (a.value.timestamp - b.value.timestamp) || (a.key - b.key)
125 +}
126 +
127 +u.pullSort = function (comparator) {
128 + return function (read) {
129 + return u.readNext(function (cb) {
130 + pull(read, pull.collect(function (err, items) {
131 + if (err) return cb(err)
132 + items.sort(comparator)
133 + cb(null, pull.values(items))
134 + }))
135 + })
136 + }
137 +}
138 +
139 +u.sortMsgs = function () {
140 + return u.pullSort(compareMsgs)
141 +}
lib/votes.jsView
@@ -1,0 +1,59 @@
1 +var pull = require('pull-stream')
2 +var asyncMemo = require('asyncmemo')
3 +
4 +module.exports = function (sbot) {
5 + return asyncMemo(getVotes, sbot)
6 +}
7 +
8 +function getVotes(sbot, id, cb) {
9 + var upvoters, downvoters
10 + var result = {
11 + upvoters: upvoters = {},
12 + downvoters: downvoters = {},
13 + upvotes: 0,
14 + downvotes: 0
15 + }
16 +
17 + var opts = {
18 + dest: id,
19 + rel: 'vote',
20 + values: true,
21 + keys: false
22 + }
23 + pull(
24 + sbot.links(opts),
25 + pull.drain(processMsg, function (err) {
26 + cb(err, result)
27 + // keep the result updated
28 + opts.live = true
29 + pull(
30 + sbot.links(opts),
31 + pull.drain(processMsg)
32 + )
33 + })
34 + )
35 +
36 + function processMsg(msg) {
37 + if (msg.sync) return cb(null, result)
38 + var vote = ((msg.value.content || 0).vote || 0).value
39 + var author = msg.value.author
40 +
41 + // remove old vote, if any
42 + if (author in upvoters) {
43 + result.upvotes--
44 + delete result.upvoters[author]
45 + } else if (author in downvoters) {
46 + result.downvotes--
47 + delete result.downvoters[author]
48 + }
49 +
50 + // add new vote
51 + if (vote > 0) {
52 + result.upvoters[author] = vote
53 + result.upvotes++
54 + } else if (vote < 0) {
55 + result.downvoters[author] = vote
56 + result.downvotes++
57 + }
58 + }
59 +}
votes.jsView
@@ -1,59 +1,0 @@
1-var pull = require('pull-stream')
2-var asyncMemo = require('asyncmemo')
3-
4-module.exports = function (sbot) {
5- return asyncMemo(getVotes, sbot)
6-}
7-
8-function getVotes(sbot, id, cb) {
9- var upvoters, downvoters
10- var result = {
11- upvoters: upvoters = {},
12- downvoters: downvoters = {},
13- upvotes: 0,
14- downvotes: 0
15- }
16-
17- var opts = {
18- dest: id,
19- rel: 'vote',
20- values: true,
21- keys: false
22- }
23- pull(
24- sbot.links(opts),
25- pull.drain(processMsg, function (err) {
26- cb(err, result)
27- // keep the result updated
28- opts.live = true
29- pull(
30- sbot.links(opts),
31- pull.drain(processMsg)
32- )
33- })
34- )
35-
36- function processMsg(msg) {
37- if (msg.sync) return cb(null, result)
38- var vote = ((msg.value.content || 0).vote || 0).value
39- var author = msg.value.author
40-
41- // remove old vote, if any
42- if (author in upvoters) {
43- result.upvotes--
44- delete result.upvoters[author]
45- } else if (author in downvoters) {
46- result.downvotes--
47- delete result.downvoters[author]
48- }
49-
50- // add new vote
51- if (vote > 0) {
52- result.upvoters[author] = vote
53- result.upvotes++
54- } else if (vote < 0) {
55- result.downvoters[author] = vote
56- result.downvotes++
57- }
58- }
59-}

Built with git-ssb-web