Commit dd4e83623a7471b95f8faa0bd8c16453c6b9f6f5
Refactor
Close %MF5xNDxiqidFUpmTO3ZoEQ0s/5shkodQLho6zWijE/E=.sha256Charles Lehner committed on 4/22/2016, 2:59:35 AM
Parent: b8aa0f8352d62cc90e2ab57a3b56f4925e06ed9b
Files changed
index.js | changed |
about.js | deleted |
i18n.js | deleted |
lib/about.js | added |
lib/i18n.js | added |
lib/markdown.js | added |
lib/paginate.js | added |
lib/repos/index.js | added |
lib/repos/issues.js | added |
lib/repos/pulls.js | added |
lib/users.js | added |
lib/util.js | added |
lib/votes.js | added |
votes.js | deleted |
index.js | ||
---|---|---|
@@ -8,107 +8,23 @@ | ||
8 | 8 … | var pull = require('pull-stream') |
9 | 9 … | var ssbGit = require('ssb-git-repo') |
10 | 10 … | var toPull = require('stream-to-pull-stream') |
11 | 11 … | 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') | |
17 | 16 … | var asyncMemo = require('asyncmemo') |
18 | 17 … | var multicb = require('multicb') |
19 | 18 … | var schemas = require('ssb-msg-schemas') |
20 | 19 … | var Issues = require('ssb-issues') |
21 | 20 … | var PullRequests = require('ssb-pull-requests') |
22 | 21 … | var paramap = require('pull-paramap') |
23 | -var gitPack = require('pull-git-pack') | |
24 | 22 … | var Mentions = require('ssb-mentions') |
25 | -var Highlight = require('highlight.js') | |
26 | -var JsDiff = require('diff') | |
27 | 23 … | var many = require('pull-many') |
28 | 24 … | |
29 | 25 … | var hlCssPath = path.resolve(require.resolve('highlight.js'), '../../styles') |
30 | 26 … | |
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 | - | |
111 | 27 … | function ParamError(msg) { |
112 | 28 … | var err = Error.call(this, msg) |
113 | 29 … | err.name = ParamError.name |
114 | 30 … | return err |
@@ -122,256 +38,8 @@ | ||
122 | 38 … | if (isNaN(str)) return {host: str, port: def.port} |
123 | 39 … | return {host: def.host, port: str} |
124 | 40 … | } |
125 | 41 … | |
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(/(@|%|&|&)[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, '&') | |
168 | - .replace(/</g, '<') | |
169 | - .replace(/>/g, '>') | |
170 | - .replace(/"/g, '"') | |
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 | - | |
374 | 42 … | function tryDecodeURIComponent(str) { |
375 | 43 … | if (!str || (str[0] == '%' && ref.isBlobId(str))) |
376 | 44 … | return str |
377 | 45 … | try { |
@@ -380,24 +48,11 @@ | ||
380 | 48 … | return str |
381 | 49 … | } |
382 | 50 … | } |
383 | 51 … | |
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 | - | |
397 | 52 … | 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' | |
400 | 55 … | } |
401 | 56 … | |
402 | 57 … | var contentTypes = { |
403 | 58 … | css: 'text/css' |
@@ -418,41 +73,15 @@ | ||
418 | 73 … | }) |
419 | 74 … | ) |
420 | 75 … | } |
421 | 76 … | |
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 | - | |
438 | 77 … | var msgTypes = { |
439 | 78 … | 'git-repo': true, |
440 | 79 … | 'git-update': true, |
441 | 80 … | 'issue': true, |
442 | 81 … | 'pull-request': true |
443 | 82 … | } |
444 | 83 … | |
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 | - | |
455 | 84 … | var _httpServer |
456 | 85 … | |
457 | 86 … | module.exports = { |
458 | 87 … | name: 'git-ssb-web', |
@@ -477,9 +106,9 @@ | ||
477 | 106 … | this.reconnect = reconnect |
478 | 107 … | |
479 | 108 … | this.ssbAppname = config.appname || 'ssb' |
480 | 109 … | this.isPublic = config.public |
481 | - this.getVotes = ssbVotes(ssb) | |
110 … | + this.getVotes = require('./lib/votes')(ssb) | |
482 | 111 … | this.getMsg = asyncMemo(ssb.get) |
483 | 112 … | this.issues = Issues.init(ssb) |
484 | 113 … | this.pullReqs = PullRequests.init(ssb) |
485 | 114 … | this.getRepo = asyncMemo(function (id, cb) { |
@@ -491,11 +120,15 @@ | ||
491 | 120 … | |
492 | 121 … | this.about = function (id, cb) { cb(null, {name: id}) } |
493 | 122 … | ssb.whoami(function (err, feed) { |
494 | 123 … | this.myId = feed.id |
495 | - this.about = ssbAbout(ssb, this.myId) | |
124 … | + this.about = require('./lib/about')(ssb, this.myId) | |
496 | 125 … | }.bind(this)) |
497 | 126 … | |
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 … | + | |
498 | 131 … | var webConfig = config['git-ssb-web'] || {} |
499 | 132 … | var addr = parseAddr(config.listenAddr, { |
500 | 133 … | host: webConfig.host || 'localhost', |
501 | 134 … | port: webConfig.port || 7718 |
@@ -563,9 +196,9 @@ | ||
563 | 196 … | req._u = url.parse(req.url, true) |
564 | 197 … | var locale = req._u.query.locale || |
565 | 198 … | (/locale=([^;]*)/.exec(req.headers.cookie) || [])[1] |
566 | 199 … | var reqLocales = req.headers['accept-language'] |
567 | - i18n.pickCatalog(reqLocales, locale, function (err, t) { | |
200 … | + this.i18n.pickCatalog(reqLocales, locale, function (err, t) { | |
568 | 201 … | if (err) return pull(this.serveError(req, err, 500), serve(req, res)) |
569 | 202 … | req._t = t |
570 | 203 … | req._locale = t.locale |
571 | 204 … | pull(this.handleRequest(req), serve(req, res)) |
@@ -589,9 +222,9 @@ | ||
589 | 222 … | return this.serveBlob(req, dir) |
590 | 223 … | else if (ref.isMsgId(dir)) |
591 | 224 … | return this.serveMessage(req, dir, dirs.slice(1)) |
592 | 225 … | else if (ref.isFeedId(dir)) |
593 | - return this.serveUserPage(req, dir, dirs.slice(1)) | |
226 … | + return this.users.serveUserPage(req, dir, dirs.slice(1)) | |
594 | 227 … | else if (dir == 'static') |
595 | 228 … | return this.serveFile(req, dirs) |
596 | 229 … | else if (dir == 'highlight') |
597 | 230 … | return this.serveFile(req, [hlCssPath].concat(dirs.slice(1)), true) |
@@ -602,27 +235,27 @@ | ||
602 | 235 … | G.handlePOST = function (req, dir) { |
603 | 236 … | var self = this |
604 | 237 … | if (self.isPublic) |
605 | 238 … | return self.serveBuffer(405, req._t('error.POSTNotAllowed')) |
606 | - return readNext(function (cb) { | |
239 … | + return u.readNext(function (cb) { | |
607 | 240 … | readReqForm(req, function (err, data) { |
608 | 241 … | if (err) return cb(null, self.serveError(req, err, 400)) |
609 | 242 … | if (!data) return cb(null, self.serveError(req, |
610 | 243 … | new ParamError(req._t('error.MissingData')), 400)) |
611 | 244 … | |
612 | 245 … | switch (data.action) { |
613 | 246 … | case 'fork-prompt': |
614 | 247 … | return cb(null, self.serveRedirect(req, |
615 | - encodeLink([data.id, 'fork']))) | |
248 … | + u.encodeLink([data.id, 'fork']))) | |
616 | 249 … | |
617 | 250 … | case 'fork': |
618 | 251 … | if (!data.id) |
619 | 252 … | return cb(null, self.serveError(req, |
620 | 253 … | new ParamError(req._t('error.MissingId')), 400)) |
621 | 254 … | return ssbGit.createRepo(self.ssb, {upstream: data.id}, |
622 | 255 … | function (err, repo) { |
623 | 256 … | 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))) | |
625 | 258 … | }) |
626 | 259 … | |
627 | 260 … | case 'vote': |
628 | 261 … | var voteValue = +data.value || 0 |
@@ -686,9 +319,9 @@ | ||
686 | 319 … | if (mentions.length) |
687 | 320 … | msg.mentions = mentions |
688 | 321 … | return self.ssb.publish(msg, function (err, msg) { |
689 | 322 … | 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))) | |
691 | 324 … | }) |
692 | 325 … | |
693 | 326 … | case 'new-pull': |
694 | 327 … | var msg = PullRequests.schemas.new(dir, data.branch, |
@@ -697,9 +330,9 @@ | ||
697 | 330 … | if (mentions.length) |
698 | 331 … | msg.mentions = mentions |
699 | 332 … | return self.ssb.publish(msg, function (err, msg) { |
700 | 333 … | 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))) | |
702 | 335 … | }) |
703 | 336 … | |
704 | 337 … | case 'markdown': |
705 | 338 … | return cb(null, self.serveMarkdown(data.text, {id: data.repo})) |
@@ -716,9 +349,9 @@ | ||
716 | 349 … | // prevent escaping base dir |
717 | 350 … | if (!outside && filename.indexOf('../') === 0) |
718 | 351 … | return this.serveBuffer(403, req._t("error.403Forbidden")) |
719 | 352 … | |
720 | - return readNext(function (cb) { | |
353 … | + return u.readNext(function (cb) { | |
721 | 354 … | fs.stat(filename, function (err, stats) { |
722 | 355 … | cb(null, err ? |
723 | 356 … | err.code == 'ENOENT' ? this.serve404(req) |
724 | 357 … | : this.serveBuffer(500, err.message) |
@@ -756,9 +389,9 @@ | ||
756 | 389 … | G.serveRedirect = function (req, path) { |
757 | 390 … | return this.serveBuffer(302, |
758 | 391 … | '<!doctype><html><head>' + |
759 | 392 … | '<title>' + req._t('Redirect') + '</title></head><body>' + |
760 | - '<p><a href="' + escapeHTML(path) + '">' + | |
393 … | + '<p><a href="' + u.escape(path) + '">' + | |
761 | 394 … | req._t('Continue') + '</a></p>' + |
762 | 395 … | '</body></html>', 'text/html; charset=utf-8', {Location: path}) |
763 | 396 … | } |
764 | 397 … | |
@@ -766,24 +399,25 @@ | ||
766 | 399 … | return this.serveBuffer(200, markdown(text, repo), |
767 | 400 … | 'text/html; charset=utf-8') |
768 | 401 … | } |
769 | 402 … | |
770 | -function renderError(err, tag) { | |
403 … | +G.renderError = function (err, tag) { | |
771 | 404 … | tag = tag || 'h3' |
772 | 405 … | return '<' + tag + '>' + err.name + '</' + tag + '>' + |
773 | - '<pre>' + escapeHTML(err.stack) + '</pre>' | |
406 … | + '<pre>' + u.escape(err.stack) + '</pre>' | |
774 | 407 … | } |
775 | 408 … | |
776 | 409 … | function renderTry(read) { |
410 … | + var self = this | |
777 | 411 … | var ended |
778 | 412 … | return function (end, cb) { |
779 | 413 … | if (ended) return cb(ended) |
780 | 414 … | read(end, function (err, data) { |
781 | 415 … | if (err === true) |
782 | 416 … | cb(true) |
783 | 417 … | else if (err) { |
784 | 418 … | ended = true |
785 | - cb(null, renderError(err, 'h3')) | |
419 … | + cb(null, self.renderError(err)) | |
786 | 420 … | } else |
787 | 421 … | cb(null, data) |
788 | 422 … | }) |
789 | 423 … | } |
@@ -791,9 +425,9 @@ | ||
791 | 425 … | |
792 | 426 … | G.serveTemplate = function (req, title, code, read) { |
793 | 427 … | if (read === undefined) |
794 | 428 … | 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) || '' | |
796 | 430 … | var app = 'git ssb' |
797 | 431 … | var appName = this.ssbAppname |
798 | 432 … | if (req._t) app = req._t(app) |
799 | 433 … | return cat([ |
@@ -824,17 +458,17 @@ | ||
824 | 458 … | G.serveError = function (req, err, status) { |
825 | 459 … | if (err.message == 'stream is closed') |
826 | 460 … | this.reconnect && this.reconnect() |
827 | 461 … | return pull( |
828 | - pull.once(renderError(err, 'h2')), | |
462 … | + pull.once(this.renderError(err, 'h2')), | |
829 | 463 … | this.serveTemplate(req, err.name, status || 500) |
830 | 464 … | ) |
831 | 465 … | } |
832 | 466 … | |
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) { | |
837 | 471 … | buf = buf.toString('utf8') |
838 | 472 … | if (err) return cb(err) |
839 | 473 … | cb(null, (ext == 'md' || ext == 'markdown') |
840 | 474 … | ? markdown(buf, {repo: repo, rev: rev, path: path}) |
@@ -844,9 +478,9 @@ | ||
844 | 478 … | } |
845 | 479 … | |
846 | 480 … | function renderCodeTable(buf, ext) { |
847 | 481 … | 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) { | |
849 | 483 … | i++ |
850 | 484 … | return '<tr id="L' + i + '">' + |
851 | 485 … | '<td class="code-linenum">' + '<a href="#L' + i + '">' + i + '</td>' + |
852 | 486 … | '<td class="code-text">' + line + '</td></tr>' |
@@ -873,9 +507,9 @@ | ||
873 | 507 … | }), |
874 | 508 … | typeof filter == 'function' ? filter(opts) : filter, |
875 | 509 … | pull.take(20), |
876 | 510 … | this.addAuthorName(), |
877 | - query.forwards && pullReverse(), | |
511 … | + query.forwards && u.pullReverse(), | |
878 | 512 … | paginate( |
879 | 513 … | function (first, cb) { |
880 | 514 … | if (!query.lt && !query.gt) return cb(null, '') |
881 | 515 … | var gt = feedId ? first.value.sequence : first.value.timestamp + 1 |
@@ -912,12 +546,12 @@ | ||
912 | 546 … | |
913 | 547 … | G.renderFeedItem = function (req, msg, cb) { |
914 | 548 … | var self = this |
915 | 549 … | var c = msg.value.content |
916 | - var msgLink = link([msg.key], | |
550 … | + var msgLink = u.link([msg.key], | |
917 | 551 … | new Date(msg.value.timestamp).toLocaleString(req._locale)) |
918 | 552 … | var author = msg.value.author |
919 | - var authorLink = link([msg.value.author], msg.authorName) | |
553 … | + var authorLink = u.link([msg.value.author], msg.authorName) | |
920 | 554 … | switch (c.type) { |
921 | 555 … | case 'git-repo': |
922 | 556 … | var done = multicb({ pluck: 1, spread: true }) |
923 | 557 … | self.getRepoName(author, msg.key, done()) |
@@ -928,17 +562,17 @@ | ||
928 | 562 … | done(function (err, repoName, upstreamName) { |
929 | 563 … | cb(null, '<section class="collapse">' + msgLink + '<br>' + |
930 | 564 … | req._t('Forked', { |
931 | 565 … | 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) | |
934 | 568 … | }) + '</section>') |
935 | 569 … | }) |
936 | 570 … | }) |
937 | 571 … | } else { |
938 | 572 … | return done(function (err, repoName) { |
939 | 573 … | if (err) return cb(err) |
940 | - var repoLink = link([msg.key], repoName) | |
574 … | + var repoLink = u.link([msg.key], repoName) | |
941 | 575 … | cb(null, '<section class="collapse">' + msgLink + '<br>' + |
942 | 576 … | req._t('CreatedRepo', { |
943 | 577 … | name: authorLink, |
944 | 578 … | repo: repoLink |
@@ -947,24 +581,25 @@ | ||
947 | 581 … | } |
948 | 582 … | case 'git-update': |
949 | 583 … | return self.getRepoName(author, c.repo, function (err, repoName) { |
950 | 584 … | if (err) return cb(err) |
951 | - var repoLink = link([c.repo], repoName) | |
585 … | + var repoLink = u.link([c.repo], repoName) | |
952 | 586 … | cb(null, '<section class="collapse">' + msgLink + '<br>' + |
953 | 587 … | req._t('Pushed', { |
954 | 588 … | name: authorLink, |
955 | 589 … | repo: repoLink |
956 | 590 … | }) + '</section>') |
957 | 591 … | }) |
958 | 592 … | case 'issue': |
959 | 593 … | case 'pull-request': |
960 | - var issueLink = link([msg.key], c.title) | |
594 … | + var issueLink = u.link([msg.key], c.title) | |
961 | 595 … | 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)) | |
963 | 598 … | self.getRepoName(projectMsg.author, c.project, |
964 | 599 … | function (err, repoName) { |
965 | 600 … | if (err) return cb(err) |
966 | - var repoLink = link([c.project], repoName) | |
601 … | + var repoLink = u.link([c.project], repoName) | |
967 | 602 … | cb(null, '<section class="collapse">' + msgLink + '<br>' + |
968 | 603 … | req._t('OpenedIssue', { |
969 | 604 … | name: authorLink, |
970 | 605 … | type: req._t(c.type == 'pull-request' ? |
@@ -977,10 +612,10 @@ | ||
977 | 612 … | case 'about': |
978 | 613 … | return cb(null, '<section class="collapse">' + msgLink + '<br>' + |
979 | 614 … | req._t('Named', { |
980 | 615 … | 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) | |
983 | 618 … | }) + '</section>') |
984 | 619 … | case 'post': |
985 | 620 … | return this.pullReqs.get(c.issue, function (err, pr) { |
986 | 621 … | if (err) return cb(err) |
@@ -988,15 +623,15 @@ | ||
988 | 623 … | 'pull request' : 'issue.' |
989 | 624 … | return cb(null, '<section class="collapse">' + msgLink + '<br>' + |
990 | 625 … | req._t('CommentedOn', { |
991 | 626 … | 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) | |
993 | 628 … | }) + |
994 | 629 … | '<blockquote>' + markdown(c.text) + '</blockquote>' + |
995 | 630 … | '</section>') |
996 | 631 … | }) |
997 | 632 … | default: |
998 | - return cb(null, json(msg)) | |
633 … | + return cb(null, u.json(msg)) | |
999 | 634 … | } |
1000 | 635 … | } |
1001 | 636 … | |
1002 | 637 … | /* Index */ |
@@ -1004,142 +639,63 @@ | ||
1004 | 639 … | G.serveIndex = function (req) { |
1005 | 640 … | return this.serveTemplate(req)(this.renderFeed(req)) |
1006 | 641 … | } |
1007 | 642 … | |
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 */ | |
1018 | 644 … | |
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 | - | |
1094 | 645 … | G.serveMessage = function (req, id, path) { |
1095 | 646 … | var self = this |
1096 | - return readNext(function (cb) { | |
647 … | + return u.readNext(function (cb) { | |
1097 | 648 … | self.ssb.get(id, function (err, msg) { |
1098 | 649 … | if (err) return cb(null, self.serveError(req, err)) |
1099 | 650 … | var c = msg.content || {} |
1100 | 651 … | switch (c.type) { |
1101 | 652 … | case 'git-repo': |
1102 | 653 … | return self.getRepo(id, function (err, repo) { |
1103 | 654 … | 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)) | |
1105 | 656 … | }) |
1106 | 657 … | case 'git-update': |
1107 | 658 … | 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)) | |
1110 | 663 … | }) |
1111 | 664 … | case 'issue': |
1112 | 665 … | return self.getRepo(c.project, function (err, repo) { |
1113 | 666 … | if (err) return cb(null, |
1114 | - self.serveRepoNotFound(req, c.project, err)) | |
667 … | + self.repos.serveRepoNotFound(req, c.project, err)) | |
1115 | 668 … | self.issues.get(id, function (err, issue) { |
1116 | 669 … | 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)) | |
1118 | 672 … | }) |
1119 | 673 … | }) |
1120 | 674 … | case 'pull-request': |
1121 | 675 … | return self.getRepo(c.repo, function (err, repo) { |
1122 | 676 … | if (err) return cb(null, |
1123 | - self.serveRepoNotFound(req, c.project, err)) | |
677 … | + self.repos.serveRepoNotFound(req, c.project, err)) | |
1124 | 678 … | self.pullReqs.get(id, function (err, pr) { |
1125 | 679 … | 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)) | |
1127 | 682 … | }) |
1128 | 683 … | }) |
1129 | 684 … | case 'issue-edit': |
1130 | 685 … | if (ref.isMsgId(c.issue)) { |
1131 | 686 … | return self.pullReqs.get(c.issue, function (err, issue) { |
1132 | 687 … | 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 | |
1135 | 691 … | self.getRepo(issue.project, function (err, repo) { |
1136 | 692 … | if (err) { |
1137 | 693 … | if (!repo) return cb(null, |
1138 | - self.serveRepoNotFound(req, c.repo, err)) | |
694 … | + self.repos.serveRepoNotFound(req, c.repo, err)) | |
1139 | 695 … | return cb(null, self.serveError(req, err)) |
1140 | 696 … | } |
1141 | - cb(null, serve.call(self, req, Repo(repo), issue, path, id)) | |
697 … | + cb(null, serve.call(self, req, GitRepo(repo), issue, path, id)) | |
1142 | 698 … | }) |
1143 | 699 … | }) |
1144 | 700 … | } |
1145 | 701 … | // fallthrough |
@@ -1151,14 +707,15 @@ | ||
1151 | 707 … | self.pullReqs.get(c.issue, done()) |
1152 | 708 … | return done(function (err, repo, issue) { |
1153 | 709 … | if (err) { |
1154 | 710 … | if (!repo) return cb(null, |
1155 | - self.serveRepoNotFound(req, c.repo, err)) | |
711 … | + self.repos.serveRepoNotFound(req, c.repo, err)) | |
1156 | 712 … | return cb(null, self.serveError(req, err)) |
1157 | 713 … | } |
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)) | |
1161 | 718 … | }) |
1162 | 719 … | } else if (ref.isMsgId(c.root)) { |
1163 | 720 … | // comment on issue from patchwork? |
1164 | 721 … | return self.getMsg(c.root, function (err, root) { |
@@ -1172,15 +729,17 @@ | ||
1172 | 729 … | case 'issue': |
1173 | 730 … | return self.issues.get(c.root, function (err, issue) { |
1174 | 731 … | if (err) return cb(null, self.serveError(req, err)) |
1175 | 732 … | return cb(null, |
1176 | - self.serveRepoIssue(req, Repo(repo), issue, path, id)) | |
733 … | + self.repos.issues.serveRepoIssue(req, | |
734 … | + GitRepo(repo), issue, path, id)) | |
1177 | 735 … | }) |
1178 | 736 … | case 'pull-request': |
1179 | 737 … | return self.pullReqs.get(c.root, function (err, pr) { |
1180 | 738 … | if (err) return cb(null, self.serveError(req, err)) |
1181 | 739 … | return cb(null, |
1182 | - self.serveRepoPullReq(req, Repo(repo), pr, path, id)) | |
740 … | + self.repos.pulls.serveRepoPullReq(req, | |
741 … | + GitRepo(repo), pr, path, id)) | |
1183 | 742 … | }) |
1184 | 743 … | } |
1185 | 744 … | }) |
1186 | 745 … | }) |
@@ -1189,10 +748,11 @@ | ||
1189 | 748 … | default: |
1190 | 749 … | if (ref.isMsgId(c.repo)) |
1191 | 750 … | return self.getRepo(c.repo, function (err, repo) { |
1192 | 751 … | 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)) | |
1195 | 755 … | }) |
1196 | 756 … | else |
1197 | 757 … | return cb(null, self.serveGenericMessage(req, id, msg, path)) |
1198 | 758 … | } |
@@ -1201,217 +761,13 @@ | ||
1201 | 761 … | } |
1202 | 762 … | |
1203 | 763 … | G.serveGenericMessage = function (req, id, msg, path) { |
1204 | 764 … | 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) + | |
1207 | 767 … | '</section>')) |
1208 | 768 … | } |
1209 | 769 … | |
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 | - | |
1414 | 770 … | /* Search */ |
1415 | 771 … | |
1416 | 772 … | G.serveSearch = function (req) { |
1417 | 773 … | var self = this |
@@ -1459,584 +815,18 @@ | ||
1459 | 815 … | }) |
1460 | 816 … | ) |
1461 | 817 … | } |
1462 | 818 … | |
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 + ' → <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, '…') | |
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) + ' · ' + | |
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 | - | |
2015 | 819 … | G.serveBlobNotFound = function (req, repoId, err) { |
2016 | 820 … | return this.serveTemplate(req, req._t('error.BlobNotFound'), 404)(pull.once( |
2017 | 821 … | '<h2>' + req._t('error.BlobNotFound') + '</h2>' + |
2018 | 822 … | '<p>' + req._t('error.BlobNotFoundInRepo', { |
2019 | - repo: link([repoId]) | |
823 … | + repo: u.link([repoId]) | |
2020 | 824 … | }) + '</p>' + |
2021 | - '<pre>' + escapeHTML(err.stack) + '</pre>' | |
825 … | + '<pre>' + u.escape(err.stack) + '</pre>' | |
2022 | 826 … | )) |
2023 | 827 … | } |
2024 | 828 … | |
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 | - | |
2039 | 829 … | G.serveRaw = function (length, contentType) { |
2040 | 830 … | var headers = { |
2041 | 831 … | 'Content-Type': contentType || 'text/plain; charset=utf-8', |
2042 | 832 … | 'Cache-Control': 'max-age=31536000' |
@@ -2058,721 +848,12 @@ | ||
2058 | 848 … | } |
2059 | 849 … | |
2060 | 850 … | G.serveBlob = function (req, key) { |
2061 | 851 … | var self = this |
2062 | - return readNext(function (cb) { | |
852 … | + return u.readNext(function (cb) { | |
2063 | 853 … | self.getBlob(req, key, function (err, read) { |
2064 | 854 … | if (err) cb(null, self.serveError(req, err)) |
2065 | 855 … | else if (!read) cb(null, self.serve404(req)) |
2066 | 856 … | else cb(null, self.serveRaw()(read)) |
2067 | 857 … | }) |
2068 | 858 … | }) |
2069 | 859 … | } |
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">+ ' + | |
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 | - ' · ' + 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 | - })) + ' · ' + 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 | - }) + ' · ' + 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 | - }) + ' · ' + msgTimeLink + '<br/>' + | |
2360 | - commitLink + | |
2361 | - '</section>' | |
2362 | - } else { | |
2363 | - // fallthrough | |
2364 | - } | |
2365 | - | |
2366 | - default: | |
2367 | - return '<section class="collapse">' + | |
2368 | - authorLink + | |
2369 | - ' · ' + 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 + ' · ' + 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 | - }) + ' · ' + 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 | - }) + ' · ' + 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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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(/^&/, '&') | |
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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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 + ' → <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, '…') | |
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) + ' · ' + | |
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.js | ||
---|---|---|
@@ -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">+ ' + | |
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 … | + ' · ' + 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 … | + })) + ' · ' + 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 … | + }) + ' · ' + 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 … | + }) + ' · ' + msgTimeLink + '<br/>' + | |
220 … | + commitLink + | |
221 … | + '</section>' | |
222 … | + } else { | |
223 … | + // fallthrough | |
224 … | + } | |
225 … | + | |
226 … | + default: | |
227 … | + return '<section class="collapse">' + | |
228 … | + authorLink + | |
229 … | + ' · ' + msgTimeLink + | |
230 … | + u.json(c) + | |
231 … | + '</section>' | |
232 … | + } | |
233 … | +} |
lib/repos/pulls.js | ||
---|---|---|
@@ -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 + ' · ' + 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 … | + }) + ' · ' + 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 … | + }) + ' · ' + 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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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, '&') | |
45 … | + .replace(/</g, '<') | |
46 … | + .replace(/>/g, '>') | |
47 … | + .replace(/"/g, '"') | |
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(/(@|%|&|&)[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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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