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