git ssb

30+

cel / git-ssb-web



Tree: b0bc02aa8cd11a7db1f9b1758ed7762c117cad1e

Files: b0bc02aa8cd11a7db1f9b1758ed7762c117cad1e / lib / repos / pulls.js

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

Built with git-ssb-web