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