Files: 4f5323ab2d208ccdc1bd0837f332f0f560bfcecf / lib / repos / index.js
46072 bytesRaw
1 | var url = require('url') |
2 | var pull = require('pull-stream') |
3 | var once = pull.once |
4 | var cat = require('pull-cat') |
5 | var paramap = require('pull-paramap') |
6 | var multicb = require('multicb') |
7 | var JsDiff = require('diff') |
8 | var GitRepo = require('pull-git-repo') |
9 | var gitPack = require('pull-git-pack') |
10 | var u = require('../util') |
11 | var paginate = require('pull-paginate') |
12 | var markdown = require('../markdown') |
13 | var forms = require('../forms') |
14 | var ssbRef = require('ssb-ref') |
15 | var zlib = require('zlib') |
16 | var toPull = require('stream-to-pull-stream') |
17 | var h = require('pull-hyperscript') |
18 | var getObjectMsgId = require('../../lib/obj-msg-id') |
19 | |
20 | function extend(obj, props) { |
21 | for (var k in props) |
22 | obj[k] = props[k] |
23 | return obj |
24 | } |
25 | |
26 | module.exports = function (web) { |
27 | return new RepoRoutes(web) |
28 | } |
29 | |
30 | function RepoRoutes(web) { |
31 | this.web = web |
32 | this.issues = require('./issues')(this, web) |
33 | this.pulls = require('./pulls')(this, web) |
34 | } |
35 | |
36 | var R = RepoRoutes.prototype |
37 | |
38 | function getRepoObjectString(repo, id, mode, cb) { |
39 | if (!id) return cb(null, '') |
40 | if (mode == 0160000) return cb(null, |
41 | 'Subproject commit ' + id) |
42 | repo.getObjectFromAny(id, function (err, obj) { |
43 | if (err) return cb(err) |
44 | u.readObjectString(obj, cb) |
45 | }) |
46 | } |
47 | |
48 | /* Repo */ |
49 | |
50 | R.getLineCommentThreads = function (req, repo, updateId, commitId, filename, cb) { |
51 | var self = this |
52 | var sbot = self.web.ssb |
53 | var lineCommentThreads = {} |
54 | pull( |
55 | sbot.backlinks ? sbot.backlinks.read({ |
56 | query: [ |
57 | {$filter: { |
58 | dest: updateId || repo.id, |
59 | value: { |
60 | content: { |
61 | type: 'line-comment', |
62 | repo: repo.id, |
63 | commitId: commitId, |
64 | filePath: filename |
65 | } |
66 | } |
67 | }} |
68 | ] |
69 | }) : pull( |
70 | sbot.links({ |
71 | dest: updateId, |
72 | rel: 'updateId', |
73 | values: true |
74 | }), |
75 | pull.filter(function (msg) { |
76 | var c = msg && msg.value && msg.value.content |
77 | return c && c.type === 'line-comment' |
78 | && c.updateId === updateId |
79 | && c.commitId === commitId |
80 | && c.filePath === filename |
81 | }) |
82 | ), |
83 | paramap(function (msg, cb) { |
84 | pull( |
85 | self.renderThread(req, repo, msg), |
86 | pull.collect(function (err, parts) { |
87 | if (err) return cb(err) |
88 | cb(null, { |
89 | line: msg.value.content.line, |
90 | html: parts.join(''), |
91 | }) |
92 | }) |
93 | ) |
94 | }, 4), |
95 | pull.drain(function (thread) { |
96 | lineCommentThreads[thread.line] = thread.html |
97 | }, function (err) { |
98 | if (err) return cb(err) |
99 | cb(null, lineCommentThreads) |
100 | }) |
101 | ) |
102 | } |
103 | |
104 | R.renderThread = function (req, repo, msg) { |
105 | var newestMsg = msg |
106 | var root = msg.key |
107 | var self = this |
108 | return h('div', [ |
109 | pull( |
110 | cat([ |
111 | pull.once(msg), |
112 | self.web.ssb.backlinks ? self.web.ssb.backlinks.read({ |
113 | query: [ |
114 | {$filter: { |
115 | dest: root |
116 | }} |
117 | ] |
118 | }) : self.web.ssb.links({ |
119 | dest: root, |
120 | values: true |
121 | }), |
122 | ]), |
123 | pull.unique('key'), |
124 | u.decryptMessages(self.web.ssb), |
125 | u.readableMessages(), |
126 | self.web.addAuthorName(), |
127 | u.sortMsgs(), |
128 | pull.filter(function (msg) { |
129 | var c = msg && msg.value && msg.value.content |
130 | return c && ( |
131 | (c.type === 'post' && c.root === root) |
132 | || msg.key === root) |
133 | }), |
134 | pull.through(function (msg) { |
135 | // TODO: correctly calculate the thread branches |
136 | if (msg.value |
137 | && msg.value.timestamp > newestMsg.value.timestamp) |
138 | newestMsg = msg |
139 | }), |
140 | pull.map(function (msg) { |
141 | return self.renderLineComment(req, repo, msg) |
142 | }) |
143 | ), |
144 | self.web.isPublic ? '' : |
145 | pull.once(forms.lineCommentReply(req, root, newestMsg.key)) |
146 | ]) |
147 | } |
148 | |
149 | R.renderLineComment = function (req, repo, msg) { |
150 | var c = msg && msg.value && msg.value.content |
151 | var id = u.msgIdToDomId(msg.key) |
152 | return h('section', {class: 'collapse', id: id}, [ |
153 | h('div', [ |
154 | u.link([msg.value.author], msg.authorName), |
155 | ' ', |
156 | h('tt', {class: 'right-bar item-id'}, msg.key), |
157 | ' · ', |
158 | h('a', {href: u.encodeLink(msg.key) + '#' + id}, new Date(msg.value.timestamp).toLocaleString(req._locale)), |
159 | ]), |
160 | markdown(c.text, repo) |
161 | ]) |
162 | } |
163 | |
164 | R.serveRepoPage = function (req, repo, path) { |
165 | var self = this |
166 | var defaultBranch = 'master' |
167 | var query = req._u.query |
168 | |
169 | if (query.rev != null) { |
170 | // Allow navigating revs using GET query param. |
171 | // Replace the branch in the path with the rev query value |
172 | path[0] = path[0] || 'tree' |
173 | path[1] = query.rev |
174 | req._u.pathname = u.encodeLink([repo.id].concat(path)) |
175 | delete req._u.query.rev |
176 | delete req._u.search |
177 | return self.web.serveRedirect(req, url.format(req._u)) |
178 | } |
179 | |
180 | // get branch |
181 | return path[1] ? |
182 | R_serveRepoPage2.call(self, req, repo, path) : |
183 | u.readNext(function (cb) { |
184 | // TODO: handle this in pull-git-repo or ssb-git-repo |
185 | repo.getSymRef('HEAD', true, function (err, ref) { |
186 | if (err) return cb(err) |
187 | repo.resolveRef(ref, function (err, rev) { |
188 | path[1] = rev ? ref : null |
189 | cb(null, R_serveRepoPage2.call(self, req, repo, path)) |
190 | }) |
191 | }) |
192 | }) |
193 | } |
194 | |
195 | function R_serveRepoPage2(req, repo, path) { |
196 | var branch = path[1] |
197 | var filePath = path.slice(2) |
198 | switch (path[0]) { |
199 | case undefined: |
200 | case '': |
201 | return this.serveRepoTree(req, repo, branch, []) |
202 | case 'activity': |
203 | return this.serveRepoActivity(req, repo, branch) |
204 | case 'commits': |
205 | return this.serveRepoCommits(req, repo, branch) |
206 | case 'commit': |
207 | return this.serveRepoCommit(req, repo, path[1], filePath) |
208 | case 'tag': |
209 | return this.serveRepoTag(req, repo, branch, filePath) |
210 | case 'tree': |
211 | return this.serveRepoTree(req, repo, branch, filePath) |
212 | case 'blob': |
213 | return this.serveRepoBlob(req, repo, branch, filePath) |
214 | case 'raw': |
215 | return this.serveRepoRaw(req, repo, branch, filePath) |
216 | case 'digs': |
217 | return this.serveRepoDigs(req, repo) |
218 | case 'fork': |
219 | return this.serveRepoForkPrompt(req, repo) |
220 | case 'forks': |
221 | return this.serveRepoForks(req, repo) |
222 | case 'issues': |
223 | switch (path[1]) { |
224 | case 'new': |
225 | if (filePath.length == 0) |
226 | return this.issues.serveRepoNewIssue(req, repo) |
227 | break |
228 | default: |
229 | return this.issues.serveRepoIssues(req, repo, false) |
230 | } |
231 | case 'pulls': |
232 | return this.issues.serveRepoIssues(req, repo, true) |
233 | case 'compare': |
234 | return this.pulls.serveRepoCompare(req, repo) |
235 | case 'comparing': |
236 | return this.pulls.serveRepoComparing(req, repo) |
237 | case 'info': |
238 | switch (path[1]) { |
239 | case 'refs': |
240 | return this.serveRepoRefs(req, repo) |
241 | default: |
242 | return this.web.serve404(req) |
243 | } |
244 | case 'objects': |
245 | switch (path[1]) { |
246 | case 'info': |
247 | switch (path[2]) { |
248 | case 'packs': |
249 | return this.serveRepoPacksInfo(req, repo) |
250 | default: |
251 | return this.web.serve404(req) |
252 | } |
253 | case 'pack': |
254 | return this.serveRepoPack(req, repo, filePath.join('/')) |
255 | default: |
256 | var hash = path[1] + path[2] |
257 | if (hash.length === 40) { |
258 | return this.serveRepoObject(req, repo, hash) |
259 | } |
260 | return this.web.serve404(req) |
261 | } |
262 | case 'HEAD': |
263 | return this.serveRepoHead(req, repo) |
264 | default: |
265 | return this.web.serve404(req) |
266 | } |
267 | } |
268 | |
269 | R.serveRepoNotFound = function (req, id, err) { |
270 | return this.web.serveTemplate(req, req._t('error.RepoNotFound'), 404) |
271 | (pull.values([ |
272 | '<h2>' + req._t('error.RepoNotFound') + '</h2>', |
273 | '<p>' + req._t('error.RepoIdNotFound', id) + '</p>', |
274 | '<pre>' + u.escape(err.stack) + '</pre>' |
275 | ])) |
276 | } |
277 | |
278 | R.serveRepoTemplate = function (req, repo, page, branch, titleTemplate, body) { |
279 | var self = this |
280 | var digsPath = [repo.id, 'digs'] |
281 | |
282 | var done = multicb({ pluck: 1, spread: true }) |
283 | self.web.getRepoName(repo.feed, repo.id, done()) |
284 | self.web.about.getName(repo.feed, done()) |
285 | self.web.getVotes(repo.id, done()) |
286 | |
287 | if (repo.upstream) { |
288 | self.web.getRepoName(repo.upstream.feed, repo.upstream.id, done()) |
289 | self.web.about.getName(repo.upstream.feed, done()) |
290 | } |
291 | |
292 | return u.readNext(function (cb) { |
293 | done(function (err, repoName, authorName, votes, upstreamName, upstreamAuthorName) { |
294 | if (err) return cb(null, self.web.serveError(req, err)) |
295 | var upvoted = votes.upvoters[self.web.myId] > 0 |
296 | var upstreamLink = !repo.upstream ? '' : |
297 | u.link([repo.upstream]) |
298 | var title = titleTemplate ? titleTemplate |
299 | .replace(/%\{repo\}/g, repoName) |
300 | .replace(/%\{author\}/g, authorName || '') |
301 | : (authorName ? authorName + '/' : '') + repoName |
302 | var isPublic = self.web.isPublic |
303 | var isLocal = !isPublic |
304 | var isChannelRepo = /^#/.test(repo.id) |
305 | cb(null, self.web.serveTemplate(req, title)(cat([ |
306 | h('div', {class: 'repo-title'}, isChannelRepo ? [ |
307 | h('h2', {class: 'bgslash'}, |
308 | (authorName ? u.link([repo.feed], authorName, false, 'class="repo-author"') + ' / ' : '') + |
309 | u.link([repo.id], repoName) + |
310 | (repo.private ? ' ' + u.privateIcon(req) : '')) |
311 | ] : [ |
312 | h('form', {class: 'right-bar', action: '', method: 'post'}, [ |
313 | h('strong', {class: 'ml2 mr1'}, u.link(digsPath, votes.upvotes)), |
314 | h('button', |
315 | extend( |
316 | {class: 'btn', name: 'action', value: 'vote'}, |
317 | isPublic ? {disabled: 'disabled'} : {type: 'submit'} |
318 | ), [ |
319 | h('i', '✌ '), |
320 | h('span', req._t(isLocal && upvoted ? 'Undig' : 'Dig')) |
321 | ] |
322 | ), |
323 | u.when(isLocal, () => cat([ |
324 | h('input', {type: 'hidden', name: 'value', value: (upvoted ? '0' : '1')}), |
325 | h('input', {type: 'hidden', name: 'id', value: u.escape(repo.id)}) |
326 | ])), |
327 | h('a', {href: u.encodeLink([repo.id, 'forks']), title: req._t('Forks'), class: 'ml2 mr1'}, '+'), |
328 | u.when(isLocal, () => |
329 | h('button', {class: 'btn', type: 'submit', name: 'action', value: 'fork-prompt'}, [ |
330 | h('i', '⑂ '), |
331 | once(req._t('Fork')) |
332 | ]) |
333 | ) |
334 | ]), |
335 | forms.name(req, isLocal, repo.id, repoName, 'repo-name', null, req._t('repo.Rename'), |
336 | h('h2', {class: 'bgslash'}, |
337 | (authorName ? u.link([repo.feed], authorName, false, 'class="repo-author"') + ' / ' : '') + |
338 | u.link([repo.id], repoName) + |
339 | (repo.private ? ' ' + u.privateIcon(req) : '')) |
340 | ), |
341 | ]), |
342 | u.when(repo.upstream, () => |
343 | h('small', {class: 'bgslash'}, req._t('ForkedFrom', { |
344 | repo: `${u.link([repo.upstream.feed], upstreamAuthorName)} / ${u.link([repo.upstream.id], upstreamName)}` |
345 | })) |
346 | ), |
347 | u.nav([ |
348 | [[repo.id], req._t('Code'), 'code'], |
349 | [[repo.id, 'activity'], req._t('Activity'), 'activity'], |
350 | [[repo.id, 'commits', branch||''], req._t('Commits'), 'commits'], |
351 | !isChannelRepo && [[repo.id, 'issues'], self.web.indexCache ? req._t('IssuesN', { |
352 | count: self.web.indexCache.getIssuesCount(repo.id, '…') |
353 | }) : req._t('Issues'), 'issues'], |
354 | !isChannelRepo && [[repo.id, 'pulls'], self.web.indexCache ? req._t('PullRequestsN', { |
355 | count: self.web.indexCache.getPRsCount(repo.id, '…') |
356 | }) : req._t('PullRequests'), 'pulls'] |
357 | ], page), |
358 | body |
359 | ]) |
360 | )) |
361 | }) |
362 | }) |
363 | } |
364 | |
365 | R.renderEmptyRepo = function (req, repo) { |
366 | if (repo.feed != this.web.myId) |
367 | return h('section', [ |
368 | h('h3', req._t('EmptyRepo')) |
369 | ]) |
370 | |
371 | var gitUrl = 'ssb://' + repo.id |
372 | return h('section', [ |
373 | h('h3', req._t('initRepo.GettingStarted')), |
374 | h('h4', req._t('initRepo.CreateNew')), |
375 | preInitRepo(req, gitUrl), |
376 | h('h4', req._t('initRepo.PushExisting')), |
377 | preRemote(gitUrl) |
378 | ]) |
379 | } |
380 | |
381 | var preInitRepo = (req, gitUrl) => h('pre', |
382 | `touch ${req._t('initRepo.README')}.md |
383 | git init |
384 | git add ${req._t('initRepo.README')}.md |
385 | git commit -m ${req._t('initRepo.InitialCommit')} |
386 | git remote add origin ${gitUrl} |
387 | git push -u origin master`) |
388 | |
389 | var preRemote = (gitUrl) => h('pre', |
390 | `git remote add origin ${gitUrl} |
391 | git push -u origin master`) |
392 | |
393 | |
394 | R.serveRepoTree = function (req, repo, rev, path) { |
395 | var type = repo.isCommitHash(rev) ? 'Tree' : 'Branch' |
396 | var title = |
397 | (path.length ? `${path.join('/')} · ` : '') + |
398 | '%{author}/%{repo}' + |
399 | (repo.head == `refs/heads/${rev}` ? '' : `@${rev}`) |
400 | var gitUrl = 'ssb://' + repo.id |
401 | var host = req.headers.host || '127.0.0.1:7718' |
402 | var targetpath = '/' + encodeURIComponent(repo.id) |
403 | var httpUrl = 'http://' + encodeURI(host) + targetpath |
404 | var cloneUrls = '<div class="clone-urls">' + |
405 | '<select class="custom-dropdown clone-url-protocol" ' + |
406 | 'onchange="with(this.nextSibling.firstChild) {' + |
407 | 'value = this.value; select() }">' + |
408 | '<option selected="selected" value="' + gitUrl + '">SSB</option>' + |
409 | '<option class="http-clone-url" value="' + httpUrl + '">HTTP</option>' + |
410 | '</select>' + |
411 | '<div class="clone-url-wrapper">' + |
412 | '<input class="clone-url" readonly="readonly" ' + |
413 | 'value="ssb://' + repo.id + '" size="45" ' + |
414 | 'onclick="this.select()"/>' + |
415 | '<script>' + |
416 | 'var httpOpt = document.querySelector(".http-clone-url")\n' + |
417 | 'if (location.protocol === "https:") httpOpt.text = "HTTPS"\n' + |
418 | 'httpOpt.value = location.origin + "' + targetpath + '"\n' + |
419 | '</script>' + |
420 | '</div>' + |
421 | '</div>' |
422 | |
423 | return this.serveRepoTemplate(req, repo, 'code', rev, title, |
424 | u.readNext((cb) => { |
425 | if (!rev) return cb(null, this.renderEmptyRepo(req, repo)) |
426 | repo.getLatestAvailableRev(rev, 10e3, (err, revGot, numSkipped) => { |
427 | if (err) return cb(err) |
428 | cb(null, cat([ |
429 | h('section', {class: 'branch-info light-grey', method: 'get'}, [ |
430 | h('form', {action: '', method: 'get'}, [ |
431 | h('div', {class: 'rev-menu-line'}, [ |
432 | h('span', `${req._t(type)}: `), |
433 | this.revMenu(req, repo, rev) |
434 | ]), |
435 | cloneUrls |
436 | ]), |
437 | u.when(numSkipped > 0, () => |
438 | h('div', {class: 'missing-blobs-warning mt2'}, |
439 | h('em', req._t('missingBlobsWarning', numSkipped)) |
440 | ) |
441 | ) |
442 | ]), |
443 | h('section', {class: 'files'}, renderRepoTree(req, repo, revGot, path, type)), |
444 | this.renderRepoReadme(req, repo, revGot, path) |
445 | ])) |
446 | }) |
447 | })) |
448 | } |
449 | |
450 | /* Repo activity */ |
451 | |
452 | R.serveRepoActivity = function (req, repo, branch) { |
453 | var self = this |
454 | var title = req._t('Activity') + ' · %{author}/%{repo}' |
455 | return self.serveRepoTemplate(req, repo, 'activity', branch, title, cat([ |
456 | h('h3', req._t('Activity')), |
457 | pull( |
458 | self.web.ssb.backlinks ? self.web.ssb.backlinks.read({ |
459 | query: [ |
460 | {$filter: { |
461 | dest: repo.id, |
462 | value: { |
463 | content: { |
464 | repo: repo.id |
465 | } |
466 | } |
467 | }} |
468 | ] |
469 | }) : self.web.ssb.links({ |
470 | dest: repo.id, |
471 | rel: 'repo', |
472 | values: true |
473 | }), |
474 | pull.unique('key'), |
475 | u.decryptMessages(self.web.ssb), |
476 | u.sortMsgs(true), |
477 | pull.asyncMap(renderRepoUpdate.bind(self, req, repo, false)) |
478 | ), |
479 | u.readOnce(function (cb) { |
480 | var done = multicb({ pluck: 1, spread: true }) |
481 | self.web.about.getName(repo.feed, done()) |
482 | self.web.getMsg(repo.id, done()) |
483 | done(function (err, authorName, msg) { |
484 | if (err) return cb(err) |
485 | self.web.renderFeedItem(req, { |
486 | key: repo.id, |
487 | value: msg.value, |
488 | authorName: authorName |
489 | }, cb) |
490 | }) |
491 | }) |
492 | ])) |
493 | } |
494 | |
495 | function renderRepoUpdate(req, repo, full, msg, cb) { |
496 | var c = msg.value.content |
497 | |
498 | if (c.type != 'git-update') { |
499 | return cb(null, '') |
500 | // return renderFeedItem(msg, cb) |
501 | // TODO: render post, issue, pull-request |
502 | } |
503 | |
504 | var branches = [] |
505 | var tags = [] |
506 | if (c.refs) for (var name in c.refs) { |
507 | var m = name.match(/^refs\/(heads|tags)\/(.*)$/) || [,, name] |
508 | ;(m[1] == 'tags' ? tags : branches) |
509 | .push({name: m[2], value: c.refs[name]}) |
510 | } |
511 | var numObjects = c.objects ? Object.keys(c.objects).length : 0 |
512 | |
513 | var dateStr = new Date(msg.value.timestamp).toLocaleString(req._locale) |
514 | |
515 | this.web.about.getName(msg.value.author, function (err, name) { |
516 | if (err) return cb(err) |
517 | cb(null, '<section class="collapse">' + |
518 | u.link([msg.key], dateStr) + '<br>' + |
519 | u.link([msg.value.author], name) + '<br>' + |
520 | |
521 | branches.map(function (update) { |
522 | if (!update.value) { |
523 | return '<s>' + u.escape(update.name) + '</s><br/>' |
524 | } else { |
525 | var commitLink = u.link([repo.id, 'commit', update.value]) |
526 | var branchLink = u.link([repo.id, 'tree', update.name]) |
527 | return branchLink + ' → <tt>' + commitLink + '</tt><br/>' |
528 | } |
529 | }).join('') + |
530 | tags.map(function (update) { |
531 | return update.value |
532 | ? u.link([repo.id, 'tag', update.value], update.name) |
533 | : '<s>' + u.escape(update.name) + '</s>' |
534 | }).join(', ') + |
535 | '</section>') |
536 | }) |
537 | } |
538 | |
539 | /* Repo commits */ |
540 | |
541 | R.serveRepoCommits = function (req, repo, branch) { |
542 | var query = req._u.query |
543 | var title = req._t('Commits') + ' · %{author}/%{repo}' |
544 | var head = query.start || branch |
545 | return this.serveRepoTemplate(req, repo, 'commits', branch, title, cat([ |
546 | pull.once('<h3>' + req._t('Commits') + '</h3>'), |
547 | head ? pull( |
548 | repo.readLog(head), |
549 | pull.take(20), |
550 | paramap(repo.getCommitParsed.bind(repo), 8), |
551 | paginate( |
552 | !query.start ? '' : function (first, cb) { |
553 | cb(null, '…') |
554 | }, |
555 | pull.map(renderCommit.bind(this, req, repo)), |
556 | function (commit, cb) { |
557 | cb(null, commit.parents && commit.parents[0] ? |
558 | '<a href="?start=' + commit.id + '">' + |
559 | req._t('Older') + '</a>' : '') |
560 | } |
561 | ) |
562 | ) : pull.empty() |
563 | ])) |
564 | } |
565 | |
566 | function renderCommit(req, repo, commit) { |
567 | var commitPath = [repo.id, 'commit', commit.id] |
568 | var treePath = [repo.id, 'tree', commit.id] |
569 | return '<section class="collapse">' + |
570 | '<strong>' + u.link(commitPath, commit.title) + '</strong><br>' + |
571 | '<tt>' + commit.id + '</tt> ' + |
572 | u.link(treePath, req._t('Tree')) + '<br>' + |
573 | u.escape(commit.author.name) + ' · ' + |
574 | commit.author.date.toLocaleString(req._locale) + |
575 | (commit.separateAuthor ? '<br>' + req._t('CommittedOn', { |
576 | name: u.escape(commit.committer.name), |
577 | date: commit.committer.date.toLocaleString(req._locale) |
578 | }) : '') + |
579 | '</section>' |
580 | } |
581 | |
582 | /* Branch menu */ |
583 | |
584 | R.formatRevOptions = function (currentName) { |
585 | return function (name) { |
586 | var htmlName = u.escape(name) |
587 | return '<option value="' + htmlName + '"' + |
588 | (name == currentName ? ' selected="selected"' : '') + |
589 | '>' + htmlName + '</option>' |
590 | } |
591 | } |
592 | |
593 | R.formatRevType = function(req, type) { |
594 | return ( |
595 | type == 'heads' ? req._t('Branches') : |
596 | type == 'tags' ? req._t('Tags') : |
597 | type == 'remotes' ? req._t('Remotes') : |
598 | type) |
599 | } |
600 | |
601 | R.revMenu = function (req, repo, currentName) { |
602 | var self = this |
603 | return u.readOnce(function (cb) { |
604 | repo.getRefNames(function (err, refs) { |
605 | if (err) return cb(err) |
606 | cb(null, '<select class="custom-dropdown" name="rev" onchange="this.form.submit()">' + |
607 | Object.keys(refs).map(function (group) { |
608 | return '<optgroup ' + |
609 | 'label="' + self.formatRevType(req, group) + '">' + |
610 | refs[group].map(self.formatRevOptions(currentName)).join('') + |
611 | '</optgroup>' |
612 | }).join('') + |
613 | '</select><noscript> ' + |
614 | '<input type="submit" value="' + req._t('Go') + '"/></noscript>') |
615 | }) |
616 | }) |
617 | } |
618 | |
619 | /* Repo tree */ |
620 | |
621 | function renderRepoLatest(req, repo, rev) { |
622 | if (!rev) return pull.empty() |
623 | return u.readOnce(function (cb) { |
624 | repo.getCommitParsed(rev, function (err, commit) { |
625 | if (err) return cb(err) |
626 | var commitPath = [repo.id, 'commit', commit.id] |
627 | var actor = commit.separateAuthor ? 'author' : 'committer' |
628 | var actionKey = actor.slice(0,1).toUpperCase() + actor.slice(1) + 'ReleasedCommit' |
629 | cb(null, |
630 | '<span>' + |
631 | req._t(actionKey, { |
632 | name: u.escape(commit[actor].name), |
633 | commitName: u.link(commitPath, commit.title) |
634 | }) + |
635 | '</span>' + |
636 | '<span class="float-right">' + |
637 | req._t('LatestOn', { |
638 | commitId: commit.id.slice(0, 7), |
639 | date: commit[actor].date.toLocaleString(req._locale) |
640 | }) + |
641 | '</span>' |
642 | ) |
643 | }) |
644 | }) |
645 | } |
646 | |
647 | // breadcrumbs |
648 | function linkPath(basePath, path) { |
649 | path = path.slice() |
650 | var last = path.pop() |
651 | return path.map(function (dir, i) { |
652 | return u.link(basePath.concat(path.slice(0, i+1)), dir) |
653 | }).concat(last).join(' / ') |
654 | } |
655 | |
656 | function renderRepoTree(req, repo, rev, path, type) { |
657 | var source = repo.readDir(rev,path) |
658 | var pathLinks = path.length === 0 ? '' : |
659 | ': ' + linkPath([repo.id, 'tree'], [rev].concat(path)) |
660 | |
661 | var location = once('') |
662 | if (path.length !== 0) { |
663 | var link = linkPath([repo.id, 'tree'], [rev].concat(path)) |
664 | location = h('div', {class: 'fileLocation'}, `${req._t('Files')}: ${link}`) |
665 | } |
666 | |
667 | return cat([ |
668 | location, |
669 | h('table', {class: "files w-100", cellspacing: "0"}, cat([ |
670 | u.when(type === 'Branch', () => |
671 | h('thead', h('tr', h('td', {colspan: '2'}, [ |
672 | renderRepoLatest(req, repo, rev) |
673 | ]))) |
674 | ), |
675 | u.sourceMap(source, file => |
676 | h('tr', [ |
677 | h('td', [ |
678 | h('i', fileIcon(file)) |
679 | ]), |
680 | h('td', u.link(filePath(file), file.name)) |
681 | ]) |
682 | ) |
683 | ])) |
684 | ]) |
685 | |
686 | function fileIcon(file) { |
687 | return fileType(file) === 'tree' ? '📁' : '📄' |
688 | } |
689 | |
690 | function filePath(file) { |
691 | var type = fileType(file) |
692 | return [repo.id, type, rev].concat(path, file.name) |
693 | } |
694 | |
695 | function fileType(file) { |
696 | if (file.mode === 040000) return 'tree' |
697 | else if (file.mode === 0160000) return 'commit' |
698 | else return 'blob' |
699 | } |
700 | } |
701 | |
702 | /* Repo readme */ |
703 | |
704 | R.renderRepoReadme = function (req, repo, branch, path) { |
705 | var self = this |
706 | return u.readNext(function (cb) { |
707 | pull( |
708 | repo.readDir(branch, path), |
709 | pull.filter(function (file) { |
710 | return /readme(\.|$)/i.test(file.name) |
711 | }), |
712 | pull.take(1), |
713 | pull.collect(function (err, files) { |
714 | if (err) return cb(null, pull.empty()) |
715 | var file = files[0] |
716 | if (!file) |
717 | return cb(null, pull.once(path.length ? '' : |
718 | '<p>' + req._t('NoReadme') + '</p>')) |
719 | repo.getObjectFromAny(file.id, function (err, obj) { |
720 | if (err) return cb(err) |
721 | cb(null, cat([ |
722 | pull.once('<section class="readme">' + |
723 | '<div class="readme-filename">' + file.name + '</div>'), |
724 | self.web.renderObjectData(obj, file.name, repo, branch, path), |
725 | pull.once('</section>') |
726 | ])) |
727 | }) |
728 | }) |
729 | ) |
730 | }) |
731 | } |
732 | |
733 | /* Repo commit */ |
734 | |
735 | R.serveRepoCommit = function (req, repo, rev, filePath) { |
736 | // TODO: use filePath argument |
737 | var self = this |
738 | return u.readNext(function (cb) { |
739 | repo.getCommitParsed(rev, function (err, commit) { |
740 | if (err) return cb(null, |
741 | self.serveRepoTemplate(req, repo, null, rev, `%{author}/%{repo}@${rev}`, |
742 | pull.once(self.web.renderError(err)))) |
743 | getObjectMsgId(repo, commit.id, function (err, objMsgId) { |
744 | if (err) return cb(null, |
745 | self.serveRepoTemplate(req, repo, null, rev, `%{author}/%{repo}@${rev}`, |
746 | pull.once(self.web.renderError(err)))) |
747 | var commitPath = [repo.id, 'commit', commit.id] |
748 | var treePath = [repo.id, 'tree', commit.id] |
749 | var title = u.escape(commit.title) + ' · ' + |
750 | '%{author}/%{repo}@' + commit.id.substr(0, 8) |
751 | cb(null, self.serveRepoTemplate(req, repo, null, rev, title, cat([ |
752 | pull.once( |
753 | '<h3>' + u.link(commitPath, |
754 | req._t('CommitRev', {rev: rev})) + '</h3>' + |
755 | '<section class="collapse">' + |
756 | '<div class="right-bar">' + |
757 | u.link(treePath, req._t('BrowseFiles')) + |
758 | '</div>' + |
759 | '<h4>' + u.linkify(u.escape(commit.title)) + '</h4>' + |
760 | (commit.body ? u.linkify(u.pre(commit.body)) : '') + |
761 | (commit.separateAuthor ? req._t('AuthoredOn', { |
762 | name: u.escape(commit.author.name), |
763 | date: commit.author.date.toLocaleString(req._locale) |
764 | }) + '<br/>' : '') + |
765 | req._t('CommittedOn', { |
766 | name: u.escape(commit.committer.name), |
767 | date: commit.committer.date.toLocaleString(req._locale) |
768 | }) + '<br/>' + |
769 | commit.parents.map(function (id) { |
770 | return req._t('Parent') + ': ' + |
771 | u.link([repo.id, 'commit', id], id) |
772 | }).join('<br>') + |
773 | '</section>' + |
774 | '<section><h3>' + req._t('FilesChanged') + '</h3>'), |
775 | // TODO: show diff from all parents (merge commits) |
776 | self.renderDiffStat(req, [repo, repo], [commit.parents[0], commit.id], commit.id, objMsgId), |
777 | pull.once('</section>') |
778 | ]))) |
779 | }) |
780 | }) |
781 | }) |
782 | } |
783 | |
784 | /* Repo tag */ |
785 | |
786 | R.serveRepoTag = function (req, repo, rev, path) { |
787 | var self = this |
788 | return u.readNext(function (cb) { |
789 | repo.getTagParsed(rev, function (err, tag) { |
790 | if (err) { |
791 | if (/Expected tag, got commit/.test(err.message)) { |
792 | req._u.pathname = u.encodeLink([repo.id, 'commit', rev].concat(path)) |
793 | return cb(null, self.web.serveRedirect(req, url.format(req._u))) |
794 | } |
795 | return cb(null, self.web.serveError(req, err)) |
796 | } |
797 | |
798 | var title = req._t('TagName', { |
799 | tag: u.escape(tag.tag) |
800 | }) + ' · %{author}/%{repo}' |
801 | var body = (tag.title + '\n\n' + |
802 | tag.body.replace(/-----BEGIN PGP SIGNATURE-----\n[^.]*?\n-----END PGP SIGNATURE-----\s*$/, '')).trim() |
803 | var date = tag.tagger.date |
804 | cb(null, self.serveRepoTemplate(req, repo, 'tags', tag.object, title, |
805 | pull.once( |
806 | '<section class="collapse">' + |
807 | '<h3>' + u.link([repo.id, 'tag', rev], tag.tag) + '</h3>' + |
808 | req._t('TaggedOn', { |
809 | name: u.escape(tag.tagger.name), |
810 | date: date && date.toLocaleString(req._locale) |
811 | }) + '<br/>' + |
812 | u.link([repo.id, tag.type, tag.object]) + |
813 | u.linkify(u.pre(body)) + |
814 | '</section>'))) |
815 | }) |
816 | }) |
817 | } |
818 | |
819 | |
820 | /* Diff stat */ |
821 | |
822 | R.renderDiffStat = function (req, repos, treeIds, commit, updateId) { |
823 | var self = this |
824 | if (treeIds.length == 0) treeIds = [null] |
825 | var id = treeIds[0] |
826 | var lastI = treeIds.length - 1 |
827 | var oldTree = treeIds[0] |
828 | var changedFiles = [] |
829 | var source = GitRepo.diffTrees(repos, treeIds, true) |
830 | |
831 | return cat([ |
832 | h('table', u.sourceMap(source, item => { |
833 | var filename = u.escape(item.filename = item.path.join('/')) |
834 | var oldId = item.id && item.id[0] |
835 | var newId = item.id && item.id[lastI] |
836 | var oldMode = item.mode && item.mode[0] |
837 | var newMode = item.mode && item.mode[lastI] |
838 | var action = |
839 | !oldId && newId ? req._t('action.added') : |
840 | oldId && !newId ? req._t('action.deleted') : |
841 | oldMode != newMode ? req._t('action.changedMode', { |
842 | old: oldMode.toString(8), |
843 | new: newMode.toString(8) |
844 | }) : req._t('changed') |
845 | if (item.id) |
846 | changedFiles.push(item) |
847 | var blobsPath = item.id[1] |
848 | ? [repos[1].id, 'blob', treeIds[1]] |
849 | : [repos[0].id, 'blob', treeIds[0]] |
850 | var rawsPath = item.id[1] |
851 | ? [repos[1].id, 'raw', treeIds[1]] |
852 | : [repos[0].id, 'raw', treeIds[0]] |
853 | item.blobPath = blobsPath.concat(item.path) |
854 | item.rawPath = rawsPath.concat(item.path) |
855 | var fileHref = item.id ? |
856 | '#' + encodeURIComponent(item.path.join('/')) : |
857 | u.encodeLink(item.blobPath) |
858 | |
859 | return h('tr', [ |
860 | h('td', [ |
861 | h('a', {href: fileHref}, filename) |
862 | ]), |
863 | h('td', action) |
864 | ]) |
865 | })), |
866 | pull( |
867 | pull.values(changedFiles), |
868 | paramap(function (item, cb) { |
869 | var extension = u.getExtension(item.filename) |
870 | if (extension in u.imgMimes) { |
871 | var filename = u.escape(item.filename) |
872 | return cb(null, |
873 | '<pre><table class="code">' + |
874 | '<tr><th id="' + u.escape(item.filename) + '">' + |
875 | filename + '</th></tr>' + |
876 | '<tr><td><img src="' + u.encodeLink(item.rawPath) + '"' + |
877 | ' alt="' + filename + '"/></td></tr>' + |
878 | '</table></pre>') |
879 | } |
880 | var done = multicb({ pluck: 1, spread: true }) |
881 | var mode0 = item.mode && item.mode[0] |
882 | var modeI = item.mode && item.mode[lastI] |
883 | var isSubmodule = (modeI == 0160000) |
884 | var repo = repos[1] |
885 | getRepoObjectString(repos[0], item.id[0], mode0, done()) |
886 | getRepoObjectString(repos[1], item.id[lastI], modeI, done()) |
887 | self.getLineCommentThreads(req, repo, updateId, commit, item.filename, done()) |
888 | done(function (err, strOld, strNew, lineCommentThreads) { |
889 | if (err) return cb(err) |
890 | cb(null, htmlLineDiff(req, repo, updateId, commit, item.filename, item.filename, |
891 | strOld, strNew, |
892 | u.encodeLink(item.blobPath), !isSubmodule, lineCommentThreads)) |
893 | }) |
894 | }, 4) |
895 | ) |
896 | ]) |
897 | } |
898 | |
899 | function htmlLineDiff(req, repo, updateId, commit, filename, anchor, oldStr, newStr, blobHref, |
900 | showViewLink, lineCommentThreads) { |
901 | return '<div class="code-wrap"><table class="code">' + |
902 | '<tr><th colspan=3 id="' + u.escape(anchor) + '">' + filename + |
903 | (showViewLink === false ? '' : |
904 | '<span class="right-bar">' + |
905 | '<a href="' + blobHref + '">' + req._t('View') + '</a> ' + |
906 | '</span>') + |
907 | '</th></tr>' + |
908 | (oldStr.length + newStr.length > 200000 |
909 | ? '<tr><td class="diff-info" colspan=3>' + req._t('diff.TooLarge') + '<br>' + |
910 | req._t('diff.OldFileSize', {bytes: oldStr.length}) + '<br>' + |
911 | req._t('diff.NewFileSize', {bytes: newStr.length}) + '</td></tr>' |
912 | : tableDiff(req, repo, updateId, commit, oldStr, newStr, filename, lineCommentThreads)) + |
913 | '</table></div>' |
914 | } |
915 | |
916 | function tableDiff(req, repo, updateId, commit, oldStr, newStr, filename, lineCommentThreads) { |
917 | var query = req._u.query |
918 | var diff = JsDiff.structuredPatch('', '', oldStr, newStr) |
919 | var groups = diff.hunks.map(function (hunk) { |
920 | var oldLine = hunk.oldStart |
921 | var newLine = hunk.newStart |
922 | var header = '<tr class="diff-hunk-header"><td colspan=2></td><td>' + |
923 | '@@ -' + oldLine + ',' + hunk.oldLines + ' ' + |
924 | '+' + newLine + ',' + hunk.newLines + ' @@' + |
925 | '</td></tr>' |
926 | return [header].concat(hunk.lines.map(function (line) { |
927 | var s = line[0] |
928 | if (s == '\\') return |
929 | var html = u.highlight(line, u.getExtension(filename)) |
930 | var trClass = s == '+' ? 'diff-new' : s == '-' ? 'diff-old' : '' |
931 | var lineNums = [s == '+' ? '' : oldLine++, s == '-' ? '' : newLine++] |
932 | var id = [filename].concat(lineNums).join('-') |
933 | var newLineNum = lineNums[lineNums.length-1] |
934 | return '<tr id="' + u.escape(id) + '" class="' + trClass + '">' + |
935 | lineNums.map(function (num, i) { |
936 | var idEnc = encodeURIComponent(id) |
937 | return '<td class="code-linenum">' + |
938 | (num ? '<a href="#' + idEnc + '">' + |
939 | num + '</a>' + |
940 | (updateId && i === lineNums.length-1 && s !== '-' ? |
941 | // TODO: use a more descriptive icon for the comment action |
942 | ' <a href="?comment=' + idEnc + '#' + idEnc + '">…</a>' |
943 | : '') |
944 | : '') + '</td>' |
945 | }).join('') + |
946 | '<td class="code-text">' + html + '</td></tr>' + |
947 | (lineCommentThreads[newLineNum] ? |
948 | '<tr><td colspan=4>' + |
949 | lineCommentThreads[newLineNum] + |
950 | '</td></tr>' |
951 | : commit && query.comment === id ? |
952 | '<tr><td colspan=4>' + |
953 | forms.lineComment(req, repo, updateId, commit, filename, newLineNum) + |
954 | '</td></tr>' |
955 | : '') |
956 | })) |
957 | }) |
958 | return [].concat.apply([], groups).join('') |
959 | } |
960 | |
961 | /* An unknown message linking to a repo */ |
962 | |
963 | R.serveRepoSomething = function (req, repo, id, msg, path) { |
964 | return this.serveRepoTemplate(req, repo, null, null, null, |
965 | pull.once('<section><h3>' + u.link([id]) + '</h3>' + |
966 | u.json(msg) + '</section>')) |
967 | } |
968 | |
969 | /* Repo update */ |
970 | |
971 | function objsArr(objs) { |
972 | return Array.isArray(objs) ? objs : |
973 | Object.keys(objs).map(function (sha1) { |
974 | var obj = Object.create(objs[sha1]) |
975 | obj.sha1 = sha1 |
976 | return obj |
977 | }) |
978 | } |
979 | |
980 | R.serveRepoUpdate = function (req, repo, msg, path) { |
981 | var self = this |
982 | var raw = req._u.query.raw != null |
983 | var title = req._t('Update') + ' · %{author}/%{repo}' |
984 | var c = msg.value.content |
985 | |
986 | if (raw) |
987 | return self.serveRepoTemplate(req, repo, 'activity', null, title, pull.once( |
988 | '<a href="?" class="raw-link header-align">' + |
989 | req._t('Info') + '</a>' + |
990 | '<h3>' + req._t('Update') + '</h3>' + |
991 | '<section class="collapse">' + |
992 | u.json(msg) + '</section>')) |
993 | |
994 | |
995 | // convert packs to old single-object style |
996 | var packs = Array.isArray(c.packs) && Array.isArray(c.indexes) |
997 | && c.packs.map(function (pack, i) { |
998 | var idx = c.indexes[i] |
999 | return pack && pack.link && idx && idx.link && { |
1000 | pack: pack, |
1001 | idx: idx |
1002 | } |
1003 | }).filter(Boolean) |
1004 | |
1005 | var commits = cat([ |
1006 | c.objects ? pull( |
1007 | pull.values(c.objects), |
1008 | pull.filter(function (obj) { return obj.type == 'commit' }), |
1009 | paramap(function (obj, cb) { |
1010 | self.web.getBlob(req, obj.link || obj.key, function (err, readObject) { |
1011 | if (err) return cb(err) |
1012 | GitRepo.getCommitParsed({read: readObject}, cb) |
1013 | }) |
1014 | }, 8) |
1015 | ) : pull.empty(), |
1016 | packs ? pull( |
1017 | pull.values(packs), |
1018 | paramap(function (pack, cb) { |
1019 | var done = multicb({ pluck: 1, spread: true }) |
1020 | self.web.getBlob(req, pack.pack.link, done()) |
1021 | self.web.getBlob(req, pack.idx.link, done()) |
1022 | done(function (err, readPack, readIdx) { |
1023 | if (err) return cb(self.web.renderError(err)) |
1024 | cb(null, gitPack.decodeWithIndex(repo, readPack, readIdx)) |
1025 | }) |
1026 | }, 4), |
1027 | pull.flatten(), |
1028 | pull.asyncMap(function (obj, cb) { |
1029 | if (obj.type == 'commit') |
1030 | GitRepo.getCommitParsed(obj, cb) |
1031 | else |
1032 | pull(obj.read, pull.drain(null, cb)) |
1033 | }), |
1034 | pull.filter() |
1035 | ) : pull.empty() |
1036 | ]) |
1037 | |
1038 | return self.serveRepoTemplate(req, repo, 'activity', null, title, cat([ |
1039 | pull.once('<a href="?raw" class="raw-link header-align">' + |
1040 | req._t('Data') + '</a>' + |
1041 | '<h3>' + req._t('Update') + '</h3>'), |
1042 | pull( |
1043 | pull.once(msg), |
1044 | pull.asyncMap(renderRepoUpdate.bind(self, req, repo, true)) |
1045 | ), |
1046 | (c.objects || c.packs) && |
1047 | pull.once('<h3>' + req._t('Commits') + '</h3>'), |
1048 | pull(commits, pull.map(function (commit) { |
1049 | return renderCommit(req, repo, commit) |
1050 | })) |
1051 | ])) |
1052 | } |
1053 | |
1054 | /* Blob */ |
1055 | |
1056 | R.serveRepoBlob = function (req, repo, rev, path) { |
1057 | var self = this |
1058 | return u.readNext(function (cb) { |
1059 | repo.getFile(rev, path, function (err, object) { |
1060 | if (err) return cb(null, self.web.serveBlobNotFound(req, repo.id, err)) |
1061 | var type = repo.isCommitHash(rev) ? 'Tree' : 'Branch' |
1062 | var pathLinks = path.length === 0 ? '' : |
1063 | ': ' + linkPath([repo.id, 'tree'], [rev].concat(path)) |
1064 | var rawFilePath = [repo.id, 'raw', rev].concat(path) |
1065 | var dirPath = path.slice(0, path.length-1) |
1066 | var filename = path[path.length-1] |
1067 | var extension = u.getExtension(filename) |
1068 | var title = (path.length ? path.join('/') + ' · ' : '') + |
1069 | '%{author}/%{repo}' + |
1070 | (repo.head == 'refs/heads/' + rev ? '' : '@' + rev) |
1071 | cb(null, self.serveRepoTemplate(req, repo, 'code', rev, title, cat([ |
1072 | pull.once('<section><form action="" method="get">' + |
1073 | '<h3>' + req._t(type) + ': ' + rev + ' '), |
1074 | self.revMenu(req, repo, rev), |
1075 | pull.once('</h3></form>'), |
1076 | type == 'Branch' && renderRepoLatest(req, repo, rev), |
1077 | pull.once('</section><section class="collapse">' + |
1078 | '<h3>' + req._t('Files') + pathLinks + '</h3>' + |
1079 | '<div>' + object.length + ' bytes' + |
1080 | '<span class="raw-link">' + |
1081 | u.link(rawFilePath, req._t('Raw')) + '</span>' + |
1082 | '</div></section>' + |
1083 | '<section>'), |
1084 | extension in u.imgMimes |
1085 | ? pull.once('<img src="' + u.encodeLink(rawFilePath) + |
1086 | '" alt="' + u.escape(filename) + '" />') |
1087 | : self.web.renderObjectData(object, filename, repo, rev, dirPath), |
1088 | pull.once('</section>') |
1089 | ]))) |
1090 | }) |
1091 | }) |
1092 | } |
1093 | |
1094 | /* Raw blob */ |
1095 | |
1096 | R.serveRepoRaw = function (req, repo, branch, path) { |
1097 | var self = this |
1098 | return u.readNext(function (cb) { |
1099 | repo.getFile(branch, path, function (err, object) { |
1100 | if (err) return cb(null, |
1101 | self.web.serveBuffer(404, req._t('error.BlobNotFound'))) |
1102 | var extension = u.getExtension(path[path.length-1]) |
1103 | var contentType = u.imgMimes[extension] |
1104 | cb(null, pull(object.read, self.web.serveRaw(object.length, contentType))) |
1105 | }) |
1106 | }) |
1107 | } |
1108 | |
1109 | /* Digs */ |
1110 | |
1111 | R.serveRepoDigs = function serveRepoDigs (req, repo) { |
1112 | var self = this |
1113 | return u.readNext(cb => { |
1114 | var title = req._t('Digs') + ' · %{author}/%{repo}' |
1115 | self.web.getVotes(repo.id, (err, votes) => { |
1116 | cb(null, self.serveRepoTemplate(req, repo, null, null, title, |
1117 | h('section', [ |
1118 | h('h3', req._t('Digs')), |
1119 | h('div', `${req._t('Total')}: ${votes.upvotes}`), |
1120 | h('ul', u.paraSourceMap(Object.keys(votes.upvoters), (feedId, cb) => { |
1121 | self.web.about.getName(feedId, (err, name) => { |
1122 | cb(null, h('li', u.link([feedId], name))) |
1123 | }) |
1124 | })) |
1125 | ]) |
1126 | )) |
1127 | }) |
1128 | }) |
1129 | } |
1130 | |
1131 | /* Forks */ |
1132 | |
1133 | R.getForks = function (repo, includeSelf) { |
1134 | var self = this |
1135 | return pull( |
1136 | cat([ |
1137 | includeSelf && pull.once(repo.id), |
1138 | // get downstream repos |
1139 | pull( |
1140 | self.web.ssb.backlinks ? self.web.ssb.backlinks.read({ |
1141 | query: [ |
1142 | {$filter: { |
1143 | dest: repo.id, |
1144 | value: { |
1145 | content: { |
1146 | type: 'git-repo', |
1147 | upstream: repo.id, |
1148 | } |
1149 | } |
1150 | }}, |
1151 | {$map: 'key'} |
1152 | ] |
1153 | }) : pull( |
1154 | self.web.ssb.links({ |
1155 | dest: repo.id, |
1156 | rel: 'upstream' |
1157 | }), |
1158 | pull.map('key') |
1159 | ) |
1160 | ), |
1161 | // look for other repos that previously had pull requests to this one |
1162 | pull( |
1163 | self.web.ssb.backlinks ? self.web.ssb.backlinks.read({ |
1164 | query: [ |
1165 | {$filter: { |
1166 | dest: repo.id, |
1167 | value: { |
1168 | content: { |
1169 | type: 'pull-request', |
1170 | project: repo.id, |
1171 | } |
1172 | } |
1173 | }} |
1174 | ] |
1175 | }) : pull( |
1176 | self.web.ssb.links({ |
1177 | dest: repo.id, |
1178 | values: true, |
1179 | rel: 'project' |
1180 | }), |
1181 | u.decryptMessages(self.web.ssb), |
1182 | pull.filter(function (msg) { |
1183 | var c = msg && msg.value && msg.value.content |
1184 | return c && c.type == 'pull-request' |
1185 | }) |
1186 | ), |
1187 | pull.map(function (msg) { return msg.value.content.head_repo }) |
1188 | ) |
1189 | ]), |
1190 | pull.unique(), |
1191 | paramap(function (key, cb) { |
1192 | if (key && key[0] === '#') return cb(null, {key: key, value: { |
1193 | content: { |
1194 | type: 'git-repo', |
1195 | } |
1196 | }}) |
1197 | self.web.getMsg(key, cb) |
1198 | }, 4), |
1199 | u.decryptMessages(self.web.ssb), |
1200 | pull.filter(function (msg) { |
1201 | var c = msg && msg.value && msg.value.content |
1202 | return c && c.type == 'git-repo' |
1203 | }), |
1204 | paramap(function (msg, cb) { |
1205 | self.web.getRepoFullName(msg.value.author, msg.key, |
1206 | function (err, repoName, authorName) { |
1207 | if (err) return cb(err) |
1208 | cb(null, { |
1209 | key: msg.key, |
1210 | value: msg.value, |
1211 | repoName: repoName, |
1212 | authorName: authorName |
1213 | }) |
1214 | }) |
1215 | }, 8) |
1216 | ) |
1217 | } |
1218 | |
1219 | R.serveRepoForks = function (req, repo) { |
1220 | var hasForks |
1221 | var title = req._t('Forks') + ' · %{author}/%{repo}' |
1222 | return this.serveRepoTemplate(req, repo, null, null, title, cat([ |
1223 | pull.once('<h3>' + req._t('Forks') + '</h3>'), |
1224 | pull( |
1225 | this.getForks(repo), |
1226 | pull.map(function (msg) { |
1227 | hasForks = true |
1228 | return '<section class="collapse">' + |
1229 | u.link([msg.value.author], msg.authorName) + ' / ' + |
1230 | u.link([msg.key], msg.repoName) + |
1231 | '<span class="right-bar">' + |
1232 | u.timestamp(msg.value.timestamp, req) + |
1233 | '</span></section>' |
1234 | }) |
1235 | ), |
1236 | u.readOnce(function (cb) { |
1237 | cb(null, hasForks ? '' : req._t('NoForks')) |
1238 | }) |
1239 | ])) |
1240 | } |
1241 | |
1242 | R.serveRepoForkPrompt = function (req, repo) { |
1243 | var title = req._t('Fork') + ' · %{author}/%{repo}' |
1244 | return this.serveRepoTemplate(req, repo, null, null, title, pull.once( |
1245 | '<form action="" method="post" onreset="history.back()">' + |
1246 | '<h3>' + req._t('ForkRepoPrompt') + '</h3>' + |
1247 | '<p>' + u.hiddenInputs({ id: repo.id }) + |
1248 | '<button class="btn open" type="submit" name="action" value="fork">' + |
1249 | req._t('Fork') + |
1250 | '</button>' + |
1251 | ' <button class="btn" type="reset">' + |
1252 | req._t('Cancel') + '</button>' + |
1253 | '</p></form>' |
1254 | )) |
1255 | } |
1256 | |
1257 | R.serveIssueOrPullRequest = function (req, repo, issue, path, id) { |
1258 | return issue.msg.value.content.type == 'pull-request' |
1259 | ? this.pulls.serveRepoPullReq(req, repo, issue, path, id) |
1260 | : this.issues.serveRepoIssue(req, repo, issue, path, id) |
1261 | } |
1262 | |
1263 | function getRepoLastMod(repo, cb) { |
1264 | repo.getState(function (err, state) { |
1265 | if (err) return cb(err) |
1266 | var lastMod = new Date(Math.max.apply(Math, state.refs.map(function (ref) { |
1267 | return ref.link.value.timestamp |
1268 | }))) || new Date() |
1269 | cb(null, lastMod) |
1270 | }) |
1271 | } |
1272 | |
1273 | R.serveRepoRefs = function (req, repo) { |
1274 | var self = this |
1275 | return u.readNext(function (cb) { |
1276 | getRepoLastMod(repo, function (err, lastMod) { |
1277 | if (err) return cb(null, self.web.serveError(req, err, 500)) |
1278 | if (u.ifModifiedSince(req, lastMod)) { |
1279 | return cb(null, pull.once([304])) |
1280 | } |
1281 | repo.getState(function (err, state) { |
1282 | if (err) return cb(null, self.web.serveError(req, err, 500)) |
1283 | var buf = state.refs.sort(function (a, b) { |
1284 | return a.name > b.name ? 1 : a.name < b.name ? -1 : 0 |
1285 | }).map(function (ref) { |
1286 | return ref.hash + '\t' + ref.name + '\n' |
1287 | }).join('') |
1288 | cb(null, pull.values([[200, { |
1289 | 'Content-Type': 'text/plain; charset=utf-8', |
1290 | 'Content-Length': Buffer.byteLength(buf), |
1291 | 'Last-Modified': lastMod.toGMTString() |
1292 | }], buf])) |
1293 | }) |
1294 | }) |
1295 | }) |
1296 | } |
1297 | |
1298 | R.serveRepoObject = function (req, repo, sha1) { |
1299 | var self = this |
1300 | if (!/[0-9a-f]{20}/.test(sha1)) return pull.once([401]) |
1301 | return u.readNext(function (cb) { |
1302 | repo.getObjectFromAny(sha1, function (err, obj) { |
1303 | if (err) return cb(null, pull.once([404])) |
1304 | cb(null, cat([ |
1305 | pull.once([200, { |
1306 | 'Content-Type': 'application/x-git-loose-object', |
1307 | 'Cache-Control': 'max-age=31536000' |
1308 | }]), |
1309 | pull( |
1310 | cat([ |
1311 | pull.values([obj.type, ' ', obj.length.toString(10), '\0']), |
1312 | obj.read |
1313 | ]), |
1314 | toPull(zlib.createDeflate()) |
1315 | ) |
1316 | ])) |
1317 | }) |
1318 | }) |
1319 | } |
1320 | |
1321 | R.serveRepoHead = function (req, repo) { |
1322 | var self = this |
1323 | return u.readNext(function (cb) { |
1324 | repo.getHead(function (err, name) { |
1325 | if (err) return cb(null, pull.once([500])) |
1326 | return cb(null, self.web.serveBuffer(200, 'ref: ' + name)) |
1327 | }) |
1328 | }) |
1329 | } |
1330 | |
1331 | R.serveRepoPacksInfo = function (req, repo) { |
1332 | var self = this |
1333 | return u.readNext(function (cb) { |
1334 | getRepoLastMod(repo, function (err, lastMod) { |
1335 | if (err) return cb(null, self.web.serveError(req, err, 500)) |
1336 | if (u.ifModifiedSince(req, lastMod)) { |
1337 | return cb(null, pull.once([304])) |
1338 | } |
1339 | cb(null, cat([ |
1340 | pull.once([200, { |
1341 | 'Content-Type': 'text/plain; charset=utf-8', |
1342 | 'Last-Modified': lastMod.toGMTString() |
1343 | }]), |
1344 | pull( |
1345 | repo.packs(), |
1346 | pull.map(function (pack) { |
1347 | var sha1 = pack.sha1 |
1348 | if (!sha1) { |
1349 | // make up a sha1 and hope git doesn't notice |
1350 | var packId = Buffer.from(pack.packId.substr(1, 44), 'base64') |
1351 | sha1 = packId.slice(0, 20).toString('hex') |
1352 | } |
1353 | return 'P pack-' + sha1 + '.pack\n' |
1354 | }) |
1355 | ) |
1356 | ])) |
1357 | }) |
1358 | }) |
1359 | } |
1360 | |
1361 | R.serveRepoPack = function (req, repo, name) { |
1362 | var m = name.match(/^pack-(.*)\.(pack|idx)$/) |
1363 | if (!m) return pull.once([400]) |
1364 | var hex; |
1365 | try { |
1366 | hex = Buffer.from(m[1], 'hex') |
1367 | } catch(e) { |
1368 | return pull.once([400]) |
1369 | } |
1370 | |
1371 | var self = this |
1372 | return u.readNext(function (cb) { |
1373 | pull( |
1374 | repo.packs(), |
1375 | pull.filter(function (pack) { |
1376 | var sha1 = pack.sha1 |
1377 | ? Buffer.from(pack.sha1, 'hex') |
1378 | : Buffer.from(pack.packId.substr(1, 44), 'base64').slice(0, 20) |
1379 | return sha1.equals(hex) |
1380 | }), |
1381 | pull.take(1), |
1382 | pull.collect(function (err, packs) { |
1383 | if (err) return console.error(err), cb(null, pull.once([500])) |
1384 | if (packs.length < 1) return cb(null, pull.once([404])) |
1385 | var pack = packs[0] |
1386 | |
1387 | if (m[2] === 'pack') { |
1388 | repo.getPackfile(pack.packId, function (err, read) { |
1389 | if (err) return cb(err) |
1390 | cb(null, pull(read, |
1391 | self.web.serveRaw(null, 'application/x-git-packed-objects') |
1392 | )) |
1393 | }) |
1394 | } |
1395 | |
1396 | if (m[2] === 'idx') { |
1397 | repo.getPackIndex(pack.idxId, function (err, read) { |
1398 | if (err) return cb(err) |
1399 | cb(null, pull(read, |
1400 | self.web.serveRaw(null, 'application/x-git-packed-objects-toc') |
1401 | )) |
1402 | }) |
1403 | } |
1404 | }) |
1405 | ) |
1406 | }) |
1407 | } |
1408 |
Built with git-ssb-web