##// END OF EJS Templates
pull requests: add a 'Calculated' comment on current_changeset_status
Mads Kiilerich -
r4053:3625fd19 default
parent child Browse files
Show More
@@ -1,550 +1,550 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, tmpl_context as c, url
34 34 from pylons.controllers.util import 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.diffs import LimitedDiffContainer
48 48 from rhodecode.model.db import PullRequest, ChangesetStatus, ChangesetComment
49 49 from rhodecode.model.pull_request import PullRequestModel
50 50 from rhodecode.model.meta import Session
51 51 from rhodecode.model.repo import RepoModel
52 52 from rhodecode.model.comment import ChangesetCommentsModel
53 53 from rhodecode.model.changeset_status import ChangesetStatusModel
54 54 from rhodecode.model.forms import PullRequestForm
55 55 from rhodecode.lib.utils2 import safe_int
56 56
57 57 log = logging.getLogger(__name__)
58 58
59 59
60 60 class PullrequestsController(BaseRepoController):
61 61
62 62 def __before__(self):
63 63 super(PullrequestsController, self).__before__()
64 64 repo_model = RepoModel()
65 65 c.users_array = repo_model.get_users_js()
66 66 c.users_groups_array = repo_model.get_users_groups_js()
67 67
68 68 def _get_repo_refs(self, repo, rev=None, branch=None, branch_rev=None):
69 69 """return a structure with repo's interesting changesets, suitable for
70 70 the selectors in pullrequest.html
71 71
72 72 rev: a revision that must be in the list somehow and selected by default
73 73 branch: a branch that must be in the list and selected by default - even if closed
74 74 branch_rev: a revision of which peers should be preferred and available."""
75 75 # list named branches that has been merged to this named branch - it should probably merge back
76 76 peers = []
77 77
78 78 if rev:
79 79 rev = safe_str(rev)
80 80
81 81 if branch:
82 82 branch = safe_str(branch)
83 83
84 84 if branch_rev:
85 85 branch_rev = safe_str(branch_rev)
86 86 # not restricting to merge() would also get branch point and be better
87 87 # (especially because it would get the branch point) ... but is currently too expensive
88 88 otherbranches = {}
89 89 for i in repo._repo.revs(
90 90 "sort(parents(branch(id(%s)) and merge()) - branch(id(%s)))",
91 91 branch_rev, branch_rev):
92 92 cs = repo.get_changeset(i)
93 93 otherbranches[cs.branch] = cs.raw_id
94 94 for abranch, node in otherbranches.iteritems():
95 95 selected = 'branch:%s:%s' % (abranch, node)
96 96 peers.append((selected, abranch))
97 97
98 98 selected = None
99 99
100 100 branches = []
101 101 for abranch, branchrev in repo.branches.iteritems():
102 102 n = 'branch:%s:%s' % (abranch, branchrev)
103 103 branches.append((n, abranch))
104 104 if rev == branchrev:
105 105 selected = n
106 106 if branch == abranch:
107 107 selected = n
108 108 branch = None
109 109 if branch: # branch not in list - it is probably closed
110 110 revs = repo._repo.revs('max(branch(%s))', branch)
111 111 if revs:
112 112 cs = repo.get_changeset(revs[0])
113 113 selected = 'branch:%s:%s' % (branch, cs.raw_id)
114 114 branches.append((selected, branch))
115 115
116 116 bookmarks = []
117 117 for bookmark, bookmarkrev in repo.bookmarks.iteritems():
118 118 n = 'book:%s:%s' % (bookmark, bookmarkrev)
119 119 bookmarks.append((n, bookmark))
120 120 if rev == bookmarkrev:
121 121 selected = n
122 122
123 123 tags = []
124 124 for tag, tagrev in repo.tags.iteritems():
125 125 n = 'tag:%s:%s' % (tag, tagrev)
126 126 tags.append((n, tag))
127 127 if rev == tagrev and tag != 'tip': # tip is not a real tag - and its branch is better
128 128 selected = n
129 129
130 130 # prio 1: rev was selected as existing entry above
131 131
132 132 # prio 2: create special entry for rev; rev _must_ be used
133 133 specials = []
134 134 if rev and selected is None:
135 135 selected = 'rev:%s:%s' % (rev, rev)
136 136 specials = [(selected, '%s: %s' % (_("Changeset"), rev[:12]))]
137 137
138 138 # prio 3: most recent peer branch
139 139 if peers and not selected:
140 140 selected = peers[0][0][0]
141 141
142 142 # prio 4: tip revision
143 143 if not selected:
144 144 selected = 'tag:tip:%s' % repo.tags['tip']
145 145
146 146 groups = [(specials, _("Special")),
147 147 (peers, _("Peer branches")),
148 148 (bookmarks, _("Bookmarks")),
149 149 (branches, _("Branches")),
150 150 (tags, _("Tags")),
151 151 ]
152 152 return [g for g in groups if g[0]], selected
153 153
154 154 def _get_is_allowed_change_status(self, pull_request):
155 155 owner = self.rhodecode_user.user_id == pull_request.user_id
156 156 reviewer = self.rhodecode_user.user_id in [x.user_id for x in
157 157 pull_request.reviewers]
158 158 return (self.rhodecode_user.admin or owner or reviewer)
159 159
160 160 def _load_compare_data(self, pull_request, enable_comments=True):
161 161 """
162 162 Load context data needed for generating compare diff
163 163
164 164 :param pull_request:
165 165 """
166 166 org_repo = pull_request.org_repo
167 167 (org_ref_type,
168 168 org_ref_name,
169 169 org_ref_rev) = pull_request.org_ref.split(':')
170 170
171 171 other_repo = org_repo
172 172 (other_ref_type,
173 173 other_ref_name,
174 174 other_ref_rev) = pull_request.other_ref.split(':')
175 175
176 176 # despite opening revisions for bookmarks/branches/tags, we always
177 177 # convert this to rev to prevent changes after bookmark or branch change
178 178 org_ref = ('rev', org_ref_rev)
179 179 other_ref = ('rev', other_ref_rev)
180 180
181 181 c.org_repo = org_repo
182 182 c.other_repo = other_repo
183 183
184 184 c.fulldiff = fulldiff = request.GET.get('fulldiff')
185 185
186 186 c.cs_ranges = [org_repo.get_changeset(x) for x in pull_request.revisions]
187 187
188 188 c.statuses = org_repo.statuses([x.raw_id for x in c.cs_ranges])
189 189
190 190 c.org_ref = org_ref[1]
191 191 c.org_ref_type = org_ref[0]
192 192 c.other_ref = other_ref[1]
193 193 c.other_ref_type = other_ref[0]
194 194
195 195 diff_limit = self.cut_off_limit if not fulldiff else None
196 196
197 197 # we swap org/other ref since we run a simple diff on one repo
198 198 log.debug('running diff between %s and %s in %s'
199 199 % (other_ref, org_ref, org_repo.scm_instance.path))
200 200 txtdiff = org_repo.scm_instance.get_diff(rev1=safe_str(other_ref[1]), rev2=safe_str(org_ref[1]))
201 201
202 202 diff_processor = diffs.DiffProcessor(txtdiff or '', format='gitdiff',
203 203 diff_limit=diff_limit)
204 204 _parsed = diff_processor.prepare()
205 205
206 206 c.limited_diff = False
207 207 if isinstance(_parsed, LimitedDiffContainer):
208 208 c.limited_diff = True
209 209
210 210 c.files = []
211 211 c.changes = {}
212 212 c.lines_added = 0
213 213 c.lines_deleted = 0
214 214
215 215 for f in _parsed:
216 216 st = f['stats']
217 217 c.lines_added += st['added']
218 218 c.lines_deleted += st['deleted']
219 219 fid = h.FID('', f['filename'])
220 220 c.files.append([fid, f['operation'], f['filename'], f['stats']])
221 221 htmldiff = diff_processor.as_html(enable_comments=enable_comments,
222 222 parsed_lines=[f])
223 223 c.changes[fid] = [f['operation'], f['filename'], htmldiff]
224 224
225 225 @LoginRequired()
226 226 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
227 227 'repository.admin')
228 228 def show_all(self, repo_name):
229 229 c.from_ = request.GET.get('from_') or ''
230 230 c.closed = request.GET.get('closed') or ''
231 231 c.pull_requests = PullRequestModel().get_all(repo_name, from_=c.from_, closed=c.closed)
232 232 c.repo_name = repo_name
233 233 p = safe_int(request.GET.get('page', 1), 1)
234 234
235 235 c.pullrequests_pager = Page(c.pull_requests, page=p, items_per_page=10)
236 236
237 237 c.pullrequest_data = render('/pullrequests/pullrequest_data.html')
238 238
239 239 if request.environ.get('HTTP_X_PARTIAL_XHR'):
240 240 return c.pullrequest_data
241 241
242 242 return render('/pullrequests/pullrequest_show_all.html')
243 243
244 244 @LoginRequired()
245 245 @NotAnonymous()
246 246 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
247 247 'repository.admin')
248 248 def index(self):
249 249 org_repo = c.rhodecode_db_repo
250 250
251 251 if org_repo.scm_instance.alias != 'hg':
252 252 log.error('Review not available for GIT REPOS')
253 253 raise HTTPNotFound
254 254
255 255 try:
256 256 org_repo.scm_instance.get_changeset()
257 257 except EmptyRepositoryError, e:
258 258 h.flash(h.literal(_('There are no changesets yet')),
259 259 category='warning')
260 260 redirect(url('summary_home', repo_name=org_repo.repo_name))
261 261
262 262 org_rev = request.GET.get('rev_end')
263 263 # rev_start is not directly useful - its parent could however be used
264 264 # as default for other and thus give a simple compare view
265 265 #other_rev = request.POST.get('rev_start')
266 266 branch = request.GET.get('branch')
267 267
268 268 c.org_repos = []
269 269 c.org_repos.append((org_repo.repo_name, org_repo.repo_name))
270 270 c.default_org_repo = org_repo.repo_name
271 271 c.org_refs, c.default_org_ref = self._get_repo_refs(org_repo.scm_instance, rev=org_rev, branch=branch)
272 272
273 273 c.other_repos = []
274 274 other_repos_info = {}
275 275
276 276 def add_other_repo(repo, branch_rev=None):
277 277 if repo.repo_name in other_repos_info: # shouldn't happen
278 278 return
279 279 c.other_repos.append((repo.repo_name, repo.repo_name))
280 280 other_refs, selected_other_ref = self._get_repo_refs(repo.scm_instance, branch_rev=branch_rev)
281 281 other_repos_info[repo.repo_name] = {
282 282 'user': dict(user_id=repo.user.user_id,
283 283 username=repo.user.username,
284 284 firstname=repo.user.firstname,
285 285 lastname=repo.user.lastname,
286 286 gravatar_link=h.gravatar_url(repo.user.email, 14)),
287 287 'description': repo.description.split('\n', 1)[0],
288 288 'revs': h.select('other_ref', selected_other_ref, other_refs, class_='refs')
289 289 }
290 290
291 291 # add org repo to other so we can open pull request against peer branches on itself
292 292 add_other_repo(org_repo, branch_rev=org_rev)
293 293 c.default_other_repo = org_repo.repo_name
294 294
295 295 # gather forks and add to this list ... even though it is rare to
296 296 # request forks to pull from their parent
297 297 for fork in org_repo.forks:
298 298 add_other_repo(fork)
299 299
300 300 # add parents of this fork also, but only if it's not empty
301 301 if org_repo.parent and org_repo.parent.scm_instance.revisions:
302 302 add_other_repo(org_repo.parent)
303 303 c.default_other_repo = org_repo.parent.repo_name
304 304
305 305 c.default_other_repo_info = other_repos_info[c.default_other_repo]
306 306 c.other_repos_info = json.dumps(other_repos_info)
307 307
308 308 return render('/pullrequests/pullrequest.html')
309 309
310 310 @LoginRequired()
311 311 @NotAnonymous()
312 312 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
313 313 'repository.admin')
314 314 def create(self, repo_name):
315 315 repo = RepoModel()._get_repo(repo_name)
316 316 try:
317 317 _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
318 318 except formencode.Invalid, errors:
319 319 log.error(traceback.format_exc())
320 320 if errors.error_dict.get('revisions'):
321 321 msg = 'Revisions: %s' % errors.error_dict['revisions']
322 322 elif errors.error_dict.get('pullrequest_title'):
323 323 msg = _('Pull request requires a title with min. 3 chars')
324 324 else:
325 325 msg = _('Error creating pull request')
326 326
327 327 h.flash(msg, 'error')
328 328 return redirect(url('pullrequest_home', repo_name=repo_name))
329 329
330 330 org_repo = _form['org_repo']
331 331 org_ref = _form['org_ref'] # will end with merge_rev but have symbolic name
332 332 other_repo = _form['other_repo']
333 333 other_ref = 'rev:ancestor:%s' % _form['ancestor_rev'] # could be calculated from other_ref ...
334 334 revisions = [x for x in reversed(_form['revisions'])]
335 335 reviewers = _form['review_members']
336 336
337 337 title = _form['pullrequest_title']
338 338 description = _form['pullrequest_desc']
339 339 try:
340 340 pull_request = PullRequestModel().create(
341 341 self.rhodecode_user.user_id, org_repo, org_ref, other_repo,
342 342 other_ref, revisions, reviewers, title, description
343 343 )
344 344 Session().commit()
345 345 h.flash(_('Successfully opened new pull request'),
346 346 category='success')
347 347 except Exception:
348 348 h.flash(_('Error occurred during sending pull request'),
349 349 category='error')
350 350 log.error(traceback.format_exc())
351 351 return redirect(url('pullrequest_home', repo_name=repo_name))
352 352
353 353 return redirect(url('pullrequest_show', repo_name=other_repo,
354 354 pull_request_id=pull_request.pull_request_id))
355 355
356 356 @LoginRequired()
357 357 @NotAnonymous()
358 358 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
359 359 'repository.admin')
360 360 @jsonify
361 361 def update(self, repo_name, pull_request_id):
362 362 pull_request = PullRequest.get_or_404(pull_request_id)
363 363 if pull_request.is_closed():
364 364 raise HTTPForbidden()
365 365 #only owner or admin can update it
366 366 owner = pull_request.author.user_id == c.rhodecode_user.user_id
367 367 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
368 368 reviewers_ids = map(int, filter(lambda v: v not in [None, ''],
369 369 request.POST.get('reviewers_ids', '').split(',')))
370 370
371 371 PullRequestModel().update_reviewers(pull_request_id, reviewers_ids)
372 372 Session().commit()
373 373 return True
374 374 raise HTTPForbidden()
375 375
376 376 @LoginRequired()
377 377 @NotAnonymous()
378 378 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
379 379 'repository.admin')
380 380 @jsonify
381 381 def delete(self, repo_name, pull_request_id):
382 382 pull_request = PullRequest.get_or_404(pull_request_id)
383 383 #only owner can delete it !
384 384 if pull_request.author.user_id == c.rhodecode_user.user_id:
385 385 PullRequestModel().delete(pull_request)
386 386 Session().commit()
387 387 h.flash(_('Successfully deleted pull request'),
388 388 category='success')
389 389 return redirect(url('admin_settings_my_account', anchor='pullrequests'))
390 390 raise HTTPForbidden()
391 391
392 392 @LoginRequired()
393 393 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
394 394 'repository.admin')
395 395 def show(self, repo_name, pull_request_id):
396 396 repo_model = RepoModel()
397 397 c.users_array = repo_model.get_users_js()
398 398 c.users_groups_array = repo_model.get_users_groups_js()
399 399 c.pull_request = PullRequest.get_or_404(pull_request_id)
400 400 c.allowed_to_change_status = self._get_is_allowed_change_status(c.pull_request)
401 401 cc_model = ChangesetCommentsModel()
402 402 cs_model = ChangesetStatusModel()
403 403 _cs_statuses = cs_model.get_statuses(c.pull_request.org_repo,
404 404 pull_request=c.pull_request,
405 405 with_revisions=True)
406 406
407 407 cs_statuses = defaultdict(list)
408 408 for st in _cs_statuses:
409 409 cs_statuses[st.author.username] += [st]
410 410
411 411 c.pull_request_reviewers = []
412 412 c.pull_request_pending_reviewers = []
413 413 for o in c.pull_request.reviewers:
414 414 st = cs_statuses.get(o.user.username, None)
415 415 if st:
416 416 sorter = lambda k: k.version
417 417 st = [(x, list(y)[0])
418 418 for x, y in (groupby(sorted(st, key=sorter), sorter))]
419 419 else:
420 420 c.pull_request_pending_reviewers.append(o.user)
421 421 c.pull_request_reviewers.append([o.user, st])
422 422
423 423 # pull_requests repo_name we opened it against
424 424 # ie. other_repo must match
425 425 if repo_name != c.pull_request.other_repo.repo_name:
426 426 raise HTTPNotFound
427 427
428 428 # load compare data into template context
429 429 enable_comments = not c.pull_request.is_closed()
430 430 self._load_compare_data(c.pull_request, enable_comments=enable_comments)
431 431
432 432 # inline comments
433 433 c.inline_cnt = 0
434 434 c.inline_comments = cc_model.get_inline_comments(
435 435 c.rhodecode_db_repo.repo_id,
436 436 pull_request=pull_request_id)
437 437 # count inline comments
438 438 for __, lines in c.inline_comments:
439 439 for comments in lines.values():
440 440 c.inline_cnt += len(comments)
441 441 # comments
442 442 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
443 443 pull_request=pull_request_id)
444 444
445 # changeset(pull-request) status calulation based on reviewers
445 # (badly named) pull-request status calculation based on reviewer votes
446 446 c.current_changeset_status = cs_model.calculate_status(
447 447 c.pull_request_reviewers,
448 448 )
449 449 c.changeset_statuses = ChangesetStatus.STATUSES
450 450
451 451 c.as_form = False
452 452 c.ancestor = None # there is one - but right here we don't know which
453 453 return render('/pullrequests/pullrequest_show.html')
454 454
455 455 @LoginRequired()
456 456 @NotAnonymous()
457 457 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
458 458 'repository.admin')
459 459 @jsonify
460 460 def comment(self, repo_name, pull_request_id):
461 461 pull_request = PullRequest.get_or_404(pull_request_id)
462 462 if pull_request.is_closed():
463 463 raise HTTPForbidden()
464 464
465 465 status = request.POST.get('changeset_status')
466 466 change_status = request.POST.get('change_changeset_status')
467 467 text = request.POST.get('text')
468 468 close_pr = request.POST.get('save_close')
469 469
470 470 allowed_to_change_status = self._get_is_allowed_change_status(pull_request)
471 471 if status and change_status and allowed_to_change_status:
472 472 _def = (_('Status change -> %s')
473 473 % ChangesetStatus.get_status_lbl(status))
474 474 if close_pr:
475 475 _def = _('Closing with') + ' ' + _def
476 476 text = text or _def
477 477 comm = ChangesetCommentsModel().create(
478 478 text=text,
479 479 repo=c.rhodecode_db_repo.repo_id,
480 480 user=c.rhodecode_user.user_id,
481 481 pull_request=pull_request_id,
482 482 f_path=request.POST.get('f_path'),
483 483 line_no=request.POST.get('line'),
484 484 status_change=(ChangesetStatus.get_status_lbl(status)
485 485 if status and change_status
486 486 and allowed_to_change_status else None),
487 487 closing_pr=close_pr
488 488 )
489 489
490 490 action_logger(self.rhodecode_user,
491 491 'user_commented_pull_request:%s' % pull_request_id,
492 492 c.rhodecode_db_repo, self.ip_addr, self.sa)
493 493
494 494 if allowed_to_change_status:
495 495 # get status if set !
496 496 if status and change_status:
497 497 ChangesetStatusModel().set_status(
498 498 c.rhodecode_db_repo.repo_id,
499 499 status,
500 500 c.rhodecode_user.user_id,
501 501 comm,
502 502 pull_request=pull_request_id
503 503 )
504 504
505 505 if close_pr:
506 506 if status in ['rejected', 'approved']:
507 507 PullRequestModel().close_pull_request(pull_request_id)
508 508 action_logger(self.rhodecode_user,
509 509 'user_closed_pull_request:%s' % pull_request_id,
510 510 c.rhodecode_db_repo, self.ip_addr, self.sa)
511 511 else:
512 512 h.flash(_('Closing pull request on other statuses than '
513 513 'rejected or approved forbidden'),
514 514 category='warning')
515 515
516 516 Session().commit()
517 517
518 518 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
519 519 return redirect(h.url('pullrequest_show', repo_name=repo_name,
520 520 pull_request_id=pull_request_id))
521 521
522 522 data = {
523 523 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
524 524 }
525 525 if comm:
526 526 c.co = comm
527 527 data.update(comm.get_dict())
528 528 data.update({'rendered_text':
529 529 render('changeset/changeset_comment_block.html')})
530 530
531 531 return data
532 532
533 533 @LoginRequired()
534 534 @NotAnonymous()
535 535 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
536 536 'repository.admin')
537 537 @jsonify
538 538 def delete_comment(self, repo_name, comment_id):
539 539 co = ChangesetComment.get(comment_id)
540 540 if co.pull_request.is_closed():
541 541 #don't allow deleting comments on closed pull request
542 542 raise HTTPForbidden()
543 543
544 544 owner = co.author.user_id == c.rhodecode_user.user_id
545 545 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
546 546 ChangesetCommentsModel().delete(comment=co)
547 547 Session().commit()
548 548 return True
549 549 else:
550 550 raise HTTPForbidden()
@@ -1,193 +1,193 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.changeset_status
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6
7 7 :created_on: Apr 30, 2012
8 8 :author: marcink
9 9 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
10 10 :license: GPLv3, see COPYING for more details.
11 11 """
12 12 # This program is free software: you can redistribute it and/or modify
13 13 # it under the terms of the GNU General Public License as published by
14 14 # the Free Software Foundation, either version 3 of the License, or
15 15 # (at your option) any later version.
16 16 #
17 17 # This program is distributed in the hope that it will be useful,
18 18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 20 # GNU General Public License for more details.
21 21 #
22 22 # You should have received a copy of the GNU General Public License
23 23 # along with this program. If not, see <http://www.gnu.org/licenses/>.
24 24
25 25
26 26 import logging
27 27 from collections import defaultdict
28 28
29 29 from rhodecode.model import BaseModel
30 30 from rhodecode.model.db import ChangesetStatus, PullRequest
31 31 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
32 32
33 33 log = logging.getLogger(__name__)
34 34
35 35
36 36 class ChangesetStatusModel(BaseModel):
37 37
38 38 cls = ChangesetStatus
39 39
40 40 def __get_changeset_status(self, changeset_status):
41 41 return self._get_instance(ChangesetStatus, changeset_status)
42 42
43 43 def __get_pull_request(self, pull_request):
44 44 return self._get_instance(PullRequest, pull_request)
45 45
46 46 def _get_status_query(self, repo, revision, pull_request,
47 47 with_revisions=False):
48 48 repo = self._get_repo(repo)
49 49
50 50 q = ChangesetStatus.query()\
51 51 .filter(ChangesetStatus.repo == repo)
52 52 if not with_revisions:
53 53 q = q.filter(ChangesetStatus.version == 0)
54 54
55 55 if revision:
56 56 q = q.filter(ChangesetStatus.revision == revision)
57 57 elif pull_request:
58 58 pull_request = self.__get_pull_request(pull_request)
59 59 q = q.filter(ChangesetStatus.pull_request == pull_request)
60 60 else:
61 61 raise Exception('Please specify revision or pull_request')
62 62 q.order_by(ChangesetStatus.version.asc())
63 63 return q
64 64
65 65 def calculate_status(self, statuses_by_reviewers):
66 66 """
67 leading one wins, if number of occurrences are equal than weaker wins
67 approved if consensus
68 (old description: leading one wins, if number of occurrences are equal than weaker wins)
68 69
69 70 :param statuses_by_reviewers:
70 71 """
71 status = None
72 72 votes = defaultdict(int)
73 73 reviewers_number = len(statuses_by_reviewers)
74 74 for user, statuses in statuses_by_reviewers:
75 75 if statuses:
76 76 ver, latest = statuses[0]
77 77 votes[latest.status] += 1
78 78 else:
79 79 votes[ChangesetStatus.DEFAULT] += 1
80 80
81 81 if votes.get(ChangesetStatus.STATUS_APPROVED) == reviewers_number:
82 82 return ChangesetStatus.STATUS_APPROVED
83 83 else:
84 84 return ChangesetStatus.STATUS_UNDER_REVIEW
85 85
86 86 def get_statuses(self, repo, revision=None, pull_request=None,
87 87 with_revisions=False):
88 88 q = self._get_status_query(repo, revision, pull_request,
89 89 with_revisions)
90 90 return q.all()
91 91
92 92 def get_status(self, repo, revision=None, pull_request=None, as_str=True):
93 93 """
94 94 Returns latest status of changeset for given revision or for given
95 95 pull request. Statuses are versioned inside a table itself and
96 96 version == 0 is always the current one
97 97
98 98 :param repo:
99 99 :param revision: 40char hash or None
100 100 :param pull_request: pull_request reference
101 101 :param as_str: return status as string not object
102 102 """
103 103 q = self._get_status_query(repo, revision, pull_request)
104 104
105 105 # need to use first here since there can be multiple statuses
106 106 # returned from pull_request
107 107 status = q.first()
108 108 if as_str:
109 109 status = status.status if status else status
110 110 st = status or ChangesetStatus.DEFAULT
111 111 return str(st)
112 112 return status
113 113
114 114 def set_status(self, repo, status, user, comment=None, revision=None,
115 115 pull_request=None, dont_allow_on_closed_pull_request=False):
116 116 """
117 117 Creates new status for changeset or updates the old ones bumping their
118 118 version, leaving the current status at
119 119
120 120 :param repo:
121 121 :param revision:
122 122 :param status:
123 123 :param user:
124 124 :param comment:
125 125 :param dont_allow_on_closed_pull_request: don't allow a status change
126 126 if last status was for pull request and it's closed. We shouldn't
127 127 mess around this manually
128 128 """
129 129 repo = self._get_repo(repo)
130 130
131 131 q = ChangesetStatus.query()
132 132 if not comment:
133 133 from rhodecode.model.comment import ChangesetCommentsModel
134 134 comment = ChangesetCommentsModel().create(
135 135 text='Auto status change to %s' % (ChangesetStatus.get_status_lbl(status)),
136 136 repo=repo,
137 137 user=user,
138 138 pull_request=pull_request,
139 139 send_email=False
140 140 )
141 141 if revision:
142 142 q = q.filter(ChangesetStatus.repo == repo)
143 143 q = q.filter(ChangesetStatus.revision == revision)
144 144 elif pull_request:
145 145 pull_request = self.__get_pull_request(pull_request)
146 146 q = q.filter(ChangesetStatus.repo == pull_request.org_repo)
147 147 q = q.filter(ChangesetStatus.revision.in_(pull_request.revisions))
148 148 cur_statuses = q.all()
149 149
150 150 #if statuses exists and last is associated with a closed pull request
151 151 # we need to check if we can allow this status change
152 152 if (dont_allow_on_closed_pull_request and cur_statuses
153 153 and getattr(cur_statuses[0].pull_request, 'status', '')
154 154 == PullRequest.STATUS_CLOSED):
155 155 raise StatusChangeOnClosedPullRequestError(
156 156 'Changing status on closed pull request is not allowed'
157 157 )
158 158
159 159 #update all current statuses with older version
160 160 if cur_statuses:
161 161 for st in cur_statuses:
162 162 st.version += 1
163 163 self.sa.add(st)
164 164
165 165 def _create_status(user, repo, status, comment, revision, pull_request):
166 166 new_status = ChangesetStatus()
167 167 new_status.author = self._get_user(user)
168 168 new_status.repo = self._get_repo(repo)
169 169 new_status.status = status
170 170 new_status.comment = comment
171 171 new_status.revision = revision
172 172 new_status.pull_request = pull_request
173 173 return new_status
174 174
175 175 if revision:
176 176 new_status = _create_status(user=user, repo=repo, status=status,
177 177 comment=comment, revision=revision,
178 178 pull_request=None)
179 179 self.sa.add(new_status)
180 180 return new_status
181 181 elif pull_request:
182 182 #pull request can have more than one revision associated to it
183 183 #we need to create new version for each one
184 184 new_statuses = []
185 185 repo = pull_request.org_repo
186 186 for rev in pull_request.revisions:
187 187 new_status = _create_status(user=user, repo=repo,
188 188 status=status, comment=comment,
189 189 revision=rev,
190 190 pull_request=pull_request)
191 191 new_statuses.append(new_status)
192 192 self.sa.add(new_status)
193 193 return new_statuses
@@ -1,257 +1,257 b''
1 1 <%inherit file="/base/base.html"/>
2 2
3 3 <%def name="title()">
4 4 ${_('%s Pull Request #%s') % (c.repo_name, c.pull_request.pull_request_id)} &middot; ${c.rhodecode_name}
5 5 </%def>
6 6
7 7 <%def name="breadcrumbs_links()">
8 8 ${_('Pull request #%s') % c.pull_request.pull_request_id}
9 9 </%def>
10 10
11 11 <%def name="page_nav()">
12 12 ${self.menu('repositories')}
13 13 </%def>
14 14
15 15 <%def name="main()">
16 16 ${self.repo_context_bar('showpullrequest')}
17 17 <div class="box">
18 18 <!-- box / title -->
19 19 <div class="title">
20 20 ${self.breadcrumbs()}
21 21 </div>
22 22
23 23 <h3 class="${'closed' if c.pull_request.is_closed() else ''}">
24 24 <img src="${h.url('/images/icons/flag_status_%s.png' % str(c.pull_request.last_review_status))}" />
25 25 ${_('Title')}: ${c.pull_request.title}
26 26 %if c.pull_request.is_closed():
27 27 (${_('Closed')})
28 28 %endif
29 29 </h3>
30 30
31 31 <div class="form">
32 32 <div id="summary" class="fields">
33 33 <div class="field">
34 34 <div class="label-summary">
35 35 <label>${_('Review status')}:</label>
36 36 </div>
37 37 <div class="input">
38 38 <div class="changeset-status-container" style="float:none;clear:both">
39 39 %if c.current_changeset_status:
40 <div title="${_('Pull request status')}" class="changeset-status-lbl">
40 <div title="${_('Pull request status calculated from votes')}" class="changeset-status-lbl">
41 41 %if c.pull_request.is_closed():
42 42 ${_('Closed')},
43 43 %endif
44 44 ${h.changeset_status_lbl(c.current_changeset_status)}
45 45 </div>
46 <div class="changeset-status-ico" style="padding:1px 4px"><img src="${h.url('/images/icons/flag_status_%s.png' % c.current_changeset_status)}" /></div>
46 <div class="changeset-status-ico" style="padding:1px 4px"><img src="${h.url('/images/icons/flag_status_%s.png' % c.current_changeset_status)}" title="${_('Pull request status calculated from votes')}"/></div>
47 47 %endif
48 48 </div>
49 49 </div>
50 50 </div>
51 51 <div class="field">
52 52 <div class="label-summary">
53 53 <label>${_('Still not reviewed by')}:</label>
54 54 </div>
55 55 <div class="input">
56 56 % if len(c.pull_request_pending_reviewers) > 0:
57 57 <div class="tooltip" title="${h.tooltip(', '.join([x.username for x in c.pull_request_pending_reviewers]))}">${ungettext('%d reviewer', '%d reviewers',len(c.pull_request_pending_reviewers)) % len(c.pull_request_pending_reviewers)}</div>
58 58 %else:
59 59 <div>${_('Pull request was reviewed by all reviewers')}</div>
60 60 %endif
61 61 </div>
62 62 </div>
63 63 <div class="field">
64 64 <div class="label-summary">
65 65 <label>${_('Origin repository')}:</label>
66 66 </div>
67 67 <div class="input">
68 68 <div>
69 69 ##%if h.is_hg(c.pull_request.org_repo):
70 70 ## <img class="icon" title="${_('Mercurial repository')}" alt="${_('Mercurial repository')}" src="${h.url('/images/icons/hgicon.png')}"/>
71 71 ##%elif h.is_git(c.pull_request.org_repo):
72 72 ## <img class="icon" title="${_('Git repository')}" alt="${_('Git repository')}" src="${h.url('/images/icons/giticon.png')}"/>
73 73 ##%endif
74 74 <span class="spantag">${c.pull_request.org_ref_parts[0]}: ${c.pull_request.org_ref_parts[1]}</span>
75 75 <span>
76 76 %if h.is_hg(c.pull_request.org_repo):
77 77 | ${_('Pull changes')} <span style="font-family: monospace">hg pull -r ${h.short_id(c.cs_ranges[-1].raw_id)} <a href="${h.url('summary_home', repo_name=c.pull_request.org_repo.repo_name)}">${c.pull_request.org_repo.clone_url()}</a></span>
78 78 %elif h.is_git(c.pull_request.org_repo):
79 79 | ${_('Pull changes')}
80 80 %endif
81 81 </div>
82 82 </div>
83 83 </div>
84 84 <div class="field">
85 85 <div class="label-summary">
86 86 <label>${_('Description')}:</label>
87 87 </div>
88 88 <div class="input">
89 89 <div style="white-space:pre-wrap">${h.urlify_commit(c.pull_request.description)}</div>
90 90 </div>
91 91 </div>
92 92 <div class="field">
93 93 <div class="label-summary">
94 94 <label>${_('Created on')}:</label>
95 95 </div>
96 96 <div class="input">
97 97 <div>${h.fmt_date(c.pull_request.created_on)}</div>
98 98 </div>
99 99 </div>
100 100 </div>
101 101 </div>
102 102
103 103 <div style="overflow: auto;">
104 104 ##DIFF
105 105 <div class="table" style="float:left;clear:none">
106 106 <div id="body" class="diffblock">
107 107 <div style="white-space:pre-wrap;padding:5px">${_('Compare view')}</div>
108 108 </div>
109 109 <div id="changeset_compare_view_content">
110 110 ##CS
111 111 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${ungettext('Showing %s commit','Showing %s commits', len(c.cs_ranges)) % len(c.cs_ranges)}</div>
112 112 <%include file="/compare/compare_cs.html" />
113 113
114 114 ## FILES
115 115 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">
116 116
117 117 % if c.limited_diff:
118 118 ${ungettext('%s file changed', '%s files changed', len(c.files)) % len(c.files)}
119 119 % else:
120 120 ${ungettext('%s file changed with %s insertions and %s deletions','%s files changed with %s insertions and %s deletions', len(c.files)) % (len(c.files),c.lines_added,c.lines_deleted)}:
121 121 %endif
122 122
123 123 </div>
124 124 <div class="cs_files">
125 125 %if not c.files:
126 126 <span class="empty_data">${_('No files')}</span>
127 127 %endif
128 128 %for fid, change, f, stat in c.files:
129 129 <div class="cs_${change}">
130 130 <div class="node">${h.link_to(h.safe_unicode(f),h.url.current(anchor=fid))}</div>
131 131 <div class="changes">${h.fancy_file_stats(stat)}</div>
132 132 </div>
133 133 %endfor
134 134 </div>
135 135 % if c.limited_diff:
136 136 <h5>${_('Changeset was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}" onclick="return confirm('${_("Showing a huge diff might take some time and resources")}')">${_('Show full diff')}</a></h5>
137 137 % endif
138 138 </div>
139 139 </div>
140 140 ## REVIEWERS
141 141 <div style="float:left; border-left:1px dashed #eee">
142 142 <h4>${_('Pull request reviewers')}</h4>
143 143 <div id="reviewers" style="padding:0px 0px 5px 10px">
144 144 ## members goes here !
145 145 <div class="group_members_wrap" style="min-height:45px">
146 146 <ul id="review_members" class="group_members">
147 147 %for member,status in c.pull_request_reviewers:
148 148 <li id="reviewer_${member.user_id}">
149 149 <div class="reviewers_member">
150 150 <div style="float:left;padding:0px 3px 0px 0px" class="tooltip" title="${h.tooltip(h.changeset_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
151 151 <img src="${h.url(str('/images/icons/flag_status_%s.png' % (status[0][1].status if status else 'not_reviewed')))}"/>
152 152 </div>
153 153 <div class="gravatar"><img alt="gravatar" src="${h.gravatar_url(member.email,14)}"/> </div>
154 154 <div style="float:left">${member.full_name} (${_('owner') if c.pull_request.user_id == member.user_id else _('reviewer')})</div>
155 155 <input type="hidden" value="${member.user_id}" name="review_members" />
156 156 %if not c.pull_request.is_closed() and (h.HasPermissionAny('hg.admin', 'repository.admin')() or c.pull_request.user_id == c.rhodecode_user.user_id):
157 157 <span class="delete_icon action_button" onclick="removeReviewMember(${member.user_id})"></span>
158 158 %endif
159 159 </div>
160 160 </li>
161 161 %endfor
162 162 </ul>
163 163 </div>
164 164 %if not c.pull_request.is_closed():
165 165 <div class='ac'>
166 166 %if h.HasPermissionAny('hg.admin', 'repository.admin')() or c.pull_request.author.user_id == c.rhodecode_user.user_id:
167 167 <div class="reviewer_ac">
168 168 ${h.text('user', class_='yui-ac-input')}
169 169 <span class="help-block">${_('Add or remove reviewer to this pull request.')}</span>
170 170 <div id="reviewers_container"></div>
171 171 </div>
172 172 <div style="padding:0px 10px">
173 173 <span id="update_pull_request" class="ui-btn xsmall">${_('Save changes')}</span>
174 174 </div>
175 175 %endif
176 176 </div>
177 177 %endif
178 178 </div>
179 179 </div>
180 180 </div>
181 181 <script>
182 182 var _USERS_AC_DATA = ${c.users_array|n};
183 183 var _GROUPS_AC_DATA = ${c.users_groups_array|n};
184 184 // TODO: switch this to pyroutes
185 185 AJAX_COMMENT_URL = "${url('pullrequest_comment',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id)}";
186 186 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
187 187
188 188 pyroutes.register('pullrequest_comment', "${url('pullrequest_comment',repo_name='%(repo_name)s',pull_request_id='%(pull_request_id)s')}", ['repo_name', 'pull_request_id']);
189 189 pyroutes.register('pullrequest_comment_delete', "${url('pullrequest_comment_delete',repo_name='%(repo_name)s',comment_id='%(comment_id)s')}", ['repo_name', 'comment_id']);
190 190 pyroutes.register('pullrequest_update', "${url('pullrequest_update',repo_name='%(repo_name)s',pull_request_id='%(pull_request_id)s')}", ['repo_name', 'pull_request_id']);
191 191
192 192 </script>
193 193
194 194 ## diff block
195 195 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
196 196 %for fid, change, f, stat in c.files:
197 197 ${diff_block.diff_block_simple([c.changes[fid]])}
198 198 %endfor
199 199 % if c.limited_diff:
200 200 <h4>${_('Changeset was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}" onclick="return confirm('${_("Showing a huge diff might take some time and resources")}')">${_('Show full diff')}</a></h4>
201 201 % endif
202 202
203 203
204 204 ## template for inline comment form
205 205 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
206 206 ${comment.comment_inline_form()}
207 207
208 208 ## render comments and inlines
209 209 ${comment.generate_comments(include_pr=True)}
210 210
211 211 % if not c.pull_request.is_closed():
212 212 ## main comment form and it status
213 213 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
214 214 pull_request_id=c.pull_request.pull_request_id),
215 215 c.current_changeset_status,
216 216 is_pr=True, change_status=c.allowed_to_change_status)}
217 217 %endif
218 218
219 219 <script type="text/javascript">
220 220 YUE.onDOMReady(function(){
221 221 PullRequestAutoComplete('user', 'reviewers_container', _USERS_AC_DATA, _GROUPS_AC_DATA);
222 222
223 223 YUE.on(YUQ('.show-inline-comments'),'change',function(e){
224 224 var show = 'none';
225 225 var target = e.currentTarget;
226 226 if(target.checked){
227 227 var show = ''
228 228 }
229 229 var boxid = YUD.getAttribute(target,'id_for');
230 230 var comments = YUQ('#{0} .inline-comments'.format(boxid));
231 231 for(c in comments){
232 232 YUD.setStyle(comments[c],'display',show);
233 233 }
234 234 var btns = YUQ('#{0} .inline-comments-button'.format(boxid));
235 235 for(c in btns){
236 236 YUD.setStyle(btns[c],'display',show);
237 237 }
238 238 })
239 239
240 240 YUE.on(YUQ('.line'),'click',function(e){
241 241 var tr = e.currentTarget;
242 242 injectInlineForm(tr);
243 243 });
244 244
245 245 // inject comments into they proper positions
246 246 var file_comments = YUQ('.inline-comment-placeholder');
247 247 renderInlineComments(file_comments);
248 248
249 249 YUE.on(YUD.get('update_pull_request'),'click',function(e){
250 250 updateReviewers(undefined, "${c.repo_name}", "${c.pull_request.pull_request_id}");
251 251 })
252 252 })
253 253 </script>
254 254
255 255 </div>
256 256
257 257 </%def>
General Comments 0
You need to be logged in to leave comments. Login now