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