##// END OF EJS Templates
protect agains pull requests on empty repositories
marcink -
r2874:95923493 beta
parent child Browse files
Show More
@@ -1,431 +1,439 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 from pylons.decorators import jsonify
37 37
38 38 from rhodecode.lib.compat import json
39 39 from rhodecode.lib.base import BaseRepoController, render
40 40 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator,\
41 41 NotAnonymous
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
45 45 from rhodecode.model.db import User, PullRequest, ChangesetStatus,\
46 46 ChangesetComment
47 47 from rhodecode.model.pull_request import PullRequestModel
48 48 from rhodecode.model.meta import Session
49 49 from rhodecode.model.repo import RepoModel
50 50 from rhodecode.model.comment import ChangesetCommentsModel
51 51 from rhodecode.model.changeset_status import ChangesetStatusModel
52 52 from rhodecode.model.forms import PullRequestForm
53 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError
53 54
54 55 log = logging.getLogger(__name__)
55 56
56 57
57 58 class PullrequestsController(BaseRepoController):
58 59
59 60 @LoginRequired()
60 61 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
61 62 'repository.admin')
62 63 def __before__(self):
63 64 super(PullrequestsController, self).__before__()
64 65 repo_model = RepoModel()
65 66 c.users_array = repo_model.get_users_js()
66 67 c.users_groups_array = repo_model.get_users_groups_js()
67 68
68 69 def _get_repo_refs(self, repo):
69 70 hist_l = []
70 71
71 72 branches_group = ([('branch:%s:%s' % (k, v), k) for
72 73 k, v in repo.branches.iteritems()], _("Branches"))
73 74 bookmarks_group = ([('book:%s:%s' % (k, v), k) for
74 75 k, v in repo.bookmarks.iteritems()], _("Bookmarks"))
75 76 tags_group = ([('tag:%s:%s' % (k, v), k) for
76 77 k, v in repo.tags.iteritems()], _("Tags"))
77 78
78 79 hist_l.append(bookmarks_group)
79 80 hist_l.append(branches_group)
80 81 hist_l.append(tags_group)
81 82
82 83 return hist_l
83 84
84 85 def _get_default_rev(self, repo):
85 86 """
86 87 Get's default revision to do compare on pull request
87 88
88 89 :param repo:
89 90 """
90 91 repo = repo.scm_instance
91 92 if 'default' in repo.branches:
92 93 return 'default'
93 94 else:
94 95 #if repo doesn't have default branch return first found
95 96 return repo.branches.keys()[0]
96 97
97 98 def show_all(self, repo_name):
98 99 c.pull_requests = PullRequestModel().get_all(repo_name)
99 100 c.repo_name = repo_name
100 101 return render('/pullrequests/pullrequest_show_all.html')
101 102
102 103 @NotAnonymous()
103 104 def index(self):
104 105 org_repo = c.rhodecode_db_repo
105 106
106 107 if org_repo.scm_instance.alias != 'hg':
107 108 log.error('Review not available for GIT REPOS')
108 109 raise HTTPNotFound
109 110
111 try:
112 org_repo.scm_instance.get_changeset()
113 except EmptyRepositoryError, e:
114 h.flash(h.literal(_('There are no changesets yet')),
115 category='warning')
116 redirect(url('summary_home', repo_name=org_repo.repo_name))
117
110 118 other_repos_info = {}
111 119
112 120 c.org_refs = self._get_repo_refs(c.rhodecode_repo)
113 121 c.org_repos = []
114 122 c.other_repos = []
115 123 c.org_repos.append((org_repo.repo_name, '%s/%s' % (
116 124 org_repo.user.username, c.repo_name))
117 125 )
118 126
119 127 # add org repo to other so we can open pull request agains itself
120 128 c.other_repos.extend(c.org_repos)
121 129
122 130 c.default_pull_request = org_repo.repo_name # repo name pre-selected
123 131 c.default_pull_request_rev = self._get_default_rev(org_repo) # revision pre-selected
124 132 c.default_revs = self._get_repo_refs(org_repo.scm_instance)
125 133 #add orginal repo
126 134 other_repos_info[org_repo.repo_name] = {
127 135 'gravatar': h.gravatar_url(org_repo.user.email, 24),
128 136 'description': org_repo.description,
129 137 'revs': h.select('other_ref', '', c.default_revs, class_='refs')
130 138 }
131 139
132 140 #gather forks and add to this list
133 141 for fork in org_repo.forks:
134 142 c.other_repos.append((fork.repo_name, '%s/%s' % (
135 143 fork.user.username, fork.repo_name))
136 144 )
137 145 other_repos_info[fork.repo_name] = {
138 146 'gravatar': h.gravatar_url(fork.user.email, 24),
139 147 'description': fork.description,
140 148 'revs': h.select('other_ref', '',
141 149 self._get_repo_refs(fork.scm_instance),
142 150 class_='refs')
143 151 }
144 152 #add parents of this fork also
145 153 if org_repo.parent:
146 154 c.default_pull_request = org_repo.parent.repo_name
147 155 c.default_pull_request_rev = self._get_default_rev(org_repo.parent)
148 156 c.default_revs = self._get_repo_refs(org_repo.parent.scm_instance)
149 157 c.other_repos.append((org_repo.parent.repo_name, '%s/%s' % (
150 158 org_repo.parent.user.username,
151 159 org_repo.parent.repo_name))
152 160 )
153 161 other_repos_info[org_repo.parent.repo_name] = {
154 162 'gravatar': h.gravatar_url(org_repo.parent.user.email, 24),
155 163 'description': org_repo.parent.description,
156 164 'revs': h.select('other_ref', '',
157 165 self._get_repo_refs(org_repo.parent.scm_instance),
158 166 class_='refs')
159 167 }
160 168
161 169 c.other_repos_info = json.dumps(other_repos_info)
162 170 c.review_members = [org_repo.user]
163 171 return render('/pullrequests/pullrequest.html')
164 172
165 173 @NotAnonymous()
166 174 def create(self, repo_name):
167 175 try:
168 176 _form = PullRequestForm()().to_python(request.POST)
169 177 except formencode.Invalid, errors:
170 178 log.error(traceback.format_exc())
171 179 if errors.error_dict.get('revisions'):
172 180 msg = 'Revisions: %s' % errors.error_dict['revisions']
173 181 elif errors.error_dict.get('pullrequest_title'):
174 182 msg = _('Pull request requires a title with min. 3 chars')
175 183 else:
176 184 msg = _('error during creation of pull request')
177 185
178 186 h.flash(msg, 'error')
179 187 return redirect(url('pullrequest_home', repo_name=repo_name))
180 188
181 189 org_repo = _form['org_repo']
182 190 org_ref = _form['org_ref']
183 191 other_repo = _form['other_repo']
184 192 other_ref = _form['other_ref']
185 193 revisions = _form['revisions']
186 194 reviewers = _form['review_members']
187 195
188 196 title = _form['pullrequest_title']
189 197 description = _form['pullrequest_desc']
190 198
191 199 try:
192 200 pull_request = PullRequestModel().create(
193 201 self.rhodecode_user.user_id, org_repo, org_ref, other_repo,
194 202 other_ref, revisions, reviewers, title, description
195 203 )
196 204 Session().commit()
197 205 h.flash(_('Successfully opened new pull request'),
198 206 category='success')
199 207 except Exception:
200 208 h.flash(_('Error occurred during sending pull request'),
201 209 category='error')
202 210 log.error(traceback.format_exc())
203 211 return redirect(url('pullrequest_home', repo_name=repo_name))
204 212
205 213 return redirect(url('pullrequest_show', repo_name=other_repo,
206 214 pull_request_id=pull_request.pull_request_id))
207 215
208 216 @NotAnonymous()
209 217 @jsonify
210 218 def update(self, repo_name, pull_request_id):
211 219 pull_request = PullRequest.get_or_404(pull_request_id)
212 220 if pull_request.is_closed():
213 221 raise HTTPForbidden()
214 222 #only owner or admin can update it
215 223 owner = pull_request.author.user_id == c.rhodecode_user.user_id
216 224 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
217 225 reviewers_ids = map(int, filter(lambda v: v not in [None, ''],
218 226 request.POST.get('reviewers_ids', '').split(',')))
219 227
220 228 PullRequestModel().update_reviewers(pull_request_id, reviewers_ids)
221 229 Session.commit()
222 230 return True
223 231 raise HTTPForbidden()
224 232
225 233 @NotAnonymous()
226 234 @jsonify
227 235 def delete(self, repo_name, pull_request_id):
228 236 pull_request = PullRequest.get_or_404(pull_request_id)
229 237 #only owner can delete it !
230 238 if pull_request.author.user_id == c.rhodecode_user.user_id:
231 239 PullRequestModel().delete(pull_request)
232 240 Session().commit()
233 241 h.flash(_('Successfully deleted pull request'),
234 242 category='success')
235 243 return redirect(url('admin_settings_my_account'))
236 244 raise HTTPForbidden()
237 245
238 246 def _load_compare_data(self, pull_request, enable_comments=True):
239 247 """
240 248 Load context data needed for generating compare diff
241 249
242 250 :param pull_request:
243 251 :type pull_request:
244 252 """
245 253
246 254 org_repo = pull_request.org_repo
247 255 (org_ref_type,
248 256 org_ref_name,
249 257 org_ref_rev) = pull_request.org_ref.split(':')
250 258
251 259 other_repo = pull_request.other_repo
252 260 (other_ref_type,
253 261 other_ref_name,
254 262 other_ref_rev) = pull_request.other_ref.split(':')
255 263
256 264 # despite opening revisions for bookmarks/branches/tags, we always
257 265 # convert this to rev to prevent changes after book or branch change
258 266 org_ref = ('rev', org_ref_rev)
259 267 other_ref = ('rev', other_ref_rev)
260 268
261 269 c.org_repo = org_repo
262 270 c.other_repo = other_repo
263 271
264 272 c.cs_ranges, discovery_data = PullRequestModel().get_compare_data(
265 273 org_repo, org_ref, other_repo, other_ref
266 274 )
267 275
268 276 c.statuses = org_repo.statuses([x.raw_id for x in c.cs_ranges])
269 277 # defines that we need hidden inputs with changesets
270 278 c.as_form = request.GET.get('as_form', False)
271 279
272 280 c.org_ref = org_ref[1]
273 281 c.other_ref = other_ref[1]
274 282 # diff needs to have swapped org with other to generate proper diff
275 283 _diff = diffs.differ(other_repo, other_ref, org_repo, org_ref,
276 284 discovery_data)
277 285 diff_processor = diffs.DiffProcessor(_diff, format='gitdiff')
278 286 _parsed = diff_processor.prepare()
279 287
280 288 c.files = []
281 289 c.changes = {}
282 290
283 291 for f in _parsed:
284 292 fid = h.FID('', f['filename'])
285 293 c.files.append([fid, f['operation'], f['filename'], f['stats']])
286 294 diff = diff_processor.as_html(enable_comments=enable_comments,
287 295 diff_lines=[f])
288 296 c.changes[fid] = [f['operation'], f['filename'], diff]
289 297
290 298 def show(self, repo_name, pull_request_id):
291 299 repo_model = RepoModel()
292 300 c.users_array = repo_model.get_users_js()
293 301 c.users_groups_array = repo_model.get_users_groups_js()
294 302 c.pull_request = PullRequest.get_or_404(pull_request_id)
295 303 c.target_repo = c.pull_request.org_repo.repo_name
296 304
297 305 cc_model = ChangesetCommentsModel()
298 306 cs_model = ChangesetStatusModel()
299 307 _cs_statuses = cs_model.get_statuses(c.pull_request.org_repo,
300 308 pull_request=c.pull_request,
301 309 with_revisions=True)
302 310
303 311 cs_statuses = defaultdict(list)
304 312 for st in _cs_statuses:
305 313 cs_statuses[st.author.username] += [st]
306 314
307 315 c.pull_request_reviewers = []
308 316 c.pull_request_pending_reviewers = []
309 317 for o in c.pull_request.reviewers:
310 318 st = cs_statuses.get(o.user.username, None)
311 319 if st:
312 320 sorter = lambda k: k.version
313 321 st = [(x, list(y)[0])
314 322 for x, y in (groupby(sorted(st, key=sorter), sorter))]
315 323 else:
316 324 c.pull_request_pending_reviewers.append(o.user)
317 325 c.pull_request_reviewers.append([o.user, st])
318 326
319 327 # pull_requests repo_name we opened it against
320 328 # ie. other_repo must match
321 329 if repo_name != c.pull_request.other_repo.repo_name:
322 330 raise HTTPNotFound
323 331
324 332 # load compare data into template context
325 333 enable_comments = not c.pull_request.is_closed()
326 334 self._load_compare_data(c.pull_request, enable_comments=enable_comments)
327 335
328 336 # inline comments
329 337 c.inline_cnt = 0
330 338 c.inline_comments = cc_model.get_inline_comments(
331 339 c.rhodecode_db_repo.repo_id,
332 340 pull_request=pull_request_id)
333 341 # count inline comments
334 342 for __, lines in c.inline_comments:
335 343 for comments in lines.values():
336 344 c.inline_cnt += len(comments)
337 345 # comments
338 346 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
339 347 pull_request=pull_request_id)
340 348
341 349 try:
342 350 cur_status = c.statuses[c.pull_request.revisions[0]][0]
343 351 except:
344 352 log.error(traceback.format_exc())
345 353 cur_status = 'undefined'
346 354 if c.pull_request.is_closed() and 0:
347 355 c.current_changeset_status = cur_status
348 356 else:
349 357 # changeset(pull-request) status calulation based on reviewers
350 358 c.current_changeset_status = cs_model.calculate_status(
351 359 c.pull_request_reviewers,
352 360 )
353 361 c.changeset_statuses = ChangesetStatus.STATUSES
354 362
355 363 return render('/pullrequests/pullrequest_show.html')
356 364
357 365 @NotAnonymous()
358 366 @jsonify
359 367 def comment(self, repo_name, pull_request_id):
360 368 pull_request = PullRequest.get_or_404(pull_request_id)
361 369 if pull_request.is_closed():
362 370 raise HTTPForbidden()
363 371
364 372 status = request.POST.get('changeset_status')
365 373 change_status = request.POST.get('change_changeset_status')
366 374 text = request.POST.get('text')
367 375 if status and change_status:
368 376 text = text or (_('Status change -> %s')
369 377 % ChangesetStatus.get_status_lbl(status))
370 378 comm = ChangesetCommentsModel().create(
371 379 text=text,
372 380 repo=c.rhodecode_db_repo.repo_id,
373 381 user=c.rhodecode_user.user_id,
374 382 pull_request=pull_request_id,
375 383 f_path=request.POST.get('f_path'),
376 384 line_no=request.POST.get('line'),
377 385 status_change=(ChangesetStatus.get_status_lbl(status)
378 386 if status and change_status else None)
379 387 )
380 388
381 389 # get status if set !
382 390 if status and change_status:
383 391 ChangesetStatusModel().set_status(
384 392 c.rhodecode_db_repo.repo_id,
385 393 status,
386 394 c.rhodecode_user.user_id,
387 395 comm,
388 396 pull_request=pull_request_id
389 397 )
390 398 action_logger(self.rhodecode_user,
391 399 'user_commented_pull_request:%s' % pull_request_id,
392 400 c.rhodecode_db_repo, self.ip_addr, self.sa)
393 401
394 402 if request.POST.get('save_close'):
395 403 PullRequestModel().close_pull_request(pull_request_id)
396 404 action_logger(self.rhodecode_user,
397 405 'user_closed_pull_request:%s' % pull_request_id,
398 406 c.rhodecode_db_repo, self.ip_addr, self.sa)
399 407
400 408 Session().commit()
401 409
402 410 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
403 411 return redirect(h.url('pullrequest_show', repo_name=repo_name,
404 412 pull_request_id=pull_request_id))
405 413
406 414 data = {
407 415 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
408 416 }
409 417 if comm:
410 418 c.co = comm
411 419 data.update(comm.get_dict())
412 420 data.update({'rendered_text':
413 421 render('changeset/changeset_comment_block.html')})
414 422
415 423 return data
416 424
417 425 @NotAnonymous()
418 426 @jsonify
419 427 def delete_comment(self, repo_name, comment_id):
420 428 co = ChangesetComment.get(comment_id)
421 429 if co.pull_request.is_closed():
422 430 #don't allow deleting comments on closed pull request
423 431 raise HTTPForbidden()
424 432
425 433 owner = lambda: co.author.user_id == c.rhodecode_user.user_id
426 434 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
427 435 ChangesetCommentsModel().delete(comment=co)
428 436 Session().commit()
429 437 return True
430 438 else:
431 439 raise HTTPForbidden()
General Comments 0
You need to be logged in to leave comments. Login now