##// END OF EJS Templates
pullrequests: fix changesets ordering being reversed when creating new pull requests...
Mads Kiilerich -
r3720:9855b31d beta
parent child Browse files
Show More
@@ -1,521 +1,521 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.pullrequests
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 pull requests controller for rhodecode for initializing pull requests
7 7
8 8 :created_on: May 7, 2012
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25 import logging
26 26 import traceback
27 27 import formencode
28 28
29 29 from webob.exc import HTTPNotFound, HTTPForbidden
30 30 from collections import defaultdict
31 31 from itertools import groupby
32 32
33 33 from pylons import request, response, session, tmpl_context as c, url
34 34 from pylons.controllers.util import abort, redirect
35 35 from pylons.i18n.translation import _
36 36
37 37 from rhodecode.lib.compat import json
38 38 from rhodecode.lib.base import BaseRepoController, render
39 39 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator,\
40 40 NotAnonymous
41 41 from rhodecode.lib.helpers import Page
42 42 from rhodecode.lib import helpers as h
43 43 from rhodecode.lib import diffs
44 44 from rhodecode.lib.utils import action_logger, jsonify
45 45 from rhodecode.lib.vcs.utils import safe_str
46 46 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError
47 47 from rhodecode.lib.vcs.backends.base import EmptyChangeset
48 48 from rhodecode.lib.diffs import LimitedDiffContainer
49 49 from rhodecode.model.db import User, PullRequest, ChangesetStatus,\
50 50 ChangesetComment
51 51 from rhodecode.model.pull_request import PullRequestModel
52 52 from rhodecode.model.meta import Session
53 53 from rhodecode.model.repo import RepoModel
54 54 from rhodecode.model.comment import ChangesetCommentsModel
55 55 from rhodecode.model.changeset_status import ChangesetStatusModel
56 56 from rhodecode.model.forms import PullRequestForm
57 57 from mercurial import scmutil
58 58 from rhodecode.lib.utils2 import safe_int
59 59
60 60 log = logging.getLogger(__name__)
61 61
62 62
63 63 class PullrequestsController(BaseRepoController):
64 64
65 65 @LoginRequired()
66 66 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
67 67 'repository.admin')
68 68 def __before__(self):
69 69 super(PullrequestsController, self).__before__()
70 70 repo_model = RepoModel()
71 71 c.users_array = repo_model.get_users_js()
72 72 c.users_groups_array = repo_model.get_users_groups_js()
73 73
74 74 def _get_repo_refs(self, repo, rev=None, branch_rev=None):
75 75 """return a structure with repo's interesting changesets, suitable for
76 76 the selectors in pullrequest.html"""
77 77 # list named branches that has been merged to this named branch - it should probably merge back
78 78 peers = []
79 79
80 80 if rev:
81 81 rev = safe_str(rev)
82 82
83 83 if branch_rev:
84 84 branch_rev = safe_str(branch_rev)
85 85 # not restricting to merge() would also get branch point and be better
86 86 # (especially because it would get the branch point) ... but is currently too expensive
87 87 revs = ["sort(parents(branch(id('%s')) and merge()) - branch(id('%s')))" %
88 88 (branch_rev, branch_rev)]
89 89 otherbranches = {}
90 90 for i in scmutil.revrange(repo._repo, revs):
91 91 cs = repo.get_changeset(i)
92 92 otherbranches[cs.branch] = cs.raw_id
93 93 for branch, node in otherbranches.iteritems():
94 94 selected = 'branch:%s:%s' % (branch, node)
95 95 peers.append((selected, branch))
96 96
97 97 selected = None
98 98 branches = []
99 99 for branch, branchrev in repo.branches.iteritems():
100 100 n = 'branch:%s:%s' % (branch, branchrev)
101 101 branches.append((n, branch))
102 102 if rev == branchrev:
103 103 selected = n
104 104 bookmarks = []
105 105 for bookmark, bookmarkrev in repo.bookmarks.iteritems():
106 106 n = 'book:%s:%s' % (bookmark, bookmarkrev)
107 107 bookmarks.append((n, bookmark))
108 108 if rev == bookmarkrev:
109 109 selected = n
110 110 tags = []
111 111 for tag, tagrev in repo.tags.iteritems():
112 112 n = 'tag:%s:%s' % (tag, tagrev)
113 113 tags.append((n, tag))
114 114 if rev == tagrev and tag != 'tip': # tip is not a real tag - and its branch is better
115 115 selected = n
116 116
117 117 # prio 1: rev was selected as existing entry above
118 118
119 119 # prio 2: create special entry for rev; rev _must_ be used
120 120 specials = []
121 121 if rev and selected is None:
122 122 selected = 'rev:%s:%s' % (rev, rev)
123 123 specials = [(selected, '%s: %s' % (_("Changeset"), rev[:12]))]
124 124
125 125 # prio 3: most recent peer branch
126 126 if peers and not selected:
127 127 selected = peers[0][0][0]
128 128
129 129 # prio 4: tip revision
130 130 if not selected:
131 131 selected = 'tag:tip:%s' % repo.tags['tip']
132 132
133 133 groups = [(specials, _("Special")),
134 134 (peers, _("Peer branches")),
135 135 (bookmarks, _("Bookmarks")),
136 136 (branches, _("Branches")),
137 137 (tags, _("Tags")),
138 138 ]
139 139 return [g for g in groups if g[0]], selected
140 140
141 141 def _get_is_allowed_change_status(self, pull_request):
142 142 owner = self.rhodecode_user.user_id == pull_request.user_id
143 143 reviewer = self.rhodecode_user.user_id in [x.user_id for x in
144 144 pull_request.reviewers]
145 145 return (self.rhodecode_user.admin or owner or reviewer)
146 146
147 147 def show_all(self, repo_name):
148 148 c.pull_requests = PullRequestModel().get_all(repo_name)
149 149 c.repo_name = repo_name
150 150 p = safe_int(request.params.get('page', 1), 1)
151 151
152 152 c.pullrequests_pager = Page(c.pull_requests, page=p, items_per_page=10)
153 153
154 154 c.pullrequest_data = render('/pullrequests/pullrequest_data.html')
155 155
156 156 if request.environ.get('HTTP_X_PARTIAL_XHR'):
157 157 return c.pullrequest_data
158 158
159 159 return render('/pullrequests/pullrequest_show_all.html')
160 160
161 161 @NotAnonymous()
162 162 def index(self):
163 163 org_repo = c.rhodecode_db_repo
164 164
165 165 if org_repo.scm_instance.alias != 'hg':
166 166 log.error('Review not available for GIT REPOS')
167 167 raise HTTPNotFound
168 168
169 169 try:
170 170 org_repo.scm_instance.get_changeset()
171 171 except EmptyRepositoryError, e:
172 172 h.flash(h.literal(_('There are no changesets yet')),
173 173 category='warning')
174 174 redirect(url('summary_home', repo_name=org_repo.repo_name))
175 175
176 176 org_rev = request.GET.get('rev_end')
177 177 # rev_start is not directly useful - its parent could however be used
178 178 # as default for other and thus give a simple compare view
179 179 #other_rev = request.POST.get('rev_start')
180 180
181 181 c.org_repos = []
182 182 c.org_repos.append((org_repo.repo_name, org_repo.repo_name))
183 183 c.default_org_repo = org_repo.repo_name
184 184 c.org_refs, c.default_org_ref = self._get_repo_refs(org_repo.scm_instance, org_rev)
185 185
186 186 c.other_repos = []
187 187 other_repos_info = {}
188 188
189 189 def add_other_repo(repo, branch_rev=None):
190 190 if repo.repo_name in other_repos_info: # shouldn't happen
191 191 return
192 192 c.other_repos.append((repo.repo_name, repo.repo_name))
193 193 other_refs, selected_other_ref = self._get_repo_refs(repo.scm_instance, branch_rev=branch_rev)
194 194 other_repos_info[repo.repo_name] = {
195 195 'user': dict(user_id=repo.user.user_id,
196 196 username=repo.user.username,
197 197 firstname=repo.user.firstname,
198 198 lastname=repo.user.lastname,
199 199 gravatar_link=h.gravatar_url(repo.user.email, 14)),
200 200 'description': repo.description.split('\n', 1)[0],
201 201 'revs': h.select('other_ref', selected_other_ref, other_refs, class_='refs')
202 202 }
203 203
204 204 # add org repo to other so we can open pull request against peer branches on itself
205 205 add_other_repo(org_repo, branch_rev=org_rev)
206 206 c.default_other_repo = org_repo.repo_name
207 207
208 208 # gather forks and add to this list ... even though it is rare to
209 209 # request forks to pull from their parent
210 210 for fork in org_repo.forks:
211 211 add_other_repo(fork)
212 212
213 213 # add parents of this fork also, but only if it's not empty
214 214 if org_repo.parent and org_repo.parent.scm_instance.revisions:
215 215 add_other_repo(org_repo.parent)
216 216 c.default_other_repo = org_repo.parent.repo_name
217 217
218 218 c.default_other_repo_info = other_repos_info[c.default_other_repo]
219 219 c.other_repos_info = json.dumps(other_repos_info)
220 220
221 221 return render('/pullrequests/pullrequest.html')
222 222
223 223 @NotAnonymous()
224 224 def create(self, repo_name):
225 225 repo = RepoModel()._get_repo(repo_name)
226 226 try:
227 227 _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
228 228 except formencode.Invalid, errors:
229 229 log.error(traceback.format_exc())
230 230 if errors.error_dict.get('revisions'):
231 231 msg = 'Revisions: %s' % errors.error_dict['revisions']
232 232 elif errors.error_dict.get('pullrequest_title'):
233 233 msg = _('Pull request requires a title with min. 3 chars')
234 234 else:
235 235 msg = _('Error creating pull request')
236 236
237 237 h.flash(msg, 'error')
238 238 return redirect(url('pullrequest_home', repo_name=repo_name))
239 239
240 240 org_repo = _form['org_repo']
241 241 org_ref = 'rev:merge:%s' % _form['merge_rev']
242 242 other_repo = _form['other_repo']
243 243 other_ref = 'rev:ancestor:%s' % _form['ancestor_rev']
244 revisions = _form['revisions']
244 revisions = reversed(_form['revisions'])
245 245 reviewers = _form['review_members']
246 246
247 247 title = _form['pullrequest_title']
248 248 description = _form['pullrequest_desc']
249 249
250 250 try:
251 251 pull_request = PullRequestModel().create(
252 252 self.rhodecode_user.user_id, org_repo, org_ref, other_repo,
253 253 other_ref, revisions, reviewers, title, description
254 254 )
255 255 Session().commit()
256 256 h.flash(_('Successfully opened new pull request'),
257 257 category='success')
258 258 except Exception:
259 259 h.flash(_('Error occurred during sending pull request'),
260 260 category='error')
261 261 log.error(traceback.format_exc())
262 262 return redirect(url('pullrequest_home', repo_name=repo_name))
263 263
264 264 return redirect(url('pullrequest_show', repo_name=other_repo,
265 265 pull_request_id=pull_request.pull_request_id))
266 266
267 267 @NotAnonymous()
268 268 @jsonify
269 269 def update(self, repo_name, pull_request_id):
270 270 pull_request = PullRequest.get_or_404(pull_request_id)
271 271 if pull_request.is_closed():
272 272 raise HTTPForbidden()
273 273 #only owner or admin can update it
274 274 owner = pull_request.author.user_id == c.rhodecode_user.user_id
275 275 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
276 276 reviewers_ids = map(int, filter(lambda v: v not in [None, ''],
277 277 request.POST.get('reviewers_ids', '').split(',')))
278 278
279 279 PullRequestModel().update_reviewers(pull_request_id, reviewers_ids)
280 280 Session().commit()
281 281 return True
282 282 raise HTTPForbidden()
283 283
284 284 @NotAnonymous()
285 285 @jsonify
286 286 def delete(self, repo_name, pull_request_id):
287 287 pull_request = PullRequest.get_or_404(pull_request_id)
288 288 #only owner can delete it !
289 289 if pull_request.author.user_id == c.rhodecode_user.user_id:
290 290 PullRequestModel().delete(pull_request)
291 291 Session().commit()
292 292 h.flash(_('Successfully deleted pull request'),
293 293 category='success')
294 294 return redirect(url('admin_settings_my_account', anchor='pullrequests'))
295 295 raise HTTPForbidden()
296 296
297 297 def _load_compare_data(self, pull_request, enable_comments=True):
298 298 """
299 299 Load context data needed for generating compare diff
300 300
301 301 :param pull_request:
302 302 :type pull_request:
303 303 """
304 304 org_repo = pull_request.org_repo
305 305 (org_ref_type,
306 306 org_ref_name,
307 307 org_ref_rev) = pull_request.org_ref.split(':')
308 308
309 309 other_repo = org_repo
310 310 (other_ref_type,
311 311 other_ref_name,
312 312 other_ref_rev) = pull_request.other_ref.split(':')
313 313
314 314 # despite opening revisions for bookmarks/branches/tags, we always
315 315 # convert this to rev to prevent changes after bookmark or branch change
316 316 org_ref = ('rev', org_ref_rev)
317 317 other_ref = ('rev', other_ref_rev)
318 318
319 319 c.org_repo = org_repo
320 320 c.other_repo = other_repo
321 321
322 322 c.fulldiff = fulldiff = request.GET.get('fulldiff')
323 323
324 324 c.cs_ranges = [org_repo.get_changeset(x) for x in pull_request.revisions]
325 325
326 326 c.statuses = org_repo.statuses([x.raw_id for x in c.cs_ranges])
327 327
328 328 c.org_ref = org_ref[1]
329 329 c.org_ref_type = org_ref[0]
330 330 c.other_ref = other_ref[1]
331 331 c.other_ref_type = other_ref[0]
332 332
333 333 diff_limit = self.cut_off_limit if not fulldiff else None
334 334
335 335 #we swap org/other ref since we run a simple diff on one repo
336 336 log.debug('running diff between %s@%s and %s@%s'
337 337 % (org_repo.scm_instance.path, org_ref,
338 338 other_repo.scm_instance.path, other_ref))
339 339 _diff = org_repo.scm_instance.get_diff(rev1=safe_str(org_ref[1]), rev2=safe_str(other_ref[1]))
340 340
341 341 diff_processor = diffs.DiffProcessor(_diff or '', format='gitdiff',
342 342 diff_limit=diff_limit)
343 343 _parsed = diff_processor.prepare()
344 344
345 345 c.limited_diff = False
346 346 if isinstance(_parsed, LimitedDiffContainer):
347 347 c.limited_diff = True
348 348
349 349 c.files = []
350 350 c.changes = {}
351 351 c.lines_added = 0
352 352 c.lines_deleted = 0
353 353 for f in _parsed:
354 354 st = f['stats']
355 355 if st[0] != 'b':
356 356 c.lines_added += st[0]
357 357 c.lines_deleted += st[1]
358 358 fid = h.FID('', f['filename'])
359 359 c.files.append([fid, f['operation'], f['filename'], f['stats']])
360 360 diff = diff_processor.as_html(enable_comments=enable_comments,
361 361 parsed_lines=[f])
362 362 c.changes[fid] = [f['operation'], f['filename'], diff]
363 363
364 364 def show(self, repo_name, pull_request_id):
365 365 repo_model = RepoModel()
366 366 c.users_array = repo_model.get_users_js()
367 367 c.users_groups_array = repo_model.get_users_groups_js()
368 368 c.pull_request = PullRequest.get_or_404(pull_request_id)
369 369 c.allowed_to_change_status = self._get_is_allowed_change_status(c.pull_request)
370 370 cc_model = ChangesetCommentsModel()
371 371 cs_model = ChangesetStatusModel()
372 372 _cs_statuses = cs_model.get_statuses(c.pull_request.org_repo,
373 373 pull_request=c.pull_request,
374 374 with_revisions=True)
375 375
376 376 cs_statuses = defaultdict(list)
377 377 for st in _cs_statuses:
378 378 cs_statuses[st.author.username] += [st]
379 379
380 380 c.pull_request_reviewers = []
381 381 c.pull_request_pending_reviewers = []
382 382 for o in c.pull_request.reviewers:
383 383 st = cs_statuses.get(o.user.username, None)
384 384 if st:
385 385 sorter = lambda k: k.version
386 386 st = [(x, list(y)[0])
387 387 for x, y in (groupby(sorted(st, key=sorter), sorter))]
388 388 else:
389 389 c.pull_request_pending_reviewers.append(o.user)
390 390 c.pull_request_reviewers.append([o.user, st])
391 391
392 392 # pull_requests repo_name we opened it against
393 393 # ie. other_repo must match
394 394 if repo_name != c.pull_request.other_repo.repo_name:
395 395 raise HTTPNotFound
396 396
397 397 # load compare data into template context
398 398 enable_comments = not c.pull_request.is_closed()
399 399 self._load_compare_data(c.pull_request, enable_comments=enable_comments)
400 400
401 401 # inline comments
402 402 c.inline_cnt = 0
403 403 c.inline_comments = cc_model.get_inline_comments(
404 404 c.rhodecode_db_repo.repo_id,
405 405 pull_request=pull_request_id)
406 406 # count inline comments
407 407 for __, lines in c.inline_comments:
408 408 for comments in lines.values():
409 409 c.inline_cnt += len(comments)
410 410 # comments
411 411 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
412 412 pull_request=pull_request_id)
413 413
414 414 try:
415 415 cur_status = c.statuses[c.pull_request.revisions[0]][0]
416 416 except Exception:
417 417 log.error(traceback.format_exc())
418 418 cur_status = 'undefined'
419 419 if c.pull_request.is_closed() and 0:
420 420 c.current_changeset_status = cur_status
421 421 else:
422 422 # changeset(pull-request) status calulation based on reviewers
423 423 c.current_changeset_status = cs_model.calculate_status(
424 424 c.pull_request_reviewers,
425 425 )
426 426 c.changeset_statuses = ChangesetStatus.STATUSES
427 427
428 428 c.as_form = False
429 429 c.ancestor = None # there is one - but right here we don't know which
430 430 return render('/pullrequests/pullrequest_show.html')
431 431
432 432 @NotAnonymous()
433 433 @jsonify
434 434 def comment(self, repo_name, pull_request_id):
435 435 pull_request = PullRequest.get_or_404(pull_request_id)
436 436 if pull_request.is_closed():
437 437 raise HTTPForbidden()
438 438
439 439 status = request.POST.get('changeset_status')
440 440 change_status = request.POST.get('change_changeset_status')
441 441 text = request.POST.get('text')
442 442 close_pr = request.POST.get('save_close')
443 443
444 444 allowed_to_change_status = self._get_is_allowed_change_status(pull_request)
445 445 if status and change_status and allowed_to_change_status:
446 446 _def = (_('Status change -> %s')
447 447 % ChangesetStatus.get_status_lbl(status))
448 448 if close_pr:
449 449 _def = _('Closing with') + ' ' + _def
450 450 text = text or _def
451 451 comm = ChangesetCommentsModel().create(
452 452 text=text,
453 453 repo=c.rhodecode_db_repo.repo_id,
454 454 user=c.rhodecode_user.user_id,
455 455 pull_request=pull_request_id,
456 456 f_path=request.POST.get('f_path'),
457 457 line_no=request.POST.get('line'),
458 458 status_change=(ChangesetStatus.get_status_lbl(status)
459 459 if status and change_status
460 460 and allowed_to_change_status else None),
461 461 closing_pr=close_pr
462 462 )
463 463
464 464 action_logger(self.rhodecode_user,
465 465 'user_commented_pull_request:%s' % pull_request_id,
466 466 c.rhodecode_db_repo, self.ip_addr, self.sa)
467 467
468 468 if allowed_to_change_status:
469 469 # get status if set !
470 470 if status and change_status:
471 471 ChangesetStatusModel().set_status(
472 472 c.rhodecode_db_repo.repo_id,
473 473 status,
474 474 c.rhodecode_user.user_id,
475 475 comm,
476 476 pull_request=pull_request_id
477 477 )
478 478
479 479 if close_pr:
480 480 if status in ['rejected', 'approved']:
481 481 PullRequestModel().close_pull_request(pull_request_id)
482 482 action_logger(self.rhodecode_user,
483 483 'user_closed_pull_request:%s' % pull_request_id,
484 484 c.rhodecode_db_repo, self.ip_addr, self.sa)
485 485 else:
486 486 h.flash(_('Closing pull request on other statuses than '
487 487 'rejected or approved forbidden'),
488 488 category='warning')
489 489
490 490 Session().commit()
491 491
492 492 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
493 493 return redirect(h.url('pullrequest_show', repo_name=repo_name,
494 494 pull_request_id=pull_request_id))
495 495
496 496 data = {
497 497 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
498 498 }
499 499 if comm:
500 500 c.co = comm
501 501 data.update(comm.get_dict())
502 502 data.update({'rendered_text':
503 503 render('changeset/changeset_comment_block.html')})
504 504
505 505 return data
506 506
507 507 @NotAnonymous()
508 508 @jsonify
509 509 def delete_comment(self, repo_name, comment_id):
510 510 co = ChangesetComment.get(comment_id)
511 511 if co.pull_request.is_closed():
512 512 #don't allow deleting comments on closed pull request
513 513 raise HTTPForbidden()
514 514
515 515 owner = co.author.user_id == c.rhodecode_user.user_id
516 516 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
517 517 ChangesetCommentsModel().delete(comment=co)
518 518 Session().commit()
519 519 return True
520 520 else:
521 521 raise HTTPForbidden()
General Comments 0
You need to be logged in to leave comments. Login now