Files: 63bcff079120449a73ea2cceec00484d3378041c / lib / repos / pulls.js
16340 bytesRaw
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 | var ssbRef = require('ssb-ref') |
11 | |
12 | module.exports = function (repoRoutes, web) { |
13 | return new RepoPullReqRoutes(repoRoutes, web) |
14 | } |
15 | |
16 | function RepoPullReqRoutes(repoRoutes, web) { |
17 | this.repo = repoRoutes |
18 | this.web = web |
19 | } |
20 | |
21 | var P = RepoPullReqRoutes.prototype |
22 | |
23 | /* Pull Request */ |
24 | |
25 | P.serveRepoPullReq = function (req, repo, pr, path, postId) { |
26 | var self = this |
27 | var headRepo, authorLink |
28 | var page = path[0] || 'activity' |
29 | var title = u.formatMarkdownTitle(pr.title) + ' · %{author}/%{repo}' |
30 | return self.repo.serveRepoTemplate(req, repo, 'pulls', null, title, cat([ |
31 | pull.once('<div class="pull-request">' + |
32 | '<h3>' + u.link([pr.id], u.formatMarkdownTitle(pr.title)) + '</h3>' + |
33 | '<code>' + pr.id + '</code>'), |
34 | u.readOnce(function (cb) { |
35 | var done = multicb({ pluck: 1, spread: true }) |
36 | var gotHeadRepo = done() |
37 | self.web.about.getName(pr.author, done()) |
38 | var sameRepo = (pr.headRepo == pr.baseRepo) |
39 | self.web.getRepo(pr.headRepo, function (err, headRepo) { |
40 | if (err) return cb(err) |
41 | self.web.getRepoName(headRepo.feed, headRepo.id, done()) |
42 | self.web.about.getName(headRepo.feed, done()) |
43 | gotHeadRepo(null, GitRepo(headRepo)) |
44 | }) |
45 | |
46 | done(function (err, _headRepo, issueAuthorName, |
47 | headRepoName, headRepoAuthorName) { |
48 | if (err) return cb(err) |
49 | headRepo = _headRepo |
50 | authorLink = u.link([pr.author], issueAuthorName) |
51 | var repoLink = u.link([pr.headRepo], headRepoName) |
52 | var headRepoAuthorLink = u.link([headRepo.feed], headRepoAuthorName) |
53 | var headRepoLink = u.link([headRepo.id], headRepoName) |
54 | var headBranchLink = u.link([headRepo.id, 'tree', pr.headBranch]) |
55 | var baseBranchLink = u.link([repo.id, 'tree', pr.baseBranch]) |
56 | cb(null, '<section class="collapse">' + |
57 | '<strong class="issue-status ' + |
58 | (pr.open ? 'open' : 'closed') + '">' + |
59 | req._t(pr.open ? 'issue.state.Open' : 'issue.state.Closed') + |
60 | '</strong> ' + |
61 | req._t('pullRequest.WantToMerge', { |
62 | name: authorLink, |
63 | base: '<code>' + baseBranchLink + '</code>', |
64 | head: (sameRepo ? |
65 | '<code>' + headBranchLink + '</code>' : |
66 | '<code class="bgslash">' + |
67 | headRepoAuthorLink + ' / ' + |
68 | headRepoLink + ' / ' + |
69 | headBranchLink + '</code>') |
70 | }) + '</section>') |
71 | }) |
72 | }), |
73 | u.nav([ |
74 | [[pr.id], req._t('Discussion'), 'activity'], |
75 | [[pr.id, 'commits'], req._t('Commits'), 'commits'], |
76 | [[pr.id, 'files'], req._t('Files'), 'files'] |
77 | ], page), |
78 | u.readNext(function (cb) { |
79 | if (page == 'commits') |
80 | self.renderPullReqCommits(req, pr, repo, headRepo, cb) |
81 | else if (page == 'files') |
82 | self.renderPullReqFiles(req, pr, repo, headRepo, cb) |
83 | else cb(null, |
84 | self.renderPullReqActivity(req, pr, repo, headRepo, authorLink, postId)) |
85 | }) |
86 | ])) |
87 | } |
88 | |
89 | P.renderPullReqCommits = function (req, pr, baseRepo, headRepo, cb) { |
90 | var self = this |
91 | self.web.pullReqs.getRevs(pr.id, function (err, revs) { |
92 | if (err) return cb(err) |
93 | if (!revs.head) return cb(null, pull.once('<section>' + |
94 | req._t('pullRequest.NoCommits') + '</section>')) |
95 | GitRepo.getMergeBase(baseRepo, revs.base, headRepo, revs.head, |
96 | function (err, mergeBase) { |
97 | if (err) return cb(err) |
98 | cb(null, cat([ |
99 | pull.once('<section>'), |
100 | self.renderCommitLog(req, baseRepo, mergeBase, headRepo, revs.head), |
101 | pull.once('</section>') |
102 | ])) |
103 | } |
104 | ) |
105 | }) |
106 | } |
107 | |
108 | P.renderPullReqFiles = function (req, pr, baseRepo, headRepo, cb) { |
109 | var self = this |
110 | self.web.pullReqs.getRevs(pr.id, function (err, revs) { |
111 | if (err) return cb(err) |
112 | if (!revs.head) return cb(null, pull.once('<section>' + |
113 | req._t('pullRequest.NoChanges') + '</section>')) |
114 | GitRepo.getMergeBase(baseRepo, revs.base, headRepo, revs.head, |
115 | function (err, mergeBase) { |
116 | var repoBranch = [] |
117 | if (err) return cb(err) |
118 | cb(null, cat([ |
119 | pull.once('<section>'), |
120 | self.repo.renderDiffStat(req, |
121 | [baseRepo, headRepo], [mergeBase, revs.head], revs.head, repoBranch), |
122 | pull.once('</section>') |
123 | ])) |
124 | } |
125 | ) |
126 | }) |
127 | } |
128 | |
129 | P.renderPullReqActivity = function (req, pr, repo, headRepo, authorLink, postId) { |
130 | var self = this |
131 | var msgTimeLink = u.link([pr.id], |
132 | new Date(pr.created_at).toLocaleString(req._locale)) |
133 | var newestMsg = {key: pr.id, value: {timestamp: pr.created_at}} |
134 | return cat([ |
135 | u.readOnce(function (cb) { |
136 | cb(null, |
137 | '<section class="collapse">' + |
138 | authorLink + ' · ' + msgTimeLink + |
139 | markdown(pr.text, repo) + '</section>') |
140 | }), |
141 | // render posts, edits, and updates |
142 | pull( |
143 | many([ |
144 | pull( |
145 | self.web.ssb.links({ |
146 | dest: pr.id, |
147 | values: true |
148 | }), |
149 | u.decryptMessages(self.web.ssb) |
150 | ), |
151 | pull( |
152 | self.web.ssb.links({ |
153 | dest: headRepo.id, |
154 | rel: 'repo', |
155 | values: true, |
156 | reverse: true |
157 | }), |
158 | u.decryptMessages(self.web.ssb), |
159 | pull.take(function (link) { |
160 | return link.value.timestamp > pr.created_at |
161 | }), |
162 | pull.filter(function (link) { |
163 | return link.value.content.type == 'git-update' |
164 | && ('refs/heads/' + pr.headBranch) in (link.value.content.refs || {}) |
165 | }) |
166 | ) |
167 | ]), |
168 | pull.unique('key'), |
169 | u.readableMessages(), |
170 | self.web.addAuthorName(), |
171 | pull.through(function (msg) { |
172 | if (msg.value |
173 | && msg.value.timestamp > newestMsg.value.timestamp |
174 | && msg.value.content.root === pr.id) |
175 | newestMsg = msg |
176 | }), |
177 | u.sortMsgs(), |
178 | pull.map(function (item) { |
179 | if (item.value.content.type == 'git-update') |
180 | return self.renderBranchUpdate(req, pr, item) |
181 | return self.repo.issues.renderIssueActivityMsg(req, repo, pr, |
182 | req._t('pull request'), postId, item) |
183 | }) |
184 | ), |
185 | !self.web.isPublic && pr.open && pull.once(` |
186 | <section class="merge-instructions"> |
187 | <input type="checkbox" class="toggle" id="merge-instructions"/> |
188 | <h4><label for="merge-instructions" class="toggle-link"><a> |
189 | ${req._t('mergeInstructions.MergeViaCmdLine')} |
190 | </a></label></h4> |
191 | <div class="contents"> |
192 | <p>${req._t('mergeInstructions.CheckOut')}</p> |
193 | <pre> |
194 | git fetch ssb://${u.escape(pr.headRepo)} ${u.escape(pr.headBranch)} |
195 | git checkout -b ${u.escape(pr.headBranch)} FETCH_HEAD</pre> |
196 | <p>${req._t('mergeInstructions.MergeAndPush')}</p> |
197 | <pre> |
198 | git checkout ${u.escape(pr.baseBranch)} |
199 | git merge ${u.escape(pr.headBranch)} |
200 | git push ssb ${u.escape(pr.baseBranch)}</pre> |
201 | </div></section>`), |
202 | !self.web.isPublic && u.readOnce(function (cb) { |
203 | cb(null, forms.issueComment(req, pr, repo, newestMsg.key, |
204 | req._t('pull request'))) |
205 | }) |
206 | ]) |
207 | } |
208 | |
209 | P.renderBranchUpdate = function (req, pr, msg) { |
210 | var authorLink = u.link([msg.value.author], msg.authorName) |
211 | var msgLink = u.link([msg.key], |
212 | new Date(msg.value.timestamp).toLocaleString(req._locale)) |
213 | var rev = msg.value.content.refs['refs/heads/' + pr.headBranch] |
214 | if (!rev) |
215 | return '<section class="collapse">' + |
216 | req._t('NameDeletedBranch', { |
217 | name: authorLink, |
218 | branch: '<code>' + pr.headBranch + '</code>' |
219 | }) + ' · ' + msgLink + |
220 | '</section>' |
221 | |
222 | var revLink = u.link([pr.headRepo, 'commit', rev], rev.substr(0, 8)) |
223 | return '<section class="collapse">' + |
224 | req._t('NameUpdatedBranch', { |
225 | name: authorLink, |
226 | rev: '<code>' + revLink + '</code>' |
227 | }) + ' · ' + msgLink + |
228 | '</section>' |
229 | } |
230 | |
231 | /* Compare changes */ |
232 | |
233 | P.branchMenu = function (repo, name, currentName) { |
234 | return cat([ |
235 | pull.once('<select name="' + name + '">'), |
236 | pull( |
237 | repo.refs(), |
238 | pull.map(function (ref) { |
239 | var m = ref.name.match(/^refs\/([^\/]*)\/(.*)$/) || [,, ref.name] |
240 | return m[1] == 'heads' && m[2] |
241 | }), |
242 | pull.filter(Boolean), |
243 | u.pullSort(), |
244 | pull.map(this.repo.formatRevOptions(currentName)) |
245 | ), |
246 | pull.once('</select>') |
247 | ]) |
248 | } |
249 | |
250 | P.serveRepoCompare = function (req, repo) { |
251 | var self = this |
252 | var query = req._u.query |
253 | var base |
254 | var count = 0 |
255 | var title = req._t('CompareChanges') + ' · %{author}/%{repo}' |
256 | |
257 | return self.repo.serveRepoTemplate(req, repo, 'pulls', null, title, cat([ |
258 | pull.once('<h3>' + req._t('CompareChanges') + '</h3>' + |
259 | '<form action="' + u.encodeLink(repo.id) + '/comparing" method="get">' + |
260 | '<section>'), |
261 | pull.once(req._t('BaseBranch') + ': '), |
262 | u.readNext(function (cb) { |
263 | if (query.base) gotBase(null, query.base) |
264 | else repo.getSymRef('HEAD', true, gotBase) |
265 | function gotBase(err, ref) { |
266 | if (err) return cb(err) |
267 | cb(null, self.branchMenu(repo, 'base', base = ref || 'HEAD')) |
268 | } |
269 | }), |
270 | pull.once('<br/>' + req._t('ComparisonRepoBranch') + ':'), |
271 | pull( |
272 | self.repo.getForks(repo, true), |
273 | pull.asyncMap(function (msg, cb) { |
274 | self.web.getRepo(msg.key, function (err, repo) { |
275 | if (err) return cb(err) |
276 | cb(null, { |
277 | msg: msg, |
278 | repo: repo |
279 | }) |
280 | }) |
281 | }), |
282 | pull.map(renderFork), |
283 | pull.flatten() |
284 | ), |
285 | pull.once('<div class="bgslash">' + |
286 | '<input type="radio" name="head" value="other" id="other-radio"> ' + |
287 | '<label for="other-radio">other</label>: ' + |
288 | '<input name="other_repo" placeholder="repo id"> / ' + |
289 | '<input name="other_branch" placeholder="branch"></div>'), |
290 | pull.once('</section>'), |
291 | u.readOnce(function (cb) { |
292 | cb(null, |
293 | '<button type="submit" class="btn">' + |
294 | req._t('Compare') + '</button>') |
295 | }), |
296 | pull.once('</form>') |
297 | ])) |
298 | |
299 | function renderFork(fork) { |
300 | return pull( |
301 | fork.repo.refs({inherited: false}), |
302 | pull.map(function (ref) { |
303 | var m = /^refs\/([^\/]*)\/(.*)$/.exec(ref.name) || [,ref.name] |
304 | return { |
305 | type: m[1], |
306 | name: m[2], |
307 | value: ref.value |
308 | } |
309 | }), |
310 | pull.filter(function (ref) { |
311 | return ref.type == 'heads' |
312 | && !(ref.name == base && fork.msg.key == repo.id) |
313 | }), |
314 | pull.map(function (ref) { |
315 | var branchLink = u.link([fork.msg.key, 'tree', ref.name], ref.name) |
316 | var authorLink = u.link([fork.msg.value.author], fork.msg.authorName) |
317 | var repoLink = u.link([fork.msg.key], fork.msg.repoName) |
318 | var value = fork.msg.key + ':' + ref.name |
319 | return '<div class="bgslash">' + |
320 | '<input type="radio" name="head"' + |
321 | ' value="' + u.escape(value) + '"' + |
322 | (query.head == value ? ' checked="checked"' : '') + '> ' + |
323 | authorLink + ' / ' + repoLink + ' / ' + branchLink + '</div>' |
324 | }) |
325 | ) |
326 | } |
327 | } |
328 | |
329 | P.serveRepoComparing = function (req, repo) { |
330 | var self = this |
331 | var query = req._u.query |
332 | var baseBranch = query.base |
333 | var headRepoId, headBranch |
334 | |
335 | if (query.head === 'other') { |
336 | headRepoId = String(query.other_repo).replace(/^ssb:\/*/, '') |
337 | headBranch = String(query.other_branch) |
338 | } else if (query.head) { |
339 | var s = String(query.head).split(':') |
340 | headRepoId = s[0] |
341 | headBranch = s[1] |
342 | } |
343 | |
344 | if (!baseBranch) |
345 | return self.web.serveRedirect(req, u.encodeLink([repo.id, 'compare'])) |
346 | if (!ssbRef.isMsgId(headRepoId)) |
347 | return self.web.serveError(req, new Error('bad repo id'), 400) |
348 | |
349 | var baseLink = u.link([repo.id, 'tree', baseBranch]) |
350 | var headBranchLink = u.link([headRepoId, 'tree', headBranch]) |
351 | var backHref = u.encodeLink([repo.id, 'compare']) + req._u.search |
352 | var title = req._t(query.expand ? 'OpenPullRequest': 'ComparingChanges') |
353 | var pageTitle = title + ' · %{author}/%{repo}' |
354 | |
355 | return self.repo.serveRepoTemplate(req, repo, 'pulls', null, pageTitle, cat([ |
356 | pull.once('<h3>' + title + '</h3>'), |
357 | u.readNext(function (cb) { |
358 | self.web.getRepo(headRepoId, function (err, headRepo) { |
359 | if (err) return cb(err) |
360 | self.web.getRepoFullName(headRepo.feed, headRepo.id, |
361 | function (err, repoName, authorName) { |
362 | if (err) return cb(err) |
363 | cb(null, renderRepoInfo(GitRepo(headRepo), repoName, authorName)) |
364 | } |
365 | ) |
366 | }) |
367 | }) |
368 | ])) |
369 | |
370 | function renderRepoInfo(headRepo, headRepoName, headRepoAuthorName) { |
371 | var authorLink = u.link([headRepo.feed], headRepoAuthorName) |
372 | var repoLink = u.link([headRepoId], headRepoName) |
373 | return cat([ |
374 | pull.once('<section>' + |
375 | req._t('Base') + ': ' + baseLink + '<br/>' + |
376 | req._t('Head') + ': ' + |
377 | '<span class="bgslash">' + authorLink + ' / ' + repoLink + |
378 | ' / ' + headBranchLink + '</span>' + |
379 | '</section>' + |
380 | (query.expand ? '<section><form method="post" action="">' + |
381 | u.hiddenInputs({ |
382 | action: 'new-pull', |
383 | branch: baseBranch, |
384 | head_repo: headRepoId, |
385 | head_branch: headBranch |
386 | }) + |
387 | forms.post(req, repo, null, 8) + |
388 | '<button type="submit" class="btn open">' + |
389 | req._t('Create') + '</button>' + |
390 | '</form></section>' |
391 | : self.web.isPublic ? '' |
392 | : '<section><form method="get" action="">' + |
393 | u.hiddenInputs({ |
394 | base: baseBranch, |
395 | head: query.head, |
396 | other_repo: query.other_repo, |
397 | other_branch: query.other_branch, |
398 | }) + |
399 | '<button class="btn open" type="submit" name="expand" value="1">' + |
400 | '<i>⎇</i> ' + req._t('CreatePullRequest') + '</button> ' + |
401 | '<a href="' + backHref + '">' + req._t('Back') + '</a>' + |
402 | '</form></section>') + |
403 | '<div id="commits"></div>' + |
404 | '<div class="tab-links">' + |
405 | '<a href="#" id="files-link">' + req._t('FilesChanged') + '</a> ' + |
406 | '<a href="#commits" id="commits-link">' + |
407 | req._t('Commits') + '</a>' + |
408 | '</div>'), |
409 | u.readNext(function (cb) { |
410 | GitRepo.getMergeBase(repo, baseBranch, headRepo, headBranch, |
411 | function (err, concestor) { |
412 | if (err) return cb(err) |
413 | cb(null, cat([ |
414 | pull.once('<section id="files-tab">'), |
415 | self.repo.renderDiffStat(req, [repo, headRepo], |
416 | [concestor, headBranch]), |
417 | pull.once('</section>' + |
418 | '<section id="commits-tab">'), |
419 | self.renderCommitLog(req, repo, concestor, headRepo, headBranch), |
420 | pull.once('</section>') |
421 | ])) |
422 | } |
423 | ) |
424 | }) |
425 | ]) |
426 | } |
427 | } |
428 | |
429 | P.renderCommitLog = function (req, baseRepo, baseBranch, headRepo, headBranch) { |
430 | return cat([ |
431 | pull.once('<table class="compare-commits w-100">'), |
432 | u.readNext(function (cb) { |
433 | baseRepo.resolveRef(baseBranch, function (err, baseBranchRev) { |
434 | if (err) return cb(err) |
435 | var currentDay |
436 | return cb(null, pull( |
437 | headRepo.readLog(headBranch), |
438 | pull.take(function (rev) { return rev != baseBranchRev }), |
439 | u.pullReverse(), |
440 | paramap(headRepo.getCommitParsed.bind(headRepo), 8), |
441 | pull.map(function (commit) { |
442 | var commitPath = [headRepo.id, 'commit', commit.id] |
443 | var commitIdShort = '<tt>' + commit.id.substr(0, 8) + '</tt>' |
444 | var day = Math.floor(commit.author.date / 86400000) |
445 | var dateRow = day == currentDay ? '' : |
446 | '<tr><th colspan=3 class="date-info">' + |
447 | commit.author.date.toLocaleDateString(req._locale) + |
448 | '</th><tr>' |
449 | currentDay = day |
450 | return dateRow + '<tr>' + |
451 | '<td>' + u.escape(commit.author.name) + '</td>' + |
452 | '<td class="commit-title">' + |
453 | u.link(commitPath, commit.title) + '</td>' + |
454 | '<td>' + u.link(commitPath, commitIdShort, true) + '</td>' + |
455 | '</tr>' |
456 | }) |
457 | )) |
458 | }) |
459 | }), |
460 | pull.once('</table>') |
461 | ]) |
462 | } |
463 |
Built with git-ssb-web