git ssb

30+

cel / git-ssb-web



Tree: d19c73f6a5c9563914be6761839a3ba08ba10394

Files: d19c73f6a5c9563914be6761839a3ba08ba10394 / lib / repos / pulls.js

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

Built with git-ssb-web