git ssb

30+

cel / git-ssb-web



Tree: 63bcff079120449a73ea2cceec00484d3378041c

Files: 63bcff079120449a73ea2cceec00484d3378041c / lib / repos / pulls.js

16340 bytesRaw
1var pull = require('pull-stream')
2var paramap = require('pull-paramap')
3var cat = require('pull-cat')
4var many = require('pull-many')
5var multicb = require('multicb')
6var GitRepo = require('pull-git-repo')
7var u = require('../../lib/util')
8var markdown = require('../../lib/markdown')
9var forms = require('../../lib/forms')
10var ssbRef = require('ssb-ref')
11
12module.exports = function (repoRoutes, web) {
13 return new RepoPullReqRoutes(repoRoutes, web)
14}
15
16function RepoPullReqRoutes(repoRoutes, web) {
17 this.repo = repoRoutes
18 this.web = web
19}
20
21var P = RepoPullReqRoutes.prototype
22
23/* Pull Request */
24
25P.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
89P.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
108P.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
129P.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 + ' &middot; ' + 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>
194git fetch ssb://${u.escape(pr.headRepo)} ${u.escape(pr.headBranch)}
195git checkout -b ${u.escape(pr.headBranch)} FETCH_HEAD</pre>
196 <p>${req._t('mergeInstructions.MergeAndPush')}</p>
197 <pre>
198git checkout ${u.escape(pr.baseBranch)}
199git merge ${u.escape(pr.headBranch)}
200git 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
209P.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 }) + ' &middot; ' + 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 }) + ' &middot; ' + msgLink +
228 '</section>'
229}
230
231/* Compare changes */
232
233P.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
250P.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
329P.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
429P.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