Files: 126b1bb2d5440631bb06f7e3ad4448677d202031 / lib / repos / index.js
33126 bytesRaw
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, mode, cb) { |
27 | if (!id) return cb(null, '') |
28 | if (mode == 0160000) return cb(null, |
29 | 'Subproject commit ' + id) |
30 | repo.getObjectFromAny(id, function (err, obj) { |
31 | if (err) return cb(err) |
32 | u.readObjectString(obj, cb) |
33 | }) |
34 | } |
35 | |
36 | function table(props) { |
37 | return function (read) { |
38 | return cat([ |
39 | pull.once('<table' + (props ? ' ' + props : '') + '>'), |
40 | pull( |
41 | read, |
42 | pull.map(function (row) { |
43 | return row ? '<tr>' + row.map(function (cell, i) { |
44 | var className = (i>0) ? 'w-100' : '' |
45 | return '<td class="' + className +'">' + cell + '</td>' |
46 | }).join('') + '</tr>' : '' |
47 | }) |
48 | ), |
49 | pull.once('</table>') |
50 | ]) |
51 | } |
52 | } |
53 | |
54 | function ul(props) { |
55 | return function (read) { |
56 | return cat([ |
57 | pull.once('<ul' + (props ? ' ' + props : '') + '>'), |
58 | pull(read, pull.map(function (li) { return '<li>' + li + '</li>' })), |
59 | pull.once('</ul>') |
60 | ]) |
61 | } |
62 | } |
63 | |
64 | /* Repo */ |
65 | |
66 | R.serveRepoPage = function (req, repo, path) { |
67 | var self = this |
68 | var defaultBranch = 'master' |
69 | var query = req._u.query |
70 | |
71 | if (query.rev != null) { |
72 | // Allow navigating revs using GET query param. |
73 | // Replace the branch in the path with the rev query value |
74 | path[0] = path[0] || 'tree' |
75 | path[1] = query.rev |
76 | req._u.pathname = u.encodeLink([repo.id].concat(path)) |
77 | delete req._u.query.rev |
78 | delete req._u.search |
79 | return self.web.serveRedirect(req, url.format(req._u)) |
80 | } |
81 | |
82 | // get branch |
83 | return path[1] ? |
84 | R_serveRepoPage2.call(self, req, repo, path) : |
85 | u.readNext(function (cb) { |
86 | // TODO: handle this in pull-git-repo or ssb-git-repo |
87 | repo.getSymRef('HEAD', true, function (err, ref) { |
88 | if (err) return cb(err) |
89 | repo.resolveRef(ref, function (err, rev) { |
90 | path[1] = rev ? ref : null |
91 | cb(null, R_serveRepoPage2.call(self, req, repo, path)) |
92 | }) |
93 | }) |
94 | }) |
95 | } |
96 | |
97 | function R_serveRepoPage2(req, repo, path) { |
98 | var branch = path[1] |
99 | var filePath = path.slice(2) |
100 | switch (path[0]) { |
101 | case undefined: |
102 | case '': |
103 | return this.serveRepoTree(req, repo, branch, []) |
104 | case 'activity': |
105 | return this.serveRepoActivity(req, repo, branch) |
106 | case 'commits': |
107 | return this.serveRepoCommits(req, repo, branch) |
108 | case 'commit': |
109 | return this.serveRepoCommit(req, repo, path[1]) |
110 | case 'tag': |
111 | return this.serveRepoTag(req, repo, branch, filePath) |
112 | case 'tree': |
113 | return this.serveRepoTree(req, repo, branch, filePath) |
114 | case 'blob': |
115 | return this.serveRepoBlob(req, repo, branch, filePath) |
116 | case 'raw': |
117 | return this.serveRepoRaw(req, repo, branch, filePath) |
118 | case 'digs': |
119 | return this.serveRepoDigs(req, repo) |
120 | case 'fork': |
121 | return this.serveRepoForkPrompt(req, repo) |
122 | case 'forks': |
123 | return this.serveRepoForks(req, repo) |
124 | case 'issues': |
125 | switch (path[1]) { |
126 | case 'new': |
127 | if (filePath.length == 0) |
128 | return this.issues.serveRepoNewIssue(req, repo) |
129 | break |
130 | default: |
131 | return this.issues.serveRepoIssues(req, repo, false) |
132 | } |
133 | case 'pulls': |
134 | return this.issues.serveRepoIssues(req, repo, true) |
135 | case 'compare': |
136 | return this.pulls.serveRepoCompare(req, repo) |
137 | case 'comparing': |
138 | return this.pulls.serveRepoComparing(req, repo) |
139 | default: |
140 | return this.web.serve404(req) |
141 | } |
142 | } |
143 | |
144 | R.serveRepoNotFound = function (req, id, err) { |
145 | return this.web.serveTemplate(req, req._t('error.RepoNotFound'), 404) |
146 | (pull.values([ |
147 | '<h2>' + req._t('error.RepoNotFound') + '</h2>', |
148 | '<p>' + req._t('error.RepoNameNotFound') + '</p>', |
149 | '<pre>' + u.escape(err.stack) + '</pre>' |
150 | ])) |
151 | } |
152 | |
153 | R.renderRepoPage = function (req, repo, page, branch, titleTemplate, body) { |
154 | var self = this |
155 | var gitUrl = 'ssb://' + repo.id |
156 | var gitLink = '<input class="clone-url" readonly="readonly" ' + |
157 | 'value="' + gitUrl + '" size="45" ' + |
158 | 'onclick="this.select()"/>' |
159 | var digsPath = [repo.id, 'digs'] |
160 | |
161 | var done = multicb({ pluck: 1, spread: true }) |
162 | self.web.getRepoName(repo.feed, repo.id, done()) |
163 | self.web.about.getName(repo.feed, done()) |
164 | self.web.getVotes(repo.id, done()) |
165 | |
166 | if (repo.upstream) { |
167 | self.web.getRepoName(repo.upstream.feed, repo.upstream.id, done()) |
168 | self.web.about.getName(repo.upstream.feed, done()) |
169 | } |
170 | |
171 | return u.readNext(function (cb) { |
172 | done(function (err, repoName, authorName, votes, |
173 | upstreamName, upstreamAuthorName) { |
174 | if (err) return cb(null, self.web.serveError(req, err)) |
175 | var upvoted = votes.upvoters[self.web.myId] > 0 |
176 | var upstreamLink = !repo.upstream ? '' : |
177 | u.link([repo.upstream]) |
178 | var title = titleTemplate ? titleTemplate |
179 | .replace(/%\{repo\}/g, repoName) |
180 | .replace(/%\{author\}/g, authorName) |
181 | : authorName + '/' + repoName |
182 | var isPublic = self.web.isPublic |
183 | cb(null, self.web.serveTemplate(req, title)(cat([ |
184 | pull.once( |
185 | '<div class="repo-title">' + |
186 | '<form class="right-bar" action="" method="post">' + |
187 | '<button class="btn" name="action" value="vote" ' + |
188 | (isPublic ? 'disabled="disabled"' : ' type="submit"') + '>' + |
189 | '<i>✌</i> ' + req._t(!isPublic && upvoted ? 'Undig' : 'Dig') + |
190 | '</button>' + |
191 | (isPublic ? '' : '<input type="hidden" name="value" value="' + |
192 | (upvoted ? '0' : '1') + '">' + |
193 | '<input type="hidden" name="id" value="' + |
194 | u.escape(repo.id) + '">') + ' ' + |
195 | '<strong>' + u.link(digsPath, votes.upvotes) + '</strong> ' + |
196 | (isPublic ? '' : '<button class="btn" type="submit" ' + |
197 | ' name="action" value="fork-prompt">' + |
198 | '<i>⑂</i> ' + req._t('Fork') + |
199 | '</button>') + ' ' + |
200 | u.link([repo.id, 'forks'], '+', false, ' title="' + |
201 | req._t('Forks') + '"') + |
202 | '</form>' + |
203 | forms.name(req, !isPublic, repo.id, repoName, 'repo-name', |
204 | null, req._t('repo.Rename'), |
205 | '<h2 class="bgslash">' + u.link([repo.feed], authorName) + ' / ' + |
206 | u.link([repo.id], repoName) + '</h2>') + |
207 | '</div>' + |
208 | (repo.upstream ? '<small class="bgslash">' + req._t('ForkedFrom', { |
209 | repo: u.link([repo.upstream.feed], upstreamAuthorName) + '/' + |
210 | u.link([repo.upstream.id], upstreamName) |
211 | }) + '</small>' : '') + |
212 | u.nav([ |
213 | [[repo.id], req._t('Code'), 'code'], |
214 | [[repo.id, 'activity'], req._t('Activity'), 'activity'], |
215 | [[repo.id, 'commits', branch||''], req._t('Commits'), 'commits'], |
216 | [[repo.id, 'issues'], req._t('Issues'), 'issues'], |
217 | [[repo.id, 'pulls'], req._t('PullRequests'), 'pulls'] |
218 | ], page, gitLink)), |
219 | body |
220 | ]))) |
221 | }) |
222 | }) |
223 | } |
224 | |
225 | R.serveEmptyRepo = function (req, repo) { |
226 | if (repo.feed != this.web.myId) |
227 | return this.renderRepoPage(req, repo, 'code', null, null, pull.once( |
228 | '<section>' + |
229 | '<h3>' + req._t('EmptyRepo') + '</h3>' + |
230 | '</section>')) |
231 | |
232 | var gitUrl = 'ssb://' + repo.id |
233 | return this.renderRepoPage(req, repo, 'code', null, null, pull.once( |
234 | '<section>' + |
235 | '<h3>' + req._t('initRepo.GettingStarted') + '</h3>' + |
236 | '<h4>' + req._t('initRepo.CreateNew') + '</h4><pre>' + |
237 | 'touch ' + req._t('initRepo.README') + '.md\n' + |
238 | 'git init\n' + |
239 | 'git add ' + req._t('initRepo.README') + '.md\n' + |
240 | 'git commit -m "' + req._t('initRepo.InitialCommit') + '"\n' + |
241 | 'git remote add origin ' + gitUrl + '\n' + |
242 | 'git push -u origin master</pre>\n' + |
243 | '<h4>' + req._t('initRepo.PushExisting') + '</h4>\n' + |
244 | '<pre>git remote add origin ' + gitUrl + '\n' + |
245 | 'git push -u origin master</pre>' + |
246 | '</section>')) |
247 | } |
248 | |
249 | R.serveRepoTree = function (req, repo, rev, path) { |
250 | var type = repo.isCommitHash(rev) ? 'Tree' : 'Branch' |
251 | var title = (path.length ? path.join('/') + ' · ' : '') + |
252 | '%{author}/%{repo}' + |
253 | (repo.head == 'refs/heads/' + rev ? '' : '@' + rev) |
254 | return this.renderRepoPage(req, repo, 'code', rev, title, cat([ |
255 | pull.once('<section class="branch-info light-grey"><form action="" method="get">' + |
256 | '<h3>' + req._t(type) + ': '), |
257 | this.revMenu(req, repo, rev), |
258 | pull.once('</h3></form>'), |
259 | type == 'Branch' && renderRepoLatest(req, repo, rev), |
260 | pull.once('</section>'), |
261 | rev ? cat([ |
262 | pull.once('<section class="files">'), |
263 | renderRepoTree(req, repo, rev, path), |
264 | pull.once('</section>'), |
265 | this.renderRepoReadme(req, repo, rev, path) |
266 | ]) : this.serveEmptyRepo(req, repo) |
267 | ])) |
268 | } |
269 | |
270 | /* Repo activity */ |
271 | |
272 | R.serveRepoActivity = function (req, repo, branch) { |
273 | var self = this |
274 | var title = req._t('Activity') + ' · %{author}/%{repo}' |
275 | return self.renderRepoPage(req, repo, 'activity', branch, title, cat([ |
276 | pull.once('<h3>' + req._t('Activity') + '</h3>'), |
277 | pull( |
278 | self.web.ssb.links({ |
279 | dest: repo.id, |
280 | rel: 'repo', |
281 | values: true, |
282 | reverse: true |
283 | }), |
284 | pull.asyncMap(renderRepoUpdate.bind(self, req, repo, false)) |
285 | ), |
286 | u.readOnce(function (cb) { |
287 | var done = multicb({ pluck: 1, spread: true }) |
288 | self.web.about.getName(repo.feed, done()) |
289 | self.web.getMsg(repo.id, done()) |
290 | done(function (err, authorName, msg) { |
291 | if (err) return cb(err) |
292 | self.web.renderFeedItem(req, { |
293 | key: repo.id, |
294 | value: msg, |
295 | authorName: authorName |
296 | }, cb) |
297 | }) |
298 | }) |
299 | ])) |
300 | } |
301 | |
302 | function renderRepoUpdate(req, repo, full, msg, cb) { |
303 | var c = msg.value.content |
304 | |
305 | if (c.type != 'git-update') { |
306 | return '' |
307 | // return renderFeedItem(msg, cb) |
308 | // TODO: render post, issue, pull-request |
309 | } |
310 | |
311 | var branches = [] |
312 | var tags = [] |
313 | if (c.refs) for (var name in c.refs) { |
314 | var m = name.match(/^refs\/(heads|tags)\/(.*)$/) || [,, name] |
315 | ;(m[1] == 'tags' ? tags : branches) |
316 | .push({name: m[2], value: c.refs[name]}) |
317 | } |
318 | var numObjects = c.objects ? Object.keys(c.objects).length : 0 |
319 | |
320 | var dateStr = new Date(msg.value.timestamp).toLocaleString(req._locale) |
321 | |
322 | this.web.about.getName(msg.value.author, function (err, name) { |
323 | if (err) return cb(err) |
324 | cb(null, '<section class="collapse">' + |
325 | u.link([msg.key], dateStr) + '<br>' + |
326 | u.link([msg.value.author], name) + '<br>' + |
327 | |
328 | branches.map(function (update) { |
329 | if (!update.value) { |
330 | return '<s>' + u.escape(update.name) + '</s><br/>' |
331 | } else { |
332 | var commitLink = u.link([repo.id, 'commit', update.value]) |
333 | var branchLink = u.link([repo.id, 'tree', update.name]) |
334 | return branchLink + ' → <tt>' + commitLink + '</tt><br/>' |
335 | } |
336 | }).join('') + |
337 | tags.map(function (update) { |
338 | return update.value |
339 | ? u.link([repo.id, 'tag', update.value], update.name) |
340 | : '<s>' + u.escape(update.name) + '</s>' |
341 | }).join(', ') + |
342 | '</section>') |
343 | }) |
344 | } |
345 | |
346 | /* Repo commits */ |
347 | |
348 | R.serveRepoCommits = function (req, repo, branch) { |
349 | var query = req._u.query |
350 | var title = req._t('Commits') + ' · %{author}/%{repo}' |
351 | return this.renderRepoPage(req, repo, 'commits', branch, title, cat([ |
352 | pull.once('<h3>' + req._t('Commits') + '</h3>'), |
353 | pull( |
354 | repo.readLog(query.start || branch), |
355 | pull.take(20), |
356 | paramap(repo.getCommitParsed.bind(repo), 8), |
357 | paginate( |
358 | !query.start ? '' : function (first, cb) { |
359 | cb(null, '…') |
360 | }, |
361 | pull.map(renderCommit.bind(this, req, repo)), |
362 | function (commit, cb) { |
363 | cb(null, commit.parents && commit.parents[0] ? |
364 | '<a href="?start=' + commit.id + '">' + |
365 | req._t('Older') + '</a>' : '') |
366 | } |
367 | ) |
368 | ) |
369 | ])) |
370 | } |
371 | |
372 | function renderCommit(req, repo, commit) { |
373 | var commitPath = [repo.id, 'commit', commit.id] |
374 | var treePath = [repo.id, 'tree', commit.id] |
375 | return '<section class="collapse">' + |
376 | '<strong>' + u.link(commitPath, commit.title) + '</strong><br>' + |
377 | '<tt>' + commit.id + '</tt> ' + |
378 | u.link(treePath, req._t('Tree')) + '<br>' + |
379 | u.escape(commit.author.name) + ' · ' + |
380 | commit.author.date.toLocaleString(req._locale) + |
381 | (commit.separateAuthor ? '<br>' + req._t('CommittedOn', { |
382 | name: u.escape(commit.committer.name), |
383 | date: commit.committer.date.toLocaleString(req._locale) |
384 | }) : '') + |
385 | '</section>' |
386 | } |
387 | |
388 | /* Branch menu */ |
389 | |
390 | R.formatRevOptions = function (currentName) { |
391 | return function (name) { |
392 | var htmlName = u.escape(name) |
393 | return '<option value="' + htmlName + '"' + |
394 | (name == currentName ? ' selected="selected"' : '') + |
395 | '>' + htmlName + '</option>' |
396 | } |
397 | } |
398 | |
399 | R.formatRevType = function(req, type) { |
400 | return ( |
401 | type == 'heads' ? req._t('Branches') : |
402 | type == 'tags' ? req._t('Tags') : |
403 | type) |
404 | } |
405 | |
406 | R.revMenu = function (req, repo, currentName) { |
407 | var self = this |
408 | return u.readOnce(function (cb) { |
409 | repo.getRefNames(function (err, refs) { |
410 | if (err) return cb(err) |
411 | cb(null, '<select class="custom-dropdown" name="rev" onchange="this.form.submit()">' + |
412 | Object.keys(refs).map(function (group) { |
413 | return '<optgroup ' + |
414 | 'label="' + self.formatRevType(req, group) + '">' + |
415 | refs[group].map(self.formatRevOptions(currentName)).join('') + |
416 | '</optgroup>' |
417 | }).join('') + |
418 | '</select><noscript> ' + |
419 | '<input type="submit" value="' + req._t('Go') + '"/></noscript>') |
420 | }) |
421 | }) |
422 | } |
423 | |
424 | /* Repo tree */ |
425 | |
426 | function renderRepoLatest(req, repo, rev) { |
427 | if (!rev) return pull.empty() |
428 | return u.readOnce(function (cb) { |
429 | repo.getCommitParsed(rev, function (err, commit) { |
430 | if (err) return cb(err) |
431 | var commitPath = [repo.id, 'commit', commit.id] |
432 | cb(null, |
433 | req._t('Latest') + ': ' + |
434 | u.link(commitPath, commit.title) + |
435 | '<tt class="ml2 float-right">' + commit.id + '</tt><br/> ' + |
436 | req._t('CommittedOn', { |
437 | name: u.escape(commit.committer.name), |
438 | date: commit.committer.date.toLocaleString(req._locale) |
439 | }) + |
440 | (commit.separateAuthor ? '<br/>' + req._t('AuthoredOn', { |
441 | name: u.escape(commit.author.name), |
442 | date: commit.author.date.toLocaleString(req._locale) |
443 | }) : '')) |
444 | }) |
445 | }) |
446 | } |
447 | |
448 | // breadcrumbs |
449 | function linkPath(basePath, path) { |
450 | path = path.slice() |
451 | var last = path.pop() |
452 | return path.map(function (dir, i) { |
453 | return u.link(basePath.concat(path.slice(0, i+1)), dir) |
454 | }).concat(last).join(' / ') |
455 | } |
456 | |
457 | function renderRepoTree(req, repo, rev, path) { |
458 | var pathLinks = path.length === 0 ? '' : |
459 | ': ' + linkPath([repo.id, 'tree'], [rev].concat(path)) |
460 | return cat([ |
461 | pull.once('<h3>' + req._t('Files') + pathLinks + '</h3>'), |
462 | pull( |
463 | repo.readDir(rev, path), |
464 | pull.map(function (file) { |
465 | var type = (file.mode === 040000) ? 'tree' : |
466 | (file.mode === 0160000) ? 'commit' : 'blob' |
467 | if (type == 'commit') |
468 | return [ |
469 | '<span title="' + req._t('gitCommitLink') + '">🖈</span>', |
470 | '<span title="' + u.escape(file.id) + '">' + |
471 | u.escape(file.name) + '</span>'] |
472 | var filePath = [repo.id, type, rev].concat(path, file.name) |
473 | return ['<i>' + (type == 'tree' ? '📁' : '📄') + '</i>', |
474 | u.link(filePath, file.name)] |
475 | }), |
476 | table('class="files"') |
477 | ) |
478 | ]) |
479 | } |
480 | |
481 | /* Repo readme */ |
482 | |
483 | R.renderRepoReadme = function (req, repo, branch, path) { |
484 | var self = this |
485 | return u.readNext(function (cb) { |
486 | pull( |
487 | repo.readDir(branch, path), |
488 | pull.filter(function (file) { |
489 | return /readme(\.|$)/i.test(file.name) |
490 | }), |
491 | pull.take(1), |
492 | pull.collect(function (err, files) { |
493 | if (err) return cb(null, pull.empty()) |
494 | var file = files[0] |
495 | if (!file) |
496 | return cb(null, pull.once(path.length ? '' : |
497 | '<p>' + req._t('NoReadme') + '</p>')) |
498 | repo.getObjectFromAny(file.id, function (err, obj) { |
499 | if (err) return cb(err) |
500 | cb(null, cat([ |
501 | pull.once('<section class="readme">'), |
502 | self.web.renderObjectData(obj, file.name, repo, branch, path), |
503 | pull.once('</section>') |
504 | ])) |
505 | }) |
506 | }) |
507 | ) |
508 | }) |
509 | } |
510 | |
511 | /* Repo commit */ |
512 | |
513 | R.serveRepoCommit = function (req, repo, rev) { |
514 | var self = this |
515 | return u.readNext(function (cb) { |
516 | repo.getCommitParsed(rev, function (err, commit) { |
517 | if (err) return cb(err) |
518 | var commitPath = [repo.id, 'commit', commit.id] |
519 | var treePath = [repo.id, 'tree', commit.id] |
520 | var title = u.escape(commit.title) + ' · ' + |
521 | '%{author}/%{repo}@' + commit.id.substr(0, 8) |
522 | cb(null, self.renderRepoPage(req, repo, null, rev, title, cat([ |
523 | pull.once( |
524 | '<h3>' + u.link(commitPath, |
525 | req._t('CommitRev', {rev: rev})) + '</h3>' + |
526 | '<section class="collapse">' + |
527 | '<div class="right-bar">' + |
528 | u.link(treePath, req._t('BrowseFiles')) + |
529 | '</div>' + |
530 | '<h4>' + u.linkify(u.escape(commit.title)) + '</h4>' + |
531 | (commit.body ? u.linkify(u.pre(commit.body)) : '') + |
532 | (commit.separateAuthor ? req._t('AuthoredOn', { |
533 | name: u.escape(commit.author.name), |
534 | date: commit.author.date.toLocaleString(req._locale) |
535 | }) + '<br/>' : '') + |
536 | req._t('CommittedOn', { |
537 | name: u.escape(commit.committer.name), |
538 | date: commit.committer.date.toLocaleString(req._locale) |
539 | }) + '<br/>' + |
540 | commit.parents.map(function (id) { |
541 | return req._t('Parent') + ': ' + |
542 | u.link([repo.id, 'commit', id], id) |
543 | }).join('<br>') + |
544 | '</section>' + |
545 | '<section><h3>' + req._t('FilesChanged') + '</h3>'), |
546 | // TODO: show diff from all parents (merge commits) |
547 | self.renderDiffStat(req, [repo, repo], [commit.parents[0], commit.id]), |
548 | pull.once('</section>') |
549 | ]))) |
550 | }) |
551 | }) |
552 | } |
553 | |
554 | /* Repo tag */ |
555 | |
556 | R.serveRepoTag = function (req, repo, rev, path) { |
557 | var self = this |
558 | return u.readNext(function (cb) { |
559 | repo.getTagParsed(rev, function (err, tag) { |
560 | if (err) { |
561 | if (/Expected tag, got commit/.test(err.message)) { |
562 | req._u.pathname = u.encodeLink([repo.id, 'commit', rev].concat(path)) |
563 | return cb(null, self.web.serveRedirect(req, url.format(req._u))) |
564 | } |
565 | return cb(null, self.web.serveError(req, err)) |
566 | } |
567 | |
568 | var title = req._t('TagName', { |
569 | tag: u.escape(tag.tag) |
570 | }) + ' · %{author}/%{repo}' |
571 | var body = (tag.title + '\n\n' + |
572 | tag.body.replace(/-----BEGIN PGP SIGNATURE-----\n[^.]*?\n-----END PGP SIGNATURE-----\s*$/, '')).trim() |
573 | var date = tag.tagger.date |
574 | cb(null, self.renderRepoPage(req, repo, 'tags', tag.object, title, |
575 | pull.once( |
576 | '<section class="collapse">' + |
577 | '<h3>' + u.link([repo.id, 'tag', rev], tag.tag) + '</h3>' + |
578 | req._t('TaggedOn', { |
579 | name: u.escape(tag.tagger.name), |
580 | date: date && date.toLocaleString(req._locale) |
581 | }) + '<br/>' + |
582 | u.link([repo.id, tag.type, tag.object]) + |
583 | u.linkify(u.pre(body)) + |
584 | '</section>'))) |
585 | }) |
586 | }) |
587 | } |
588 | |
589 | |
590 | /* Diff stat */ |
591 | |
592 | R.renderDiffStat = function (req, repos, treeIds) { |
593 | if (treeIds.length == 0) treeIds = [null] |
594 | var id = treeIds[0] |
595 | var lastI = treeIds.length - 1 |
596 | var oldTree = treeIds[0] |
597 | var changedFiles = [] |
598 | return cat([ |
599 | pull( |
600 | GitRepo.diffTrees(repos, treeIds, true), |
601 | pull.map(function (item) { |
602 | var filename = u.escape(item.filename = item.path.join('/')) |
603 | var oldId = item.id && item.id[0] |
604 | var newId = item.id && item.id[lastI] |
605 | var oldMode = item.mode && item.mode[0] |
606 | var newMode = item.mode && item.mode[lastI] |
607 | var action = |
608 | !oldId && newId ? req._t('action.added') : |
609 | oldId && !newId ? req._t('action.deleted') : |
610 | oldMode != newMode ? req._t('action.changedMode', { |
611 | old: oldMode.toString(8), |
612 | new: newMode.toString(8) |
613 | }) : req._t('changed') |
614 | if (item.id) |
615 | changedFiles.push(item) |
616 | var blobsPath = item.id[1] |
617 | ? [repos[1].id, 'blob', treeIds[1]] |
618 | : [repos[0].id, 'blob', treeIds[0]] |
619 | var rawsPath = item.id[1] |
620 | ? [repos[1].id, 'raw', treeIds[1]] |
621 | : [repos[0].id, 'raw', treeIds[0]] |
622 | item.blobPath = blobsPath.concat(item.path) |
623 | item.rawPath = rawsPath.concat(item.path) |
624 | var fileHref = item.id ? |
625 | '#' + encodeURIComponent(item.path.join('/')) : |
626 | u.encodeLink(item.blobPath) |
627 | return ['<a href="' + fileHref + '">' + filename + '</a>', action] |
628 | }), |
629 | table() |
630 | ), |
631 | pull( |
632 | pull.values(changedFiles), |
633 | paramap(function (item, cb) { |
634 | var extension = u.getExtension(item.filename) |
635 | if (extension in u.imgMimes) { |
636 | var filename = u.escape(item.filename) |
637 | return cb(null, |
638 | '<pre><table class="code">' + |
639 | '<tr><th id="' + u.escape(item.filename) + '">' + |
640 | filename + '</th></tr>' + |
641 | '<tr><td><img src="' + u.encodeLink(item.rawPath) + '"' + |
642 | ' alt="' + filename + '"/></td></tr>' + |
643 | '</table></pre>') |
644 | } |
645 | var done = multicb({ pluck: 1, spread: true }) |
646 | var mode0 = item.mode && item.mode[0] |
647 | var modeI = item.mode && item.mode[lastI] |
648 | var isSubmodule = (modeI == 0160000) |
649 | getRepoObjectString(repos[0], item.id[0], mode0, done()) |
650 | getRepoObjectString(repos[1], item.id[lastI], modeI, done()) |
651 | done(function (err, strOld, strNew) { |
652 | if (err) return cb(err) |
653 | cb(null, htmlLineDiff(req, item.filename, item.filename, |
654 | strOld, strNew, |
655 | u.encodeLink(item.blobPath), !isSubmodule)) |
656 | }) |
657 | }, 4) |
658 | ) |
659 | ]) |
660 | } |
661 | |
662 | function htmlLineDiff(req, filename, anchor, oldStr, newStr, blobHref, |
663 | showViewLink) { |
664 | return '<pre><table class="code">' + |
665 | '<tr><th colspan=3 id="' + u.escape(anchor) + '">' + filename + |
666 | (showViewLink === false ? '' : |
667 | '<span class="right-bar">' + |
668 | '<a href="' + blobHref + '">' + req._t('View') + '</a> ' + |
669 | '</span>') + |
670 | '</th></tr>' + |
671 | (oldStr.length + newStr.length > 200000 |
672 | ? '<tr><td class="diff-info">' + req._t('diff.TooLarge') + '<br>' + |
673 | req._t('diff.OldFileSize', {bytes: oldStr.length}) + '<br>' + |
674 | req._t('diff.NewFileSize', {bytes: newStr.length}) + '</td></tr>' |
675 | : tableDiff(oldStr, newStr, filename)) + |
676 | '</table></pre>' |
677 | } |
678 | |
679 | function tableDiff(oldStr, newStr, filename) { |
680 | var diff = JsDiff.structuredPatch('', '', oldStr, newStr) |
681 | var groups = diff.hunks.map(function (hunk) { |
682 | var oldLine = hunk.oldStart |
683 | var newLine = hunk.newStart |
684 | var header = '<tr class="diff-hunk-header"><td colspan=2></td><td>' + |
685 | '@@ -' + oldLine + ',' + hunk.oldLines + ' ' + |
686 | '+' + newLine + ',' + hunk.newLines + ' @@' + |
687 | '</td></tr>' |
688 | return [header].concat(hunk.lines.map(function (line) { |
689 | var s = line[0] |
690 | if (s == '\\') return |
691 | var html = u.highlight(line, u.getExtension(filename)) |
692 | var trClass = s == '+' ? 'diff-new' : s == '-' ? 'diff-old' : '' |
693 | var lineNums = [s == '+' ? '' : oldLine++, s == '-' ? '' : newLine++] |
694 | var id = [filename].concat(lineNums).join('-') |
695 | return '<tr id="' + u.escape(id) + '" class="' + trClass + '">' + |
696 | lineNums.map(function (num) { |
697 | return '<td class="code-linenum">' + |
698 | (num ? '<a href="#' + encodeURIComponent(id) + '">' + |
699 | num + '</a>' : '') + '</td>' |
700 | }).join('') + |
701 | '<td class="code-text">' + html + '</td></tr>' |
702 | })) |
703 | }) |
704 | return [].concat.apply([], groups).join('') |
705 | } |
706 | |
707 | /* An unknown message linking to a repo */ |
708 | |
709 | R.serveRepoSomething = function (req, repo, id, msg, path) { |
710 | return this.renderRepoPage(req, repo, null, null, null, |
711 | pull.once('<section><h3>' + u.link([id]) + '</h3>' + |
712 | u.json(msg) + '</section>')) |
713 | } |
714 | |
715 | /* Repo update */ |
716 | |
717 | function objsArr(objs) { |
718 | return Array.isArray(objs) ? objs : |
719 | Object.keys(objs).map(function (sha1) { |
720 | var obj = Object.create(objs[sha1]) |
721 | obj.sha1 = sha1 |
722 | return obj |
723 | }) |
724 | } |
725 | |
726 | R.serveRepoUpdate = function (req, repo, id, msg, path) { |
727 | var self = this |
728 | var raw = req._u.query.raw != null |
729 | var title = req._t('Update') + ' · %{author}/%{repo}' |
730 | |
731 | if (raw) |
732 | return self.renderRepoPage(req, repo, 'activity', null, title, pull.once( |
733 | '<a href="?" class="raw-link header-align">' + |
734 | req._t('Info') + '</a>' + |
735 | '<h3>' + req._t('Update') + '</h3>' + |
736 | '<section class="collapse">' + |
737 | u.json({key: id, value: msg}) + '</section>')) |
738 | |
739 | // convert packs to old single-object style |
740 | if (msg.content.indexes) { |
741 | for (var i = 0; i < msg.content.indexes.length; i++) { |
742 | msg.content.packs[i] = { |
743 | pack: {link: msg.content.packs[i].link}, |
744 | idx: msg.content.indexes[i] |
745 | } |
746 | } |
747 | } |
748 | |
749 | var commits = cat([ |
750 | msg.content.objects && pull( |
751 | pull.values(msg.content.objects), |
752 | pull.filter(function (obj) { return obj.type == 'commit' }), |
753 | paramap(function (obj, cb) { |
754 | self.web.getBlob(req, obj.link || obj.key, function (err, readObject) { |
755 | if (err) return cb(err) |
756 | GitRepo.getCommitParsed({read: readObject}, cb) |
757 | }) |
758 | }, 8) |
759 | ), |
760 | msg.content.packs && pull( |
761 | pull.values(msg.content.packs), |
762 | paramap(function (pack, cb) { |
763 | var done = multicb({ pluck: 1, spread: true }) |
764 | self.web.getBlob(req, pack.pack.link, done()) |
765 | self.web.getBlob(req, pack.idx.link, done()) |
766 | done(function (err, readPack, readIdx) { |
767 | if (err) return cb(self.web.renderError(err)) |
768 | cb(null, gitPack.decodeWithIndex(repo, readPack, readIdx)) |
769 | }) |
770 | }, 4), |
771 | pull.flatten(), |
772 | pull.asyncMap(function (obj, cb) { |
773 | if (obj.type == 'commit') |
774 | GitRepo.getCommitParsed(obj, cb) |
775 | else |
776 | pull(obj.read, pull.drain(null, cb)) |
777 | }), |
778 | pull.filter() |
779 | ) |
780 | ]) |
781 | |
782 | return self.renderRepoPage(req, repo, 'activity', null, title, cat([ |
783 | pull.once('<a href="?raw" class="raw-link header-align">' + |
784 | req._t('Data') + '</a>' + |
785 | '<h3>' + req._t('Update') + '</h3>'), |
786 | pull( |
787 | pull.once({key: id, value: msg}), |
788 | pull.asyncMap(renderRepoUpdate.bind(self, req, repo, true)) |
789 | ), |
790 | (msg.content.objects || msg.content.packs) && |
791 | pull.once('<h3>' + req._t('Commits') + '</h3>'), |
792 | pull(commits, pull.map(function (commit) { |
793 | return renderCommit(req, repo, commit) |
794 | })) |
795 | ])) |
796 | } |
797 | |
798 | /* Blob */ |
799 | |
800 | R.serveRepoBlob = function (req, repo, rev, path) { |
801 | var self = this |
802 | return u.readNext(function (cb) { |
803 | repo.getFile(rev, path, function (err, object) { |
804 | if (err) return cb(null, self.web.serveBlobNotFound(req, repo.id, err)) |
805 | var type = repo.isCommitHash(rev) ? 'Tree' : 'Branch' |
806 | var pathLinks = path.length === 0 ? '' : |
807 | ': ' + linkPath([repo.id, 'tree'], [rev].concat(path)) |
808 | var rawFilePath = [repo.id, 'raw', rev].concat(path) |
809 | var dirPath = path.slice(0, path.length-1) |
810 | var filename = path[path.length-1] |
811 | var extension = u.getExtension(filename) |
812 | var title = (path.length ? path.join('/') + ' · ' : '') + |
813 | '%{author}/%{repo}' + |
814 | (repo.head == 'refs/heads/' + rev ? '' : '@' + rev) |
815 | cb(null, self.renderRepoPage(req, repo, 'code', rev, title, cat([ |
816 | pull.once('<section><form action="" method="get">' + |
817 | '<h3>' + req._t(type) + ': ' + rev + ' '), |
818 | self.revMenu(req, repo, rev), |
819 | pull.once('</h3></form>'), |
820 | type == 'Branch' && renderRepoLatest(req, repo, rev), |
821 | pull.once('</section><section class="collapse">' + |
822 | '<h3>' + req._t('Files') + pathLinks + '</h3>' + |
823 | '<div>' + object.length + ' bytes' + |
824 | '<span class="raw-link">' + |
825 | u.link(rawFilePath, req._t('Raw')) + '</span>' + |
826 | '</div></section>' + |
827 | '<section>'), |
828 | extension in u.imgMimes |
829 | ? pull.once('<img src="' + u.encodeLink(rawFilePath) + |
830 | '" alt="' + u.escape(filename) + '" />') |
831 | : self.web.renderObjectData(object, filename, repo, rev, dirPath), |
832 | pull.once('</section>') |
833 | ]))) |
834 | }) |
835 | }) |
836 | } |
837 | |
838 | /* Raw blob */ |
839 | |
840 | R.serveRepoRaw = function (req, repo, branch, path) { |
841 | var self = this |
842 | return u.readNext(function (cb) { |
843 | repo.getFile(branch, path, function (err, object) { |
844 | if (err) return cb(null, |
845 | self.web.serveBuffer(404, req._t('error.BlobNotFound'))) |
846 | var extension = u.getExtension(path[path.length-1]) |
847 | var contentType = u.imgMimes[extension] |
848 | cb(null, pull(object.read, self.web.serveRaw(object.length, contentType))) |
849 | }) |
850 | }) |
851 | } |
852 | |
853 | /* Digs */ |
854 | |
855 | R.serveRepoDigs = function (req, repo) { |
856 | var self = this |
857 | return u.readNext(function (cb) { |
858 | var title = req._t('Digs') + ' · %{author}/%{repo}' |
859 | self.web.getVotes(repo.id, function (err, votes) { |
860 | cb(null, self.renderRepoPage(req, repo, null, null, title, cat([ |
861 | pull.once('<section><h3>' + req._t('Digs') + '</h3>' + |
862 | '<div>' + req._t('Total') + ': ' + votes.upvotes + '</div>'), |
863 | pull( |
864 | pull.values(Object.keys(votes.upvoters)), |
865 | paramap(function (feedId, cb) { |
866 | self.web.about.getName(feedId, function (err, name) { |
867 | if (err) return cb(err) |
868 | cb(null, u.link([feedId], name)) |
869 | }) |
870 | }, 8), |
871 | ul() |
872 | ), |
873 | pull.once('</section>') |
874 | ]))) |
875 | }) |
876 | }) |
877 | } |
878 | |
879 | /* Forks */ |
880 | |
881 | R.getForks = function (repo, includeSelf) { |
882 | var self = this |
883 | return pull( |
884 | cat([ |
885 | includeSelf && pull.once(repo.id), |
886 | // get downstream repos |
887 | pull( |
888 | self.web.ssb.links({ |
889 | dest: repo.id, |
890 | rel: 'upstream' |
891 | }), |
892 | pull.map('key') |
893 | ), |
894 | // look for other repos that previously had pull requests to this one |
895 | pull( |
896 | self.web.ssb.links({ |
897 | dest: repo.id, |
898 | values: true, |
899 | rel: 'project' |
900 | }), |
901 | pull.filter(function (msg) { |
902 | var c = msg && msg.value && msg.value.content |
903 | return c && c.type == 'pull-request' |
904 | }), |
905 | pull.map(function (msg) { return msg.value.content.head_repo }) |
906 | ) |
907 | ]), |
908 | pull.unique(), |
909 | paramap(function (key, cb) { |
910 | self.web.ssb.get(key, function (err, value) { |
911 | if (err) cb(err) |
912 | else cb(null, {key: key, value: value}) |
913 | }) |
914 | }, 4), |
915 | pull.filter(function (msg) { |
916 | var c = msg && msg.value && msg.value.content |
917 | return c && c.type == 'git-repo' |
918 | }), |
919 | paramap(function (msg, cb) { |
920 | self.web.getRepoFullName(msg.value.author, msg.key, |
921 | function (err, repoName, authorName) { |
922 | if (err) return cb(err) |
923 | cb(null, { |
924 | key: msg.key, |
925 | value: msg.value, |
926 | repoName: repoName, |
927 | authorName: authorName |
928 | }) |
929 | }) |
930 | }, 8) |
931 | ) |
932 | } |
933 | |
934 | R.serveRepoForks = function (req, repo) { |
935 | var hasForks |
936 | var title = req._t('Forks') + ' · %{author}/%{repo}' |
937 | return this.renderRepoPage(req, repo, null, null, title, cat([ |
938 | pull.once('<h3>' + req._t('Forks') + '</h3>'), |
939 | pull( |
940 | this.getForks(repo), |
941 | pull.map(function (msg) { |
942 | hasForks = true |
943 | return '<section class="collapse">' + |
944 | u.link([msg.value.author], msg.authorName) + ' / ' + |
945 | u.link([msg.key], msg.repoName) + |
946 | '<span class="right-bar">' + |
947 | u.timestamp(msg.value.timestamp, req) + |
948 | '</span></section>' |
949 | }) |
950 | ), |
951 | u.readOnce(function (cb) { |
952 | cb(null, hasForks ? '' : req._t('NoForks')) |
953 | }) |
954 | ])) |
955 | } |
956 | |
957 | R.serveRepoForkPrompt = function (req, repo) { |
958 | var title = req._t('Fork') + ' · %{author}/%{repo}' |
959 | return this.renderRepoPage(req, repo, null, null, title, pull.once( |
960 | '<form action="" method="post" onreset="history.back()">' + |
961 | '<h3>' + req._t('ForkRepoPrompt') + '</h3>' + |
962 | '<p>' + u.hiddenInputs({ id: repo.id }) + |
963 | '<button class="btn open" type="submit" name="action" value="fork">' + |
964 | req._t('Fork') + |
965 | '</button>' + |
966 | ' <button class="btn" type="reset">' + |
967 | req._t('Cancel') + '</button>' + |
968 | '</p></form>' |
969 | )) |
970 | } |
971 | |
972 | R.serveIssueOrPullRequest = function (req, repo, issue, path, id) { |
973 | return issue.msg.value.content.type == 'pull-request' |
974 | ? this.pulls.serveRepoPullReq(req, repo, issue, path, id) |
975 | : this.issues.serveRepoIssue(req, repo, issue, path, id) |
976 | } |
977 |
Built with git-ssb-web