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