##// END OF EJS Templates
pull requests: the node for null is not '0' but 40 * '0'...
Mads Kiilerich -
r4555:0ece1a4c default
parent child Browse files
Show More
@@ -1,763 +1,763 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.controllers.pullrequests
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 18 pull requests controller for Kallithea for initializing pull requests
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: May 7, 2012
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28 import logging
29 29 import traceback
30 30 import formencode
31 31 import re
32 32
33 33 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
34 34
35 35 from pylons import request, tmpl_context as c, url
36 36 from pylons.controllers.util import redirect
37 37 from pylons.i18n.translation import _
38 38
39 39 from kallithea.lib.compat import json
40 40 from kallithea.lib.base import BaseRepoController, render
41 41 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator,\
42 42 NotAnonymous
43 43 from kallithea.lib.helpers import Page
44 44 from kallithea.lib import helpers as h
45 45 from kallithea.lib import diffs
46 46 from kallithea.lib.utils import action_logger, jsonify
47 47 from kallithea.lib.vcs.utils import safe_str
48 48 from kallithea.lib.vcs.exceptions import EmptyRepositoryError
49 49 from kallithea.lib.diffs import LimitedDiffContainer
50 50 from kallithea.model.db import PullRequest, ChangesetStatus, ChangesetComment,\
51 51 PullRequestReviewers
52 52 from kallithea.model.pull_request import PullRequestModel
53 53 from kallithea.model.meta import Session
54 54 from kallithea.model.repo import RepoModel
55 55 from kallithea.model.comment import ChangesetCommentsModel
56 56 from kallithea.model.changeset_status import ChangesetStatusModel
57 57 from kallithea.model.forms import PullRequestForm, PullRequestPostForm
58 58 from kallithea.lib.utils2 import safe_int
59 59 from kallithea.controllers.changeset import _ignorews_url,\
60 60 _context_url, get_line_ctx, get_ignore_ws
61 61 from kallithea.controllers.compare import CompareController
62 62 from kallithea.lib.graphmod import graph_data
63 63
64 64 log = logging.getLogger(__name__)
65 65
66 66
67 67 class PullrequestsController(BaseRepoController):
68 68
69 69 def __before__(self):
70 70 super(PullrequestsController, self).__before__()
71 71 repo_model = RepoModel()
72 72 c.users_array = repo_model.get_users_js()
73 73 c.user_groups_array = repo_model.get_user_groups_js()
74 74
75 75 def _get_repo_refs(self, repo, rev=None, branch=None, branch_rev=None):
76 76 """return a structure with repo's interesting changesets, suitable for
77 77 the selectors in pullrequest.html
78 78
79 79 rev: a revision that must be in the list somehow and selected by default
80 80 branch: a branch that must be in the list and selected by default - even if closed
81 81 branch_rev: a revision of which peers should be preferred and available."""
82 82 # list named branches that has been merged to this named branch - it should probably merge back
83 83 peers = []
84 84
85 85 if rev:
86 86 rev = safe_str(rev)
87 87
88 88 if branch:
89 89 branch = safe_str(branch)
90 90
91 91 if branch_rev:
92 92 branch_rev = safe_str(branch_rev)
93 93 # a revset not restricting to merge() would be better
94 94 # (especially because it would get the branch point)
95 95 # ... but is currently too expensive
96 96 # including branches of children could be nice too
97 97 peerbranches = set()
98 98 for i in repo._repo.revs(
99 99 "sort(parents(branch(id(%s)) and merge()) - branch(id(%s)), -rev)",
100 100 branch_rev, branch_rev):
101 101 abranch = repo.get_changeset(i).branch
102 102 if abranch not in peerbranches:
103 103 n = 'branch:%s:%s' % (abranch, repo.get_changeset(abranch).raw_id)
104 104 peers.append((n, abranch))
105 105 peerbranches.add(abranch)
106 106
107 107 selected = None
108 108 tiprev = repo.tags.get('tip')
109 109 tipbranch = None
110 110
111 111 branches = []
112 112 for abranch, branchrev in repo.branches.iteritems():
113 113 n = 'branch:%s:%s' % (abranch, branchrev)
114 114 desc = abranch
115 115 if branchrev == tiprev:
116 116 tipbranch = abranch
117 117 desc = '%s (current tip)' % desc
118 118 branches.append((n, desc))
119 119 if rev == branchrev:
120 120 selected = n
121 121 if branch == abranch:
122 122 if not rev:
123 123 selected = n
124 124 branch = None
125 125 if branch: # branch not in list - it is probably closed
126 126 branchrev = repo.closed_branches.get(branch)
127 127 if branchrev:
128 128 n = 'branch:%s:%s' % (branch, branchrev)
129 129 branches.append((n, _('%s (closed)') % branch))
130 130 selected = n
131 131 branch = None
132 132 if branch:
133 133 log.error('branch %r not found in %s', branch, repo)
134 134
135 135 bookmarks = []
136 136 for bookmark, bookmarkrev in repo.bookmarks.iteritems():
137 137 n = 'book:%s:%s' % (bookmark, bookmarkrev)
138 138 bookmarks.append((n, bookmark))
139 139 if rev == bookmarkrev:
140 140 selected = n
141 141
142 142 tags = []
143 143 for tag, tagrev in repo.tags.iteritems():
144 144 if tag == 'tip':
145 145 continue
146 146 n = 'tag:%s:%s' % (tag, tagrev)
147 147 tags.append((n, tag))
148 148 if rev == tagrev:
149 149 selected = n
150 150
151 151 # prio 1: rev was selected as existing entry above
152 152
153 153 # prio 2: create special entry for rev; rev _must_ be used
154 154 specials = []
155 155 if rev and selected is None:
156 156 selected = 'rev:%s:%s' % (rev, rev)
157 157 specials = [(selected, '%s: %s' % (_("Changeset"), rev[:12]))]
158 158
159 159 # prio 3: most recent peer branch
160 160 if peers and not selected:
161 161 selected = peers[0][0]
162 162
163 163 # prio 4: tip revision
164 164 if not selected:
165 165 if h.is_hg(repo):
166 166 if tipbranch:
167 167 selected = 'branch:%s:%s' % (tipbranch, tiprev)
168 168 else:
169 selected = 'tag:null:0'
169 selected = 'tag:null:' + repo.EMPTY_CHANGESET
170 170 tags.append((selected, 'null'))
171 171 else:
172 172 if 'master' in repo.branches:
173 173 selected = 'branch:master:%s' % repo.branches['master']
174 174 else:
175 175 k, v = repo.branches.items()[0]
176 176 selected = 'branch:%s:%s' % (k, v)
177 177
178 178 groups = [(specials, _("Special")),
179 179 (peers, _("Peer branches")),
180 180 (bookmarks, _("Bookmarks")),
181 181 (branches, _("Branches")),
182 182 (tags, _("Tags")),
183 183 ]
184 184 return [g for g in groups if g[0]], selected
185 185
186 186 def _get_is_allowed_change_status(self, pull_request):
187 187 if pull_request.is_closed():
188 188 return False
189 189
190 190 owner = self.authuser.user_id == pull_request.user_id
191 191 reviewer = self.authuser.user_id in [x.user_id for x in
192 192 pull_request.reviewers]
193 193 return self.authuser.admin or owner or reviewer
194 194
195 195 @LoginRequired()
196 196 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
197 197 'repository.admin')
198 198 def show_all(self, repo_name):
199 199 c.from_ = request.GET.get('from_') or ''
200 200 c.closed = request.GET.get('closed') or ''
201 201 c.pull_requests = PullRequestModel().get_all(repo_name, from_=c.from_, closed=c.closed)
202 202 c.repo_name = repo_name
203 203 p = safe_int(request.GET.get('page', 1), 1)
204 204
205 205 c.pullrequests_pager = Page(c.pull_requests, page=p, items_per_page=10)
206 206
207 207 c.pullrequest_data = render('/pullrequests/pullrequest_data.html')
208 208
209 209 if request.environ.get('HTTP_X_PARTIAL_XHR'):
210 210 return c.pullrequest_data
211 211
212 212 return render('/pullrequests/pullrequest_show_all.html')
213 213
214 214 @LoginRequired()
215 215 def show_my(self): # my_account_my_pullrequests
216 216 c.show_closed = request.GET.get('pr_show_closed')
217 217 return render('/pullrequests/pullrequest_show_my.html')
218 218
219 219 @NotAnonymous()
220 220 def show_my_data(self):
221 221 c.show_closed = request.GET.get('pr_show_closed')
222 222
223 223 def _filter(pr):
224 224 s = sorted(pr, key=lambda o: o.created_on, reverse=True)
225 225 if not c.show_closed:
226 226 s = filter(lambda p: p.status != PullRequest.STATUS_CLOSED, s)
227 227 return s
228 228
229 229 c.my_pull_requests = _filter(PullRequest.query()\
230 230 .filter(PullRequest.user_id ==
231 231 self.authuser.user_id)\
232 232 .all())
233 233
234 234 c.participate_in_pull_requests = _filter(PullRequest.query()\
235 235 .join(PullRequestReviewers)\
236 236 .filter(PullRequestReviewers.user_id ==
237 237 self.authuser.user_id)\
238 238 )
239 239
240 240 return render('/pullrequests/pullrequest_show_my_data.html')
241 241
242 242 @LoginRequired()
243 243 @NotAnonymous()
244 244 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
245 245 'repository.admin')
246 246 def index(self):
247 247 org_repo = c.db_repo
248 248 org_scm_instance = org_repo.scm_instance
249 249 try:
250 250 org_scm_instance.get_changeset()
251 251 except EmptyRepositoryError, e:
252 252 h.flash(h.literal(_('There are no changesets yet')),
253 253 category='warning')
254 254 redirect(url('summary_home', repo_name=org_repo.repo_name))
255 255
256 256 org_rev = request.GET.get('rev_end')
257 257 # rev_start is not directly useful - its parent could however be used
258 258 # as default for other and thus give a simple compare view
259 259 #other_rev = request.POST.get('rev_start')
260 260 branch = request.GET.get('branch')
261 261
262 262 c.cs_repos = [(org_repo.repo_name, org_repo.repo_name)]
263 263 c.default_cs_repo = org_repo.repo_name
264 264 c.cs_refs, c.default_cs_ref = self._get_repo_refs(org_scm_instance, rev=org_rev, branch=branch)
265 265
266 266 # add org repo to other so we can open pull request against peer branches on itself
267 267 c.a_repos = [(org_repo.repo_name, '%s (self)' % org_repo.repo_name)]
268 268
269 269 # add parent of this fork also and select it
270 270 if org_repo.parent:
271 271 c.a_repos.append((org_repo.parent.repo_name, '%s (parent)' % org_repo.parent.repo_name))
272 272 c.a_repo = org_repo.parent
273 273 c.a_refs, c.default_a_ref = self._get_repo_refs(org_repo.parent.scm_instance)
274 274 else:
275 275 c.a_repo = org_repo
276 276 c.a_refs, c.default_a_ref = self._get_repo_refs(org_scm_instance) # without rev and branch
277 277
278 278 # gather forks and add to this list ... even though it is rare to
279 279 # request forks to pull from their parent
280 280 for fork in org_repo.forks:
281 281 c.a_repos.append((fork.repo_name, fork.repo_name))
282 282
283 283 return render('/pullrequests/pullrequest.html')
284 284
285 285 @LoginRequired()
286 286 @NotAnonymous()
287 287 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
288 288 'repository.admin')
289 289 @jsonify
290 290 def repo_info(self, repo_name):
291 291 repo = RepoModel()._get_repo(repo_name)
292 292 refs, selected_ref = self._get_repo_refs(repo.scm_instance)
293 293 return {
294 294 'description': repo.description.split('\n', 1)[0],
295 295 'selected_ref': selected_ref,
296 296 'refs': refs,
297 297 'user': dict(user_id=repo.user.user_id,
298 298 username=repo.user.username,
299 299 firstname=repo.user.firstname,
300 300 lastname=repo.user.lastname,
301 301 gravatar_link=h.gravatar_url(repo.user.email, 14)),
302 302 }
303 303
304 304 @LoginRequired()
305 305 @NotAnonymous()
306 306 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
307 307 'repository.admin')
308 308 def create(self, repo_name):
309 309 repo = RepoModel()._get_repo(repo_name)
310 310 try:
311 311 _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
312 312 except formencode.Invalid, errors:
313 313 log.error(traceback.format_exc())
314 314 log.error(str(errors))
315 315 msg = _('Error creating pull request: %s') % errors.msg
316 316 h.flash(msg, 'error')
317 317 raise HTTPBadRequest
318 318
319 319 # heads up: org and other might seem backward here ...
320 320 org_repo_name = _form['org_repo']
321 321 org_ref = _form['org_ref'] # will have merge_rev as rev but symbolic name
322 322 org_repo = RepoModel()._get_repo(org_repo_name)
323 323 (org_ref_type,
324 324 org_ref_name,
325 325 org_rev) = org_ref.split(':')
326 326
327 327 other_repo_name = _form['other_repo']
328 328 other_ref = _form['other_ref'] # will have symbolic name and head revision
329 329 other_repo = RepoModel()._get_repo(other_repo_name)
330 330 (other_ref_type,
331 331 other_ref_name,
332 332 other_rev) = other_ref.split(':')
333 333
334 334 cs_ranges, _cs_ranges_not, ancestor_rev = \
335 335 CompareController._get_changesets(org_repo.scm_instance.alias,
336 336 other_repo.scm_instance, other_rev, # org and other "swapped"
337 337 org_repo.scm_instance, org_rev,
338 338 )
339 339 revisions = [cs.raw_id for cs in cs_ranges]
340 340
341 341 # hack: ancestor_rev is not an other_rev but we want to show the
342 342 # requested destination and have the exact ancestor
343 343 other_ref = '%s:%s:%s' % (other_ref_type, other_ref_name, ancestor_rev)
344 344
345 345 reviewers = _form['review_members']
346 346
347 347 title = _form['pullrequest_title']
348 348 if not title:
349 349 title = '%s#%s to %s#%s' % (org_repo_name, h.short_ref(org_ref_type, org_ref_name),
350 350 other_repo_name, h.short_ref(other_ref_type, other_ref_name))
351 351 description = _form['pullrequest_desc'].strip() or _('No description')
352 352 try:
353 353 pull_request = PullRequestModel().create(
354 354 self.authuser.user_id, org_repo_name, org_ref, other_repo_name,
355 355 other_ref, revisions, reviewers, title, description
356 356 )
357 357 Session().commit()
358 358 h.flash(_('Successfully opened new pull request'),
359 359 category='success')
360 360 except Exception:
361 361 h.flash(_('Error occurred while creating pull request'),
362 362 category='error')
363 363 log.error(traceback.format_exc())
364 364 return redirect(url('pullrequest_home', repo_name=repo_name))
365 365
366 366 return redirect(pull_request.url())
367 367
368 368 @LoginRequired()
369 369 @NotAnonymous()
370 370 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
371 371 'repository.admin')
372 372 def copy_update(self, repo_name, pull_request_id):
373 373 old_pull_request = PullRequest.get_or_404(pull_request_id)
374 374 assert old_pull_request.other_repo.repo_name == repo_name
375 375 if old_pull_request.is_closed():
376 376 raise HTTPForbidden()
377 377
378 378 org_repo = RepoModel()._get_repo(old_pull_request.org_repo.repo_name)
379 379 org_ref_type, org_ref_name, org_rev = old_pull_request.org_ref.split(':')
380 380 updaterev = request.POST.get('updaterev')
381 381 if updaterev:
382 382 new_org_rev = self._get_ref_rev(org_repo, 'rev', updaterev)
383 383 else:
384 384 # assert org_ref_type == 'branch', org_ref_type # TODO: what if not?
385 385 new_org_rev = self._get_ref_rev(org_repo, org_ref_type, org_ref_name)
386 386
387 387 other_repo = RepoModel()._get_repo(old_pull_request.other_repo.repo_name)
388 388 other_ref_type, other_ref_name, other_rev = old_pull_request.other_ref.split(':') # other_rev is ancestor
389 389 #assert other_ref_type == 'branch', other_ref_type # TODO: what if not?
390 390 new_other_rev = self._get_ref_rev(other_repo, other_ref_type, other_ref_name)
391 391
392 392 cs_ranges, _cs_ranges_not, ancestor_rev = CompareController._get_changesets(org_repo.scm_instance.alias,
393 393 other_repo.scm_instance, new_other_rev, # org and other "swapped"
394 394 org_repo.scm_instance, new_org_rev)
395 395
396 396 old_revisions = set(old_pull_request.revisions)
397 397 revisions = [cs.raw_id for cs in cs_ranges]
398 398 new_revisions = [r for r in revisions if r not in old_revisions]
399 399 lost = old_revisions.difference(revisions)
400 400
401 401 infos = ['','', 'This is an update of %s "%s".' %
402 402 (h.canonical_url('pullrequest_show', repo_name=old_pull_request.other_repo.repo_name,
403 403 pull_request_id=pull_request_id),
404 404 old_pull_request.title)]
405 405
406 406 if lost:
407 407 infos.append(_('Missing changesets since the previous version:'))
408 408 for r in old_pull_request.revisions:
409 409 if r in lost:
410 410 desc = org_repo.get_changeset(r).message.split('\n')[0]
411 411 infos.append(' %s "%s"' % (h.short_id(r), desc))
412 412
413 413 if new_revisions:
414 414 infos.append(_('New changesets on %s %s since the previous version:') % (org_ref_type, org_ref_name))
415 415 for r in reversed(revisions):
416 416 if r in new_revisions:
417 417 desc = org_repo.get_changeset(r).message.split('\n')[0]
418 418 infos.append(' %s %s' % (h.short_id(r), h.shorter(desc, 80)))
419 419
420 420 if ancestor_rev == other_rev:
421 421 infos.append(_("Ancestor didn't change - show diff since previous version:"))
422 422 infos.append(h.canonical_url('compare_url',
423 423 repo_name=org_repo.repo_name, # other_repo is always same as repo_name
424 424 org_ref_type='rev', org_ref_name=h.short_id(org_rev), # use old org_rev as base
425 425 other_ref_type='rev', other_ref_name=h.short_id(new_org_rev),
426 426 )) # note: linear diff, merge or not doesn't matter
427 427 else:
428 428 infos.append(_('This pull request is based on another %s revision and there is no simple diff.') % other_ref_name)
429 429 else:
430 430 infos.append(_('No changes found on %s %s since previous version.') % (org_ref_type, org_ref_name))
431 431 # TODO: fail?
432 432
433 433 # hack: ancestor_rev is not an other_ref but we want to show the
434 434 # requested destination and have the exact ancestor
435 435 new_other_ref = '%s:%s:%s' % (other_ref_type, other_ref_name, ancestor_rev)
436 436 new_org_ref = '%s:%s:%s' % (org_ref_type, org_ref_name, new_org_rev)
437 437
438 438 reviewers = [r.user_id for r in old_pull_request.reviewers]
439 439 try:
440 440 old_title, old_v = re.match(r'(.*)\(v(\d+)\)\s*$', old_pull_request.title).groups()
441 441 v = int(old_v) + 1
442 442 except (AttributeError, ValueError):
443 443 old_title = old_pull_request.title
444 444 v = 2
445 445 title = '%s (v%s)' % (old_title.strip(), v)
446 446 description = (old_pull_request.description.rstrip() +
447 447 '\n'.join(infos))
448 448
449 449 try:
450 450 pull_request = PullRequestModel().create(
451 451 self.authuser.user_id,
452 452 old_pull_request.org_repo.repo_name, new_org_ref,
453 453 old_pull_request.other_repo.repo_name, new_other_ref,
454 454 revisions, reviewers, title, description
455 455 )
456 456 except Exception:
457 457 h.flash(_('Error occurred while creating pull request'),
458 458 category='error')
459 459 log.error(traceback.format_exc())
460 460 return redirect(old_pull_request.url())
461 461
462 462 ChangesetCommentsModel().create(
463 463 text=_('Closed, replaced by %s .') % h.canonical_url('pullrequest_show',
464 464 repo_name=old_pull_request.other_repo.repo_name,
465 465 pull_request_id=pull_request.pull_request_id),
466 466 repo=old_pull_request.other_repo.repo_id,
467 467 user=c.authuser.user_id,
468 468 pull_request=pull_request_id,
469 469 closing_pr=True)
470 470 PullRequestModel().close_pull_request(pull_request_id)
471 471
472 472 Session().commit()
473 473 h.flash(_('Pull request update created'),
474 474 category='success')
475 475
476 476 return redirect(pull_request.url())
477 477
478 478 # pullrequest_post for PR editing
479 479 @LoginRequired()
480 480 @NotAnonymous()
481 481 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
482 482 'repository.admin')
483 483 def post(self, repo_name, pull_request_id):
484 484 repo = RepoModel()._get_repo(repo_name)
485 485 pull_request = PullRequest.get_or_404(pull_request_id)
486 486 old_description = pull_request.description
487 487
488 488 _form = PullRequestPostForm()().to_python(request.POST)
489 489
490 490 pull_request.title = _form['pullrequest_title']
491 491 pull_request.description = _form['pullrequest_desc'].strip() or _('No description')
492 492
493 493 PullRequestModel().mention_from_description(pull_request, old_description)
494 494
495 495 Session().commit()
496 496 h.flash(_('Pull request updated'), category='success')
497 497
498 498 return redirect(pull_request.url())
499 499
500 500 # pullrequest_update for updating reviewer list
501 501 @LoginRequired()
502 502 @NotAnonymous()
503 503 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
504 504 'repository.admin')
505 505 @jsonify
506 506 def update(self, repo_name, pull_request_id):
507 507 pull_request = PullRequest.get_or_404(pull_request_id)
508 508 if pull_request.is_closed():
509 509 raise HTTPForbidden()
510 510 #only owner or admin can update it
511 511 owner = pull_request.author.user_id == c.authuser.user_id
512 512 repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
513 513 if h.HasPermissionAny('hg.admin') or repo_admin or owner:
514 514 reviewers_ids = map(int, filter(lambda v: v not in [None, ''],
515 515 request.POST.get('reviewers_ids', '').split(',')))
516 516
517 517 PullRequestModel().update_reviewers(pull_request_id, reviewers_ids)
518 518 Session().commit()
519 519 return True
520 520 raise HTTPForbidden()
521 521
522 522 @LoginRequired()
523 523 @NotAnonymous()
524 524 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
525 525 'repository.admin')
526 526 @jsonify
527 527 def delete(self, repo_name, pull_request_id):
528 528 pull_request = PullRequest.get_or_404(pull_request_id)
529 529 #only owner can delete it !
530 530 if pull_request.author.user_id == c.authuser.user_id:
531 531 PullRequestModel().delete(pull_request)
532 532 Session().commit()
533 533 h.flash(_('Successfully deleted pull request'),
534 534 category='success')
535 535 return redirect(url('my_pullrequests'))
536 536 raise HTTPForbidden()
537 537
538 538 @LoginRequired()
539 539 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
540 540 'repository.admin')
541 541 def show(self, repo_name, pull_request_id, extra=None):
542 542 repo_model = RepoModel()
543 543 c.users_array = repo_model.get_users_js()
544 544 c.user_groups_array = repo_model.get_user_groups_js()
545 545 c.pull_request = PullRequest.get_or_404(pull_request_id)
546 546 c.allowed_to_change_status = self._get_is_allowed_change_status(c.pull_request)
547 547 cc_model = ChangesetCommentsModel()
548 548 cs_model = ChangesetStatusModel()
549 549
550 550 # pull_requests repo_name we opened it against
551 551 # ie. other_repo must match
552 552 if repo_name != c.pull_request.other_repo.repo_name:
553 553 raise HTTPNotFound
554 554
555 555 # load compare data into template context
556 556 c.cs_repo = c.pull_request.org_repo
557 557 (c.cs_ref_type,
558 558 c.cs_ref_name,
559 559 c.cs_rev) = c.pull_request.org_ref.split(':')
560 560
561 561 c.a_repo = c.pull_request.other_repo
562 562 (c.a_ref_type,
563 563 c.a_ref_name,
564 564 c.a_rev) = c.pull_request.other_ref.split(':') # other_rev is ancestor
565 565
566 566 org_scm_instance = c.cs_repo.scm_instance # property with expensive cache invalidation check!!!
567 567 c.cs_repo = c.cs_repo
568 568 c.cs_ranges = [org_scm_instance.get_changeset(x) for x in c.pull_request.revisions]
569 569 c.cs_ranges_org = None # not stored and not important and moving target - could be calculated ...
570 570 revs = [ctx.revision for ctx in reversed(c.cs_ranges)]
571 571 c.jsdata = json.dumps(graph_data(org_scm_instance, revs))
572 572
573 573 c.available = []
574 574 c.cs_branch_name = c.cs_ref_name
575 575 other_scm_instance = c.a_repo.scm_instance
576 576 c.update_msg = ""
577 577 c.update_msg_other = ""
578 578 if org_scm_instance.alias == 'hg' and c.a_ref_name != 'ancestor':
579 579 if c.cs_ref_type != 'branch':
580 580 c.cs_branch_name = org_scm_instance.get_changeset(c.cs_ref_name).branch # use ref_type ?
581 581 other_branch_name = c.a_ref_name
582 582 if c.a_ref_type != 'branch':
583 583 try:
584 584 other_branch_name = other_scm_instance.get_changeset(c.a_ref_name).branch # use ref_type ?
585 585 except EmptyRepositoryError:
586 586 other_branch_name = 'null' # not a branch name ... but close enough
587 587 # candidates: descendants of old head that are on the right branch
588 588 # and not are the old head itself ...
589 589 # and nothing at all if old head is a descendent of target ref name
590 590 if other_scm_instance._repo.revs('present(%s)::&%s', c.cs_ranges[-1].raw_id, other_branch_name):
591 591 c.update_msg = _('This pull request has already been merged to %s.') % other_branch_name
592 592 else: # look for children of PR head on source branch in org repo
593 593 arevs = org_scm_instance._repo.revs('%s:: & branch(%s) - %s',
594 594 revs[0], c.cs_branch_name, revs[0])
595 595 if arevs:
596 596 if c.pull_request.is_closed():
597 597 c.update_msg = _('This pull request has been closed and can not be updated with descendent changes on %s:') % c.cs_branch_name
598 598 else:
599 599 c.update_msg = _('This pull request can be updated with descendent changes on %s:') % c.cs_branch_name
600 600 c.available = [org_scm_instance.get_changeset(x) for x in arevs]
601 601 else:
602 602 c.update_msg = _('No changesets found for updating this pull request.')
603 603
604 604 revs = org_scm_instance._repo.revs('head() & not (%s::) & branch(%s) & !closed()', revs[0], c.cs_branch_name)
605 605 if revs:
606 606 c.update_msg_other = _('Note: Branch %s also contains unrelated changes, such as %s.') % (c.cs_branch_name,
607 607 h.short_id(org_scm_instance.get_changeset((max(revs))).raw_id))
608 608 else:
609 609 c.update_msg_other = _('Branch %s does not contain other changes.') % c.cs_branch_name
610 610 elif org_scm_instance.alias == 'git':
611 611 c.update_msg = _("Git pull requests don't support updates yet.")
612 612
613 613 raw_ids = [x.raw_id for x in c.cs_ranges]
614 614 c.cs_comments = c.cs_repo.get_comments(raw_ids)
615 615 c.statuses = c.cs_repo.statuses(raw_ids)
616 616
617 617 ignore_whitespace = request.GET.get('ignorews') == '1'
618 618 line_context = request.GET.get('context', 3)
619 619 c.ignorews_url = _ignorews_url
620 620 c.context_url = _context_url
621 621 c.fulldiff = request.GET.get('fulldiff')
622 622 diff_limit = self.cut_off_limit if not c.fulldiff else None
623 623
624 624 # we swap org/other ref since we run a simple diff on one repo
625 625 log.debug('running diff between %s and %s in %s'
626 626 % (c.a_rev, c.cs_rev, org_scm_instance.path))
627 627 txtdiff = org_scm_instance.get_diff(rev1=safe_str(c.a_rev), rev2=safe_str(c.cs_rev),
628 628 ignore_whitespace=ignore_whitespace,
629 629 context=line_context)
630 630
631 631 diff_processor = diffs.DiffProcessor(txtdiff or '', format='gitdiff',
632 632 diff_limit=diff_limit)
633 633 _parsed = diff_processor.prepare()
634 634
635 635 c.limited_diff = False
636 636 if isinstance(_parsed, LimitedDiffContainer):
637 637 c.limited_diff = True
638 638
639 639 c.files = []
640 640 c.changes = {}
641 641 c.lines_added = 0
642 642 c.lines_deleted = 0
643 643
644 644 for f in _parsed:
645 645 st = f['stats']
646 646 c.lines_added += st['added']
647 647 c.lines_deleted += st['deleted']
648 648 fid = h.FID('', f['filename'])
649 649 c.files.append([fid, f['operation'], f['filename'], f['stats']])
650 650 htmldiff = diff_processor.as_html(enable_comments=True,
651 651 parsed_lines=[f])
652 652 c.changes[fid] = [f['operation'], f['filename'], htmldiff]
653 653
654 654 # inline comments
655 655 c.inline_cnt = 0
656 656 c.inline_comments = cc_model.get_inline_comments(
657 657 c.db_repo.repo_id,
658 658 pull_request=pull_request_id)
659 659 # count inline comments
660 660 for __, lines in c.inline_comments:
661 661 for comments in lines.values():
662 662 c.inline_cnt += len(comments)
663 663 # comments
664 664 c.comments = cc_model.get_comments(c.db_repo.repo_id,
665 665 pull_request=pull_request_id)
666 666
667 667 # (badly named) pull-request status calculation based on reviewer votes
668 668 (c.pull_request_reviewers,
669 669 c.pull_request_pending_reviewers,
670 670 c.current_voting_result,
671 671 ) = cs_model.calculate_pull_request_result(c.pull_request)
672 672 c.changeset_statuses = ChangesetStatus.STATUSES
673 673
674 674 c.as_form = False
675 675 c.ancestor = None # there is one - but right here we don't know which
676 676 return render('/pullrequests/pullrequest_show.html')
677 677
678 678 @LoginRequired()
679 679 @NotAnonymous()
680 680 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
681 681 'repository.admin')
682 682 @jsonify
683 683 def comment(self, repo_name, pull_request_id):
684 684 pull_request = PullRequest.get_or_404(pull_request_id)
685 685
686 686 status = 0
687 687 close_pr = False
688 688 allowed_to_change_status = self._get_is_allowed_change_status(pull_request)
689 689 if allowed_to_change_status:
690 690 status = request.POST.get('changeset_status')
691 691 close_pr = request.POST.get('save_close')
692 692 text = request.POST.get('text', '').strip() or _('No comments.')
693 693 if close_pr:
694 694 text = _('Closing.') + '\n' + text
695 695
696 696 comm = ChangesetCommentsModel().create(
697 697 text=text,
698 698 repo=c.db_repo.repo_id,
699 699 user=c.authuser.user_id,
700 700 pull_request=pull_request_id,
701 701 f_path=request.POST.get('f_path'),
702 702 line_no=request.POST.get('line'),
703 703 status_change=(ChangesetStatus.get_status_lbl(status)
704 704 if status and allowed_to_change_status else None),
705 705 closing_pr=close_pr
706 706 )
707 707
708 708 action_logger(self.authuser,
709 709 'user_commented_pull_request:%s' % pull_request_id,
710 710 c.db_repo, self.ip_addr, self.sa)
711 711
712 712 if allowed_to_change_status:
713 713 # get status if set !
714 714 if status:
715 715 ChangesetStatusModel().set_status(
716 716 c.db_repo.repo_id,
717 717 status,
718 718 c.authuser.user_id,
719 719 comm,
720 720 pull_request=pull_request_id
721 721 )
722 722
723 723 if close_pr:
724 724 PullRequestModel().close_pull_request(pull_request_id)
725 725 action_logger(self.authuser,
726 726 'user_closed_pull_request:%s' % pull_request_id,
727 727 c.db_repo, self.ip_addr, self.sa)
728 728
729 729 Session().commit()
730 730
731 731 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
732 732 return redirect(pull_request.url())
733 733
734 734 data = {
735 735 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
736 736 }
737 737 if comm:
738 738 c.co = comm
739 739 data.update(comm.get_dict())
740 740 data.update({'rendered_text':
741 741 render('changeset/changeset_comment_block.html')})
742 742
743 743 return data
744 744
745 745 @LoginRequired()
746 746 @NotAnonymous()
747 747 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
748 748 'repository.admin')
749 749 @jsonify
750 750 def delete_comment(self, repo_name, comment_id):
751 751 co = ChangesetComment.get(comment_id)
752 752 if co.pull_request.is_closed():
753 753 #don't allow deleting comments on closed pull request
754 754 raise HTTPForbidden()
755 755
756 756 owner = co.author.user_id == c.authuser.user_id
757 757 repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
758 758 if h.HasPermissionAny('hg.admin') or repo_admin or owner:
759 759 ChangesetCommentsModel().delete(comment=co)
760 760 Session().commit()
761 761 return True
762 762 else:
763 763 raise HTTPForbidden()
General Comments 0
You need to be logged in to leave comments. Login now