git ssb

30+

cel / git-ssb-web



Tree: 8a06021c8e2887b1f6bcb31c9fe4a835c7d2f7b5

Files: 8a06021c8e2887b1f6bcb31c9fe4a835c7d2f7b5 / lib / repos / pulls.js

15122 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')
10
11module.exports = function (repoRoutes, web) {
12 return new RepoPullReqRoutes(repoRoutes, web)
13}
14
15function RepoPullReqRoutes(repoRoutes, web) {
16 this.repo = repoRoutes
17 this.web = web
18}
19
20var P = RepoPullReqRoutes.prototype
21
22/* Pull Request */
23
24P.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
91P.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
103P.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
116P.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 + ' &middot; ' + 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
196P.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 }) + ' &middot; ' + 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 }) + ' &middot; ' + msgLink +
215 '</section>'
216}
217
218/* Compare changes */
219
220P.branchMenu = function (repo, name, currentName) {
221 return cat([
222 pull.once('<select name="' + name + '">'),
223 pull(
224 repo.refs(),
225 pull.map(function (ref) {
226 var m = ref.name.match(/^refs\/([^\/]*)\/(.*)$/) || [,, ref.name]
227 return m[1] == 'heads' && m[2]
228 }),
229 pull.filter(Boolean),
230 u.pullSort(),
231 pull.map(this.repo.formatRevOptions(currentName))
232 ),
233 pull.once('</select>')
234 ])
235}
236
237P.serveRepoCompare = function (req, repo) {
238 var self = this
239 var query = req._u.query
240 var base
241 var count = 0
242 var title = req._t('CompareChanges') + ' · %{author}/%{repo}'
243
244 return self.repo.renderRepoPage(req, repo, 'pulls', null, title, cat([
245 pull.once('<h3>' + req._t('CompareChanges') + '</h3>' +
246 '<form action="' + u.encodeLink(repo.id) + '/comparing" method="get">' +
247 '<section>'),
248 pull.once(req._t('BaseBranch') + ': '),
249 u.readNext(function (cb) {
250 if (query.base) gotBase(null, query.base)
251 else repo.getSymRef('HEAD', true, gotBase)
252 function gotBase(err, ref) {
253 if (err) return cb(err)
254 cb(null, self.branchMenu(repo, 'base', base = ref || 'HEAD'))
255 }
256 }),
257 pull.once('<br/>' + req._t('ComparisonRepoBranch') + ':'),
258 pull(
259 self.repo.getForks(repo, true),
260 pull.asyncMap(function (msg, cb) {
261 self.web.getRepo(msg.key, function (err, repo) {
262 if (err) return cb(err)
263 cb(null, {
264 msg: msg,
265 repo: repo
266 })
267 })
268 }),
269 pull.map(renderFork),
270 pull.flatten()
271 ),
272 pull.once('</section>'),
273 u.readOnce(function (cb) {
274 cb(null, count == 0 ? req._t('NoBranches') :
275 '<button type="submit" class="btn">' +
276 req._t('Compare') + '</button>')
277 }),
278 pull.once('</form>')
279 ]))
280
281 function renderFork(fork) {
282 return pull(
283 fork.repo.refs({inherited: false}),
284 pull.map(function (ref) {
285 var m = /^refs\/([^\/]*)\/(.*)$/.exec(ref.name) || [,ref.name]
286 return {
287 type: m[1],
288 name: m[2],
289 value: ref.value
290 }
291 }),
292 pull.filter(function (ref) {
293 return ref.type == 'heads'
294 && !(ref.name == base && fork.msg.key == repo.id)
295 }),
296 pull.map(function (ref) {
297 var branchLink = u.link([fork.msg.key, 'tree', ref.name], ref.name)
298 var authorLink = u.link([fork.msg.value.author], fork.msg.authorName)
299 var repoLink = u.link([fork.msg.key], fork.msg.repoName)
300 var value = fork.msg.key + ':' + ref.name
301 count++
302 return '<div class="bgslash">' +
303 '<input type="radio" name="head"' +
304 ' value="' + u.escape(value) + '"' +
305 (query.head == value ? ' checked="checked"' : '') + '> ' +
306 authorLink + ' / ' + repoLink + ' / ' + branchLink + '</div>'
307 })
308 )
309 }
310}
311
312P.serveRepoComparing = function (req, repo) {
313 var self = this
314 var query = req._u.query
315 var baseBranch = query.base
316 var s = (query.head || '').split(':')
317
318 if (!s || !baseBranch)
319 return self.web.serveRedirect(req, u.encodeLink([repo.id, 'compare']))
320
321 var headRepoId = s[0]
322 var headBranch = s[1]
323 var baseLink = u.link([repo.id, 'tree', baseBranch])
324 var headBranchLink = u.link([headRepoId, 'tree', headBranch])
325 var backHref = u.encodeLink([repo.id, 'compare']) + req._u.search
326 var title = req._t(query.expand ? 'OpenPullRequest': 'ComparingChanges')
327 var pageTitle = title + ' · %{author}/%{repo}'
328
329 return self.repo.renderRepoPage(req, repo, 'pulls', null, pageTitle, cat([
330 pull.once('<h3>' + title + '</h3>'),
331 u.readNext(function (cb) {
332 self.web.getRepo(headRepoId, function (err, headRepo) {
333 if (err) return cb(err)
334 self.web.getRepoFullName(headRepo.feed, headRepo.id,
335 function (err, repoName, authorName) {
336 if (err) return cb(err)
337 cb(null, renderRepoInfo(GitRepo(headRepo), repoName, authorName))
338 }
339 )
340 })
341 })
342 ]))
343
344 function renderRepoInfo(headRepo, headRepoName, headRepoAuthorName) {
345 var authorLink = u.link([headRepo.feed], headRepoAuthorName)
346 var repoLink = u.link([headRepoId], headRepoName)
347 return cat([
348 pull.once('<section>' +
349 req._t('Base') + ': ' + baseLink + '<br/>' +
350 req._t('Head') + ': ' +
351 '<span class="bgslash">' + authorLink + ' / ' + repoLink +
352 ' / ' + headBranchLink + '</span>' +
353 '</section>' +
354 (query.expand ? '<section><form method="post" action="">' +
355 u.hiddenInputs({
356 action: 'new-pull',
357 branch: baseBranch,
358 head_repo: headRepoId,
359 head_branch: headBranch
360 }) +
361 '<input class="wide-input" name="title"' +
362 ' placeholder="' + req._t('Title') + '" size="77"/>' +
363 forms.post(req, repo, req._t('Description'), 8) +
364 '<button type="submit" class="btn open">' +
365 req._t('Create') + '</button>' +
366 '</form></section>'
367 : '<section><form method="get" action="">' +
368 u.hiddenInputs({
369 base: baseBranch,
370 head: query.head
371 }) +
372 '<button class="btn open" type="submit" name="expand" value="1">' +
373 '<i>⎇</i> ' + req._t('CreatePullRequest') + '</button> ' +
374 '<a href="' + backHref + '">' + req._t('Back') + '</a>' +
375 '</form></section>') +
376 '<div id="commits"></div>' +
377 '<div class="tab-links">' +
378 '<a href="#" id="files-link">' + req._t('FilesChanged') + '</a> ' +
379 '<a href="#commits" id="commits-link">' +
380 req._t('Commits') + '</a>' +
381 '</div>' +
382 '<section id="files-tab">'),
383 self.repo.renderDiffStat(req, [repo, headRepo],
384 [baseBranch, headBranch]),
385 pull.once('</section>' +
386 '<section id="commits-tab">'),
387 self.renderCommitLog(req, repo, baseBranch, headRepo, headBranch),
388 pull.once('</section>')
389 ])
390 }
391}
392
393P.renderCommitLog = function (req, baseRepo, baseBranch, headRepo, headBranch) {
394 return cat([
395 pull.once('<table class="compare-commits">'),
396 u.readNext(function (cb) {
397 baseRepo.resolveRef(baseBranch, function (err, baseBranchRev) {
398 if (err) return cb(err)
399 var currentDay
400 return cb(null, pull(
401 headRepo.readLog(headBranch),
402 pull.take(function (rev) { return rev != baseBranchRev }),
403 u.pullReverse(),
404 paramap(headRepo.getCommitParsed.bind(headRepo), 8),
405 pull.map(function (commit) {
406 var commitPath = [headRepo.id, 'commit', commit.id]
407 var commitIdShort = '<tt>' + commit.id.substr(0, 8) + '</tt>'
408 var day = Math.floor(commit.author.date / 86400000)
409 var dateRow = day == currentDay ? '' :
410 '<tr><th colspan=3 class="date-info">' +
411 commit.author.date.toLocaleDateString(req._locale) +
412 '</th><tr>'
413 currentDay = day
414 return dateRow + '<tr>' +
415 '<td>' + u.escape(commit.author.name) + '</td>' +
416 '<td>' + u.link(commitPath, commit.title) + '</td>' +
417 '<td>' + u.link(commitPath, commitIdShort, true) + '</td>' +
418 '</tr>'
419 })
420 ))
421 })
422 }),
423 pull.once('</table>')
424 ])
425}
426

Built with git-ssb-web