##// END OF EJS Templates
pull request: for consistency use org_repo when info is available in several places
Mads Kiilerich -
r3328:12c72fbd beta
parent child Browse files
Show More
@@ -1,484 +1,484 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 import helpers as h
42 42 from rhodecode.lib import diffs
43 43 from rhodecode.lib.utils import action_logger, jsonify
44 44 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError
45 45 from rhodecode.lib.vcs.backends.base import EmptyChangeset
46 46 from rhodecode.lib.diffs import LimitedDiffContainer
47 47 from rhodecode.model.db import User, PullRequest, ChangesetStatus,\
48 48 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
56 56 log = logging.getLogger(__name__)
57 57
58 58
59 59 class PullrequestsController(BaseRepoController):
60 60
61 61 @LoginRequired()
62 62 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
63 63 'repository.admin')
64 64 def __before__(self):
65 65 super(PullrequestsController, self).__before__()
66 66 repo_model = RepoModel()
67 67 c.users_array = repo_model.get_users_js()
68 68 c.users_groups_array = repo_model.get_users_groups_js()
69 69
70 70 def _get_repo_refs(self, repo):
71 71 hist_l = []
72 72
73 73 branches_group = ([('branch:%s:%s' % (k, v), k) for
74 74 k, v in repo.branches.iteritems()], _("Branches"))
75 75 bookmarks_group = ([('book:%s:%s' % (k, v), k) for
76 76 k, v in repo.bookmarks.iteritems()], _("Bookmarks"))
77 77 tags_group = ([('tag:%s:%s' % (k, v), k) for
78 78 k, v in repo.tags.iteritems()], _("Tags"))
79 79
80 80 hist_l.append(bookmarks_group)
81 81 hist_l.append(branches_group)
82 82 hist_l.append(tags_group)
83 83
84 84 return hist_l
85 85
86 86 def _get_default_rev(self, repo):
87 87 """
88 88 Get's default revision to do compare on pull request
89 89
90 90 :param repo:
91 91 """
92 92 repo = repo.scm_instance
93 93 if 'default' in repo.branches:
94 94 return 'default'
95 95 else:
96 96 #if repo doesn't have default branch return first found
97 97 return repo.branches.keys()[0]
98 98
99 99 def _get_is_allowed_change_status(self, pull_request):
100 100 owner = self.rhodecode_user.user_id == pull_request.user_id
101 101 reviewer = self.rhodecode_user.user_id in [x.user_id for x in
102 102 pull_request.reviewers]
103 103 return (self.rhodecode_user.admin or owner or reviewer)
104 104
105 105 def show_all(self, repo_name):
106 106 c.pull_requests = PullRequestModel().get_all(repo_name)
107 107 c.repo_name = repo_name
108 108 return render('/pullrequests/pullrequest_show_all.html')
109 109
110 110 @NotAnonymous()
111 111 def index(self):
112 112 org_repo = c.rhodecode_db_repo
113 113
114 114 if org_repo.scm_instance.alias != 'hg':
115 115 log.error('Review not available for GIT REPOS')
116 116 raise HTTPNotFound
117 117
118 118 try:
119 119 org_repo.scm_instance.get_changeset()
120 120 except EmptyRepositoryError, e:
121 121 h.flash(h.literal(_('There are no changesets yet')),
122 122 category='warning')
123 123 redirect(url('summary_home', repo_name=org_repo.repo_name))
124 124
125 125 other_repos_info = {}
126 126
127 127 c.org_repos = []
128 128 c.org_repos.append((org_repo.repo_name, '%s/%s' % (
129 org_repo.user.username, c.repo_name))
129 org_repo.user.username, org_repo.repo_name))
130 130 )
131 c.org_refs = self._get_repo_refs(c.rhodecode_repo)
131 c.org_refs = self._get_repo_refs(org_repo.scm_instance)
132 132
133 133 c.other_repos = []
134 134 # add org repo to other so we can open pull request against itself
135 135 c.other_repos.extend(c.org_repos)
136 136 c.default_other_repo = org_repo.repo_name
137 137 c.default_other_refs = self._get_repo_refs(org_repo.scm_instance)
138 138 c.default_other_ref = self._get_default_rev(org_repo)
139 139 other_repos_info[org_repo.repo_name] = {
140 140 'gravatar': h.gravatar_url(org_repo.user.email, 24),
141 141 'description': org_repo.description,
142 142 'revs': h.select('other_ref', '', c.default_other_refs, class_='refs')
143 143 }
144 144
145 145 # gather forks and add to this list ... even though it is rare to request forks to pull their parent
146 146 for fork in org_repo.forks:
147 147 c.other_repos.append((fork.repo_name, '%s/%s' % (
148 148 fork.user.username, fork.repo_name))
149 149 )
150 150 other_repos_info[fork.repo_name] = {
151 151 'gravatar': h.gravatar_url(fork.user.email, 24),
152 152 'description': fork.description,
153 153 'revs': h.select('other_ref', '',
154 154 self._get_repo_refs(fork.scm_instance),
155 155 class_='refs')
156 156 }
157 157
158 158 # add parents of this fork also, but only if it's not empty
159 159 if org_repo.parent and org_repo.parent.scm_instance.revisions:
160 160 c.default_other_repo = org_repo.parent.repo_name
161 161 c.default_other_refs = self._get_repo_refs(org_repo.parent.scm_instance)
162 162 c.default_other_ref = self._get_default_rev(org_repo.parent)
163 163 c.other_repos.append((org_repo.parent.repo_name, '%s/%s' % (
164 164 org_repo.parent.user.username,
165 165 org_repo.parent.repo_name))
166 166 )
167 167 other_repos_info[org_repo.parent.repo_name] = {
168 168 'gravatar': h.gravatar_url(org_repo.parent.user.email, 24),
169 169 'description': org_repo.parent.description,
170 170 'revs': h.select('other_ref', '',
171 171 self._get_repo_refs(org_repo.parent.scm_instance),
172 172 class_='refs')
173 173 }
174 174
175 175 c.other_repos_info = json.dumps(other_repos_info)
176 176 c.review_members = [org_repo.user]
177 177 return render('/pullrequests/pullrequest.html')
178 178
179 179 @NotAnonymous()
180 180 def create(self, repo_name):
181 181 repo = RepoModel()._get_repo(repo_name)
182 182 try:
183 183 _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
184 184 except formencode.Invalid, errors:
185 185 log.error(traceback.format_exc())
186 186 if errors.error_dict.get('revisions'):
187 187 msg = 'Revisions: %s' % errors.error_dict['revisions']
188 188 elif errors.error_dict.get('pullrequest_title'):
189 189 msg = _('Pull request requires a title with min. 3 chars')
190 190 else:
191 191 msg = _('error during creation of pull request')
192 192
193 193 h.flash(msg, 'error')
194 194 return redirect(url('pullrequest_home', repo_name=repo_name))
195 195
196 196 org_repo = _form['org_repo']
197 197 org_ref = _form['org_ref']
198 198 other_repo = _form['other_repo']
199 199 other_ref = _form['other_ref']
200 200 revisions = _form['revisions']
201 201 reviewers = _form['review_members']
202 202
203 203 # if we have cherry picked pull request we don't care what is in
204 204 # org_ref/other_ref
205 205 rev_start = request.POST.get('rev_start')
206 206 rev_end = request.POST.get('rev_end')
207 207
208 208 if rev_start and rev_end:
209 209 # this is swapped to simulate that rev_end is a revision from
210 210 # parent of the fork
211 211 org_ref = 'rev:%s:%s' % (rev_end, rev_end)
212 212 other_ref = 'rev:%s:%s' % (rev_start, rev_start)
213 213
214 214 title = _form['pullrequest_title']
215 215 description = _form['pullrequest_desc']
216 216
217 217 try:
218 218 pull_request = PullRequestModel().create(
219 219 self.rhodecode_user.user_id, org_repo, org_ref, other_repo,
220 220 other_ref, revisions, reviewers, title, description
221 221 )
222 222 Session().commit()
223 223 h.flash(_('Successfully opened new pull request'),
224 224 category='success')
225 225 except Exception:
226 226 h.flash(_('Error occurred during sending pull request'),
227 227 category='error')
228 228 log.error(traceback.format_exc())
229 229 return redirect(url('pullrequest_home', repo_name=repo_name))
230 230
231 231 return redirect(url('pullrequest_show', repo_name=other_repo,
232 232 pull_request_id=pull_request.pull_request_id))
233 233
234 234 @NotAnonymous()
235 235 @jsonify
236 236 def update(self, repo_name, pull_request_id):
237 237 pull_request = PullRequest.get_or_404(pull_request_id)
238 238 if pull_request.is_closed():
239 239 raise HTTPForbidden()
240 240 #only owner or admin can update it
241 241 owner = pull_request.author.user_id == c.rhodecode_user.user_id
242 242 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
243 243 reviewers_ids = map(int, filter(lambda v: v not in [None, ''],
244 244 request.POST.get('reviewers_ids', '').split(',')))
245 245
246 246 PullRequestModel().update_reviewers(pull_request_id, reviewers_ids)
247 247 Session().commit()
248 248 return True
249 249 raise HTTPForbidden()
250 250
251 251 @NotAnonymous()
252 252 @jsonify
253 253 def delete(self, repo_name, pull_request_id):
254 254 pull_request = PullRequest.get_or_404(pull_request_id)
255 255 #only owner can delete it !
256 256 if pull_request.author.user_id == c.rhodecode_user.user_id:
257 257 PullRequestModel().delete(pull_request)
258 258 Session().commit()
259 259 h.flash(_('Successfully deleted pull request'),
260 260 category='success')
261 261 return redirect(url('admin_settings_my_account', anchor='pullrequests'))
262 262 raise HTTPForbidden()
263 263
264 264 def _load_compare_data(self, pull_request, enable_comments=True):
265 265 """
266 266 Load context data needed for generating compare diff
267 267
268 268 :param pull_request:
269 269 :type pull_request:
270 270 """
271 271 rev_start = request.GET.get('rev_start')
272 272 rev_end = request.GET.get('rev_end')
273 273
274 274 org_repo = pull_request.org_repo
275 275 (org_ref_type,
276 276 org_ref_name,
277 277 org_ref_rev) = pull_request.org_ref.split(':')
278 278
279 279 other_repo = org_repo
280 280 (other_ref_type,
281 281 other_ref_name,
282 282 other_ref_rev) = pull_request.other_ref.split(':')
283 283
284 284 # despite opening revisions for bookmarks/branches/tags, we always
285 285 # convert this to rev to prevent changes after book or branch change
286 286 org_ref = ('rev', org_ref_rev)
287 287 other_ref = ('rev', other_ref_rev)
288 288
289 289 c.org_repo = org_repo
290 290 c.other_repo = other_repo
291 291
292 292 c.fulldiff = fulldiff = request.GET.get('fulldiff')
293 293
294 294 c.cs_ranges = [org_repo.get_changeset(x) for x in pull_request.revisions]
295 295
296 296 other_ref = ('rev', getattr(c.cs_ranges[0].parents[0]
297 297 if c.cs_ranges[0].parents
298 298 else EmptyChangeset(), 'raw_id'))
299 299
300 300 c.statuses = org_repo.statuses([x.raw_id for x in c.cs_ranges])
301 301 # defines that we need hidden inputs with changesets
302 302 c.as_form = request.GET.get('as_form', False)
303 303
304 304 c.org_ref = org_ref[1]
305 305 c.other_ref = other_ref[1]
306 306
307 307 diff_limit = self.cut_off_limit if not fulldiff else None
308 308
309 309 #we swap org/other ref since we run a simple diff on one repo
310 310 _diff = diffs.differ(org_repo, other_ref, other_repo, org_ref)
311 311
312 312 diff_processor = diffs.DiffProcessor(_diff or '', format='gitdiff',
313 313 diff_limit=diff_limit)
314 314 _parsed = diff_processor.prepare()
315 315
316 316 c.limited_diff = False
317 317 if isinstance(_parsed, LimitedDiffContainer):
318 318 c.limited_diff = True
319 319
320 320 c.files = []
321 321 c.changes = {}
322 322 c.lines_added = 0
323 323 c.lines_deleted = 0
324 324 for f in _parsed:
325 325 st = f['stats']
326 326 if st[0] != 'b':
327 327 c.lines_added += st[0]
328 328 c.lines_deleted += st[1]
329 329 fid = h.FID('', f['filename'])
330 330 c.files.append([fid, f['operation'], f['filename'], f['stats']])
331 331 diff = diff_processor.as_html(enable_comments=enable_comments,
332 332 parsed_lines=[f])
333 333 c.changes[fid] = [f['operation'], f['filename'], diff]
334 334
335 335 def show(self, repo_name, pull_request_id):
336 336 repo_model = RepoModel()
337 337 c.users_array = repo_model.get_users_js()
338 338 c.users_groups_array = repo_model.get_users_groups_js()
339 339 c.pull_request = PullRequest.get_or_404(pull_request_id)
340 340 c.allowed_to_change_status = self._get_is_allowed_change_status(c.pull_request)
341 341 cc_model = ChangesetCommentsModel()
342 342 cs_model = ChangesetStatusModel()
343 343 _cs_statuses = cs_model.get_statuses(c.pull_request.org_repo,
344 344 pull_request=c.pull_request,
345 345 with_revisions=True)
346 346
347 347 cs_statuses = defaultdict(list)
348 348 for st in _cs_statuses:
349 349 cs_statuses[st.author.username] += [st]
350 350
351 351 c.pull_request_reviewers = []
352 352 c.pull_request_pending_reviewers = []
353 353 for o in c.pull_request.reviewers:
354 354 st = cs_statuses.get(o.user.username, None)
355 355 if st:
356 356 sorter = lambda k: k.version
357 357 st = [(x, list(y)[0])
358 358 for x, y in (groupby(sorted(st, key=sorter), sorter))]
359 359 else:
360 360 c.pull_request_pending_reviewers.append(o.user)
361 361 c.pull_request_reviewers.append([o.user, st])
362 362
363 363 # pull_requests repo_name we opened it against
364 364 # ie. other_repo must match
365 365 if repo_name != c.pull_request.other_repo.repo_name:
366 366 raise HTTPNotFound
367 367
368 368 # load compare data into template context
369 369 enable_comments = not c.pull_request.is_closed()
370 370 self._load_compare_data(c.pull_request, enable_comments=enable_comments)
371 371
372 372 # inline comments
373 373 c.inline_cnt = 0
374 374 c.inline_comments = cc_model.get_inline_comments(
375 375 c.rhodecode_db_repo.repo_id,
376 376 pull_request=pull_request_id)
377 377 # count inline comments
378 378 for __, lines in c.inline_comments:
379 379 for comments in lines.values():
380 380 c.inline_cnt += len(comments)
381 381 # comments
382 382 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
383 383 pull_request=pull_request_id)
384 384
385 385 try:
386 386 cur_status = c.statuses[c.pull_request.revisions[0]][0]
387 387 except:
388 388 log.error(traceback.format_exc())
389 389 cur_status = 'undefined'
390 390 if c.pull_request.is_closed() and 0:
391 391 c.current_changeset_status = cur_status
392 392 else:
393 393 # changeset(pull-request) status calulation based on reviewers
394 394 c.current_changeset_status = cs_model.calculate_status(
395 395 c.pull_request_reviewers,
396 396 )
397 397 c.changeset_statuses = ChangesetStatus.STATUSES
398 398
399 399 return render('/pullrequests/pullrequest_show.html')
400 400
401 401 @NotAnonymous()
402 402 @jsonify
403 403 def comment(self, repo_name, pull_request_id):
404 404 pull_request = PullRequest.get_or_404(pull_request_id)
405 405 if pull_request.is_closed():
406 406 raise HTTPForbidden()
407 407
408 408 status = request.POST.get('changeset_status')
409 409 change_status = request.POST.get('change_changeset_status')
410 410 text = request.POST.get('text')
411 411
412 412 allowed_to_change_status = self._get_is_allowed_change_status(pull_request)
413 413 if status and change_status and allowed_to_change_status:
414 414 text = text or (_('Status change -> %s')
415 415 % ChangesetStatus.get_status_lbl(status))
416 416 comm = ChangesetCommentsModel().create(
417 417 text=text,
418 418 repo=c.rhodecode_db_repo.repo_id,
419 419 user=c.rhodecode_user.user_id,
420 420 pull_request=pull_request_id,
421 421 f_path=request.POST.get('f_path'),
422 422 line_no=request.POST.get('line'),
423 423 status_change=(ChangesetStatus.get_status_lbl(status)
424 424 if status and change_status and allowed_to_change_status else None)
425 425 )
426 426
427 427 action_logger(self.rhodecode_user,
428 428 'user_commented_pull_request:%s' % pull_request_id,
429 429 c.rhodecode_db_repo, self.ip_addr, self.sa)
430 430
431 431 if allowed_to_change_status:
432 432 # get status if set !
433 433 if status and change_status:
434 434 ChangesetStatusModel().set_status(
435 435 c.rhodecode_db_repo.repo_id,
436 436 status,
437 437 c.rhodecode_user.user_id,
438 438 comm,
439 439 pull_request=pull_request_id
440 440 )
441 441
442 442 if request.POST.get('save_close'):
443 443 if status in ['rejected', 'approved']:
444 444 PullRequestModel().close_pull_request(pull_request_id)
445 445 action_logger(self.rhodecode_user,
446 446 'user_closed_pull_request:%s' % pull_request_id,
447 447 c.rhodecode_db_repo, self.ip_addr, self.sa)
448 448 else:
449 449 h.flash(_('Closing pull request on other statuses than '
450 450 'rejected or approved forbidden'),
451 451 category='warning')
452 452
453 453 Session().commit()
454 454
455 455 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
456 456 return redirect(h.url('pullrequest_show', repo_name=repo_name,
457 457 pull_request_id=pull_request_id))
458 458
459 459 data = {
460 460 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
461 461 }
462 462 if comm:
463 463 c.co = comm
464 464 data.update(comm.get_dict())
465 465 data.update({'rendered_text':
466 466 render('changeset/changeset_comment_block.html')})
467 467
468 468 return data
469 469
470 470 @NotAnonymous()
471 471 @jsonify
472 472 def delete_comment(self, repo_name, comment_id):
473 473 co = ChangesetComment.get(comment_id)
474 474 if co.pull_request.is_closed():
475 475 #don't allow deleting comments on closed pull request
476 476 raise HTTPForbidden()
477 477
478 478 owner = co.author.user_id == c.rhodecode_user.user_id
479 479 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
480 480 ChangesetCommentsModel().delete(comment=co)
481 481 Session().commit()
482 482 return True
483 483 else:
484 484 raise HTTPForbidden()
General Comments 0
You need to be logged in to leave comments. Login now