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