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