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