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