##// END OF EJS Templates
security: use 404 instead of 403 in case missing permissions for comment deletion....
ergo -
r1826:76aa3640 default
parent child Browse files
Show More
@@ -1,488 +1,490 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 commit controller for RhodeCode showing changes between commits
23 23 """
24 24
25 25 import logging
26 26
27 27 from collections import defaultdict
28 28 from webob.exc import HTTPForbidden, HTTPBadRequest, HTTPNotFound
29 29
30 30 from pylons import tmpl_context as c, request, response
31 31 from pylons.i18n.translation import _
32 32 from pylons.controllers.util import redirect
33 33
34 34 from rhodecode.lib import auth
35 35 from rhodecode.lib import diffs, codeblocks
36 36 from rhodecode.lib.auth import (
37 37 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous)
38 38 from rhodecode.lib.base import BaseRepoController, render
39 39 from rhodecode.lib.compat import OrderedDict
40 40 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
41 41 import rhodecode.lib.helpers as h
42 42 from rhodecode.lib.utils import jsonify
43 43 from rhodecode.lib.utils2 import safe_unicode, safe_int
44 44 from rhodecode.lib.vcs.backends.base import EmptyCommit
45 45 from rhodecode.lib.vcs.exceptions import (
46 46 RepositoryError, CommitDoesNotExistError, NodeDoesNotExistError)
47 47 from rhodecode.model.db import ChangesetComment, ChangesetStatus
48 48 from rhodecode.model.changeset_status import ChangesetStatusModel
49 49 from rhodecode.model.comment import CommentsModel
50 50 from rhodecode.model.meta import Session
51 51
52 52
53 53 log = logging.getLogger(__name__)
54 54
55 55
56 56 def _update_with_GET(params, GET):
57 57 for k in ['diff1', 'diff2', 'diff']:
58 58 params[k] += GET.getall(k)
59 59
60 60
61 61 def get_ignore_ws(fid, GET):
62 62 ig_ws_global = GET.get('ignorews')
63 63 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
64 64 if ig_ws:
65 65 try:
66 66 return int(ig_ws[0].split(':')[-1])
67 67 except Exception:
68 68 pass
69 69 return ig_ws_global
70 70
71 71
72 72 def _ignorews_url(GET, fileid=None):
73 73 fileid = str(fileid) if fileid else None
74 74 params = defaultdict(list)
75 75 _update_with_GET(params, GET)
76 76 label = _('Show whitespace')
77 77 tooltiplbl = _('Show whitespace for all diffs')
78 78 ig_ws = get_ignore_ws(fileid, GET)
79 79 ln_ctx = get_line_ctx(fileid, GET)
80 80
81 81 if ig_ws is None:
82 82 params['ignorews'] += [1]
83 83 label = _('Ignore whitespace')
84 84 tooltiplbl = _('Ignore whitespace for all diffs')
85 85 ctx_key = 'context'
86 86 ctx_val = ln_ctx
87 87
88 88 # if we have passed in ln_ctx pass it along to our params
89 89 if ln_ctx:
90 90 params[ctx_key] += [ctx_val]
91 91
92 92 if fileid:
93 93 params['anchor'] = 'a_' + fileid
94 94 return h.link_to(label, h.url.current(**params), title=tooltiplbl, class_='tooltip')
95 95
96 96
97 97 def get_line_ctx(fid, GET):
98 98 ln_ctx_global = GET.get('context')
99 99 if fid:
100 100 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
101 101 else:
102 102 _ln_ctx = filter(lambda k: k.startswith('C'), GET)
103 103 ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
104 104 if ln_ctx:
105 105 ln_ctx = [ln_ctx]
106 106
107 107 if ln_ctx:
108 108 retval = ln_ctx[0].split(':')[-1]
109 109 else:
110 110 retval = ln_ctx_global
111 111
112 112 try:
113 113 return int(retval)
114 114 except Exception:
115 115 return 3
116 116
117 117
118 118 def _context_url(GET, fileid=None):
119 119 """
120 120 Generates a url for context lines.
121 121
122 122 :param fileid:
123 123 """
124 124
125 125 fileid = str(fileid) if fileid else None
126 126 ig_ws = get_ignore_ws(fileid, GET)
127 127 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
128 128
129 129 params = defaultdict(list)
130 130 _update_with_GET(params, GET)
131 131
132 132 if ln_ctx > 0:
133 133 params['context'] += [ln_ctx]
134 134
135 135 if ig_ws:
136 136 ig_ws_key = 'ignorews'
137 137 ig_ws_val = 1
138 138 params[ig_ws_key] += [ig_ws_val]
139 139
140 140 lbl = _('Increase context')
141 141 tooltiplbl = _('Increase context for all diffs')
142 142
143 143 if fileid:
144 144 params['anchor'] = 'a_' + fileid
145 145 return h.link_to(lbl, h.url.current(**params), title=tooltiplbl, class_='tooltip')
146 146
147 147
148 148 class ChangesetController(BaseRepoController):
149 149
150 150 def __before__(self):
151 151 super(ChangesetController, self).__before__()
152 152 c.affected_files_cut_off = 60
153 153
154 154 def _index(self, commit_id_range, method):
155 155 c.ignorews_url = _ignorews_url
156 156 c.context_url = _context_url
157 157 c.fulldiff = fulldiff = request.GET.get('fulldiff')
158 158
159 159 # fetch global flags of ignore ws or context lines
160 160 context_lcl = get_line_ctx('', request.GET)
161 161 ign_whitespace_lcl = get_ignore_ws('', request.GET)
162 162
163 163 # diff_limit will cut off the whole diff if the limit is applied
164 164 # otherwise it will just hide the big files from the front-end
165 165 diff_limit = self.cut_off_limit_diff
166 166 file_limit = self.cut_off_limit_file
167 167
168 168 # get ranges of commit ids if preset
169 169 commit_range = commit_id_range.split('...')[:2]
170 170
171 171 try:
172 172 pre_load = ['affected_files', 'author', 'branch', 'date',
173 173 'message', 'parents']
174 174
175 175 if len(commit_range) == 2:
176 176 commits = c.rhodecode_repo.get_commits(
177 177 start_id=commit_range[0], end_id=commit_range[1],
178 178 pre_load=pre_load)
179 179 commits = list(commits)
180 180 else:
181 181 commits = [c.rhodecode_repo.get_commit(
182 182 commit_id=commit_id_range, pre_load=pre_load)]
183 183
184 184 c.commit_ranges = commits
185 185 if not c.commit_ranges:
186 186 raise RepositoryError(
187 187 'The commit range returned an empty result')
188 188 except CommitDoesNotExistError:
189 189 msg = _('No such commit exists for this repository')
190 190 h.flash(msg, category='error')
191 191 raise HTTPNotFound()
192 192 except Exception:
193 193 log.exception("General failure")
194 194 raise HTTPNotFound()
195 195
196 196 c.changes = OrderedDict()
197 197 c.lines_added = 0
198 198 c.lines_deleted = 0
199 199
200 200 # auto collapse if we have more than limit
201 201 collapse_limit = diffs.DiffProcessor._collapse_commits_over
202 202 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
203 203
204 204 c.commit_statuses = ChangesetStatus.STATUSES
205 205 c.inline_comments = []
206 206 c.files = []
207 207
208 208 c.statuses = []
209 209 c.comments = []
210 210 c.unresolved_comments = []
211 211 if len(c.commit_ranges) == 1:
212 212 commit = c.commit_ranges[0]
213 213 c.comments = CommentsModel().get_comments(
214 214 c.rhodecode_db_repo.repo_id,
215 215 revision=commit.raw_id)
216 216 c.statuses.append(ChangesetStatusModel().get_status(
217 217 c.rhodecode_db_repo.repo_id, commit.raw_id))
218 218 # comments from PR
219 219 statuses = ChangesetStatusModel().get_statuses(
220 220 c.rhodecode_db_repo.repo_id, commit.raw_id,
221 221 with_revisions=True)
222 222 prs = set(st.pull_request for st in statuses
223 223 if st.pull_request is not None)
224 224 # from associated statuses, check the pull requests, and
225 225 # show comments from them
226 226 for pr in prs:
227 227 c.comments.extend(pr.comments)
228 228
229 229 c.unresolved_comments = CommentsModel()\
230 230 .get_commit_unresolved_todos(commit.raw_id)
231 231
232 232 # Iterate over ranges (default commit view is always one commit)
233 233 for commit in c.commit_ranges:
234 234 c.changes[commit.raw_id] = []
235 235
236 236 commit2 = commit
237 237 commit1 = commit.parents[0] if commit.parents else EmptyCommit()
238 238
239 239 _diff = c.rhodecode_repo.get_diff(
240 240 commit1, commit2,
241 241 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
242 242 diff_processor = diffs.DiffProcessor(
243 243 _diff, format='newdiff', diff_limit=diff_limit,
244 244 file_limit=file_limit, show_full_diff=fulldiff)
245 245
246 246 commit_changes = OrderedDict()
247 247 if method == 'show':
248 248 _parsed = diff_processor.prepare()
249 249 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
250 250
251 251 _parsed = diff_processor.prepare()
252 252
253 253 def _node_getter(commit):
254 254 def get_node(fname):
255 255 try:
256 256 return commit.get_node(fname)
257 257 except NodeDoesNotExistError:
258 258 return None
259 259 return get_node
260 260
261 261 inline_comments = CommentsModel().get_inline_comments(
262 262 c.rhodecode_db_repo.repo_id, revision=commit.raw_id)
263 263 c.inline_cnt = CommentsModel().get_inline_comments_count(
264 264 inline_comments)
265 265
266 266 diffset = codeblocks.DiffSet(
267 267 repo_name=c.repo_name,
268 268 source_node_getter=_node_getter(commit1),
269 269 target_node_getter=_node_getter(commit2),
270 270 comments=inline_comments
271 271 ).render_patchset(_parsed, commit1.raw_id, commit2.raw_id)
272 272 c.changes[commit.raw_id] = diffset
273 273 else:
274 274 # downloads/raw we only need RAW diff nothing else
275 275 diff = diff_processor.as_raw()
276 276 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
277 277
278 278 # sort comments by how they were generated
279 279 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
280 280
281 281 if len(c.commit_ranges) == 1:
282 282 c.commit = c.commit_ranges[0]
283 283 c.parent_tmpl = ''.join(
284 284 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
285 285 if method == 'download':
286 286 response.content_type = 'text/plain'
287 287 response.content_disposition = (
288 288 'attachment; filename=%s.diff' % commit_id_range[:12])
289 289 return diff
290 290 elif method == 'patch':
291 291 response.content_type = 'text/plain'
292 292 c.diff = safe_unicode(diff)
293 293 return render('changeset/patch_changeset.mako')
294 294 elif method == 'raw':
295 295 response.content_type = 'text/plain'
296 296 return diff
297 297 elif method == 'show':
298 298 if len(c.commit_ranges) == 1:
299 299 return render('changeset/changeset.mako')
300 300 else:
301 301 c.ancestor = None
302 302 c.target_repo = c.rhodecode_db_repo
303 303 return render('changeset/changeset_range.mako')
304 304
305 305 @LoginRequired()
306 306 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
307 307 'repository.admin')
308 308 def index(self, revision, method='show'):
309 309 return self._index(revision, method=method)
310 310
311 311 @LoginRequired()
312 312 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
313 313 'repository.admin')
314 314 def changeset_raw(self, revision):
315 315 return self._index(revision, method='raw')
316 316
317 317 @LoginRequired()
318 318 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
319 319 'repository.admin')
320 320 def changeset_patch(self, revision):
321 321 return self._index(revision, method='patch')
322 322
323 323 @LoginRequired()
324 324 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
325 325 'repository.admin')
326 326 def changeset_download(self, revision):
327 327 return self._index(revision, method='download')
328 328
329 329 @LoginRequired()
330 330 @NotAnonymous()
331 331 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
332 332 'repository.admin')
333 333 @auth.CSRFRequired()
334 334 @jsonify
335 335 def comment(self, repo_name, revision):
336 336 commit_id = revision
337 337 status = request.POST.get('changeset_status', None)
338 338 text = request.POST.get('text')
339 339 comment_type = request.POST.get('comment_type')
340 340 resolves_comment_id = request.POST.get('resolves_comment_id', None)
341 341
342 342 if status:
343 343 text = text or (_('Status change %(transition_icon)s %(status)s')
344 344 % {'transition_icon': '>',
345 345 'status': ChangesetStatus.get_status_lbl(status)})
346 346
347 347 multi_commit_ids = []
348 348 for _commit_id in request.POST.get('commit_ids', '').split(','):
349 349 if _commit_id not in ['', None, EmptyCommit.raw_id]:
350 350 if _commit_id not in multi_commit_ids:
351 351 multi_commit_ids.append(_commit_id)
352 352
353 353 commit_ids = multi_commit_ids or [commit_id]
354 354
355 355 comment = None
356 356 for current_id in filter(None, commit_ids):
357 357 c.co = comment = CommentsModel().create(
358 358 text=text,
359 359 repo=c.rhodecode_db_repo.repo_id,
360 360 user=c.rhodecode_user.user_id,
361 361 commit_id=current_id,
362 362 f_path=request.POST.get('f_path'),
363 363 line_no=request.POST.get('line'),
364 364 status_change=(ChangesetStatus.get_status_lbl(status)
365 365 if status else None),
366 366 status_change_type=status,
367 367 comment_type=comment_type,
368 368 resolves_comment_id=resolves_comment_id
369 369 )
370 370
371 371 # get status if set !
372 372 if status:
373 373 # if latest status was from pull request and it's closed
374 374 # disallow changing status !
375 375 # dont_allow_on_closed_pull_request = True !
376 376
377 377 try:
378 378 ChangesetStatusModel().set_status(
379 379 c.rhodecode_db_repo.repo_id,
380 380 status,
381 381 c.rhodecode_user.user_id,
382 382 comment,
383 383 revision=current_id,
384 384 dont_allow_on_closed_pull_request=True
385 385 )
386 386 except StatusChangeOnClosedPullRequestError:
387 387 msg = _('Changing the status of a commit associated with '
388 388 'a closed pull request is not allowed')
389 389 log.exception(msg)
390 390 h.flash(msg, category='warning')
391 391 return redirect(h.url(
392 392 'changeset_home', repo_name=repo_name,
393 393 revision=current_id))
394 394
395 395 # finalize, commit and redirect
396 396 Session().commit()
397 397
398 398 data = {
399 399 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
400 400 }
401 401 if comment:
402 402 data.update(comment.get_dict())
403 403 data.update({'rendered_text':
404 404 render('changeset/changeset_comment_block.mako')})
405 405
406 406 return data
407 407
408 408 @LoginRequired()
409 409 @NotAnonymous()
410 410 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
411 411 'repository.admin')
412 412 @auth.CSRFRequired()
413 413 def preview_comment(self):
414 414 # Technically a CSRF token is not needed as no state changes with this
415 415 # call. However, as this is a POST is better to have it, so automated
416 416 # tools don't flag it as potential CSRF.
417 417 # Post is required because the payload could be bigger than the maximum
418 418 # allowed by GET.
419 419 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
420 420 raise HTTPBadRequest()
421 421 text = request.POST.get('text')
422 422 renderer = request.POST.get('renderer') or 'rst'
423 423 if text:
424 424 return h.render(text, renderer=renderer, mentions=True)
425 425 return ''
426 426
427 427 @LoginRequired()
428 428 @NotAnonymous()
429 429 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
430 430 'repository.admin')
431 431 @auth.CSRFRequired()
432 432 @jsonify
433 433 def delete_comment(self, repo_name, comment_id):
434 434 comment = ChangesetComment.get_or_404(safe_int(comment_id))
435 435 if not comment:
436 436 log.debug('Comment with id:%s not found, skipping', comment_id)
437 437 # comment already deleted in another call probably
438 438 return True
439 439
440 440 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
441 441 super_admin = h.HasPermissionAny('hg.admin')()
442 442 comment_owner = (comment.author.user_id == c.rhodecode_user.user_id)
443 443 is_repo_comment = comment.repo.repo_name == c.repo_name
444 444 comment_repo_admin = is_repo_admin and is_repo_comment
445 445
446 446 if super_admin or comment_owner or comment_repo_admin:
447 447 CommentsModel().delete(comment=comment, user=c.rhodecode_user)
448 448 Session().commit()
449 449 return True
450 450 else:
451 raise HTTPForbidden()
451 log.warning('No permissions for user %s to delete comment_id: %s',
452 c.rhodecode_user, comment_id)
453 raise HTTPNotFound()
452 454
453 455 @LoginRequired()
454 456 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
455 457 'repository.admin')
456 458 @jsonify
457 459 def changeset_info(self, repo_name, revision):
458 460 if request.is_xhr:
459 461 try:
460 462 return c.rhodecode_repo.get_commit(commit_id=revision)
461 463 except CommitDoesNotExistError as e:
462 464 return EmptyCommit(message=str(e))
463 465 else:
464 466 raise HTTPBadRequest()
465 467
466 468 @LoginRequired()
467 469 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
468 470 'repository.admin')
469 471 @jsonify
470 472 def changeset_children(self, repo_name, revision):
471 473 if request.is_xhr:
472 474 commit = c.rhodecode_repo.get_commit(commit_id=revision)
473 475 result = {"results": commit.children}
474 476 return result
475 477 else:
476 478 raise HTTPBadRequest()
477 479
478 480 @LoginRequired()
479 481 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
480 482 'repository.admin')
481 483 @jsonify
482 484 def changeset_parents(self, repo_name, revision):
483 485 if request.is_xhr:
484 486 commit = c.rhodecode_repo.get_commit(commit_id=revision)
485 487 result = {"results": commit.parents}
486 488 return result
487 489 else:
488 490 raise HTTPBadRequest()
@@ -1,1016 +1,1018 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 pull requests controller for rhodecode for initializing pull requests
23 23 """
24 24 import peppercorn
25 25 import formencode
26 26 import logging
27 27 import collections
28 28
29 29 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
30 30 from pylons import request, tmpl_context as c, url
31 31 from pylons.controllers.util import redirect
32 32 from pylons.i18n.translation import _
33 33 from pyramid.threadlocal import get_current_registry
34 34 from pyramid.httpexceptions import HTTPFound
35 35 from sqlalchemy.sql import func
36 36 from sqlalchemy.sql.expression import or_
37 37
38 38 from rhodecode import events
39 39 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
40 40 from rhodecode.lib.ext_json import json
41 41 from rhodecode.lib.base import (
42 42 BaseRepoController, render, vcs_operation_context)
43 43 from rhodecode.lib.auth import (
44 44 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
45 45 HasAcceptedRepoType, XHRRequired)
46 46 from rhodecode.lib.channelstream import channelstream_request
47 47 from rhodecode.lib.utils import jsonify
48 48 from rhodecode.lib.utils2 import (
49 49 safe_int, safe_str, str2bool, safe_unicode)
50 50 from rhodecode.lib.vcs.backends.base import (
51 51 EmptyCommit, UpdateFailureReason, EmptyRepository)
52 52 from rhodecode.lib.vcs.exceptions import (
53 53 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
54 54 NodeDoesNotExistError)
55 55
56 56 from rhodecode.model.changeset_status import ChangesetStatusModel
57 57 from rhodecode.model.comment import CommentsModel
58 58 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
59 59 Repository, PullRequestVersion)
60 60 from rhodecode.model.forms import PullRequestForm
61 61 from rhodecode.model.meta import Session
62 62 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
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 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
72 72 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
73 73
74 74 @LoginRequired()
75 75 @NotAnonymous()
76 76 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
77 77 'repository.admin')
78 78 @HasAcceptedRepoType('git', 'hg')
79 79 def index(self):
80 80 source_repo = c.rhodecode_db_repo
81 81
82 82 try:
83 83 source_repo.scm_instance().get_commit()
84 84 except EmptyRepositoryError:
85 85 h.flash(h.literal(_('There are no commits yet')),
86 86 category='warning')
87 87 redirect(h.route_path('repo_summary', repo_name=source_repo.repo_name))
88 88
89 89 commit_id = request.GET.get('commit')
90 90 branch_ref = request.GET.get('branch')
91 91 bookmark_ref = request.GET.get('bookmark')
92 92
93 93 try:
94 94 source_repo_data = PullRequestModel().generate_repo_data(
95 95 source_repo, commit_id=commit_id,
96 96 branch=branch_ref, bookmark=bookmark_ref)
97 97 except CommitDoesNotExistError as e:
98 98 log.exception(e)
99 99 h.flash(_('Commit does not exist'), 'error')
100 100 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
101 101
102 102 default_target_repo = source_repo
103 103
104 104 if source_repo.parent:
105 105 parent_vcs_obj = source_repo.parent.scm_instance()
106 106 if parent_vcs_obj and not parent_vcs_obj.is_empty():
107 107 # change default if we have a parent repo
108 108 default_target_repo = source_repo.parent
109 109
110 110 target_repo_data = PullRequestModel().generate_repo_data(
111 111 default_target_repo)
112 112
113 113 selected_source_ref = source_repo_data['refs']['selected_ref']
114 114
115 115 title_source_ref = selected_source_ref.split(':', 2)[1]
116 116 c.default_title = PullRequestModel().generate_pullrequest_title(
117 117 source=source_repo.repo_name,
118 118 source_ref=title_source_ref,
119 119 target=default_target_repo.repo_name
120 120 )
121 121
122 122 c.default_repo_data = {
123 123 'source_repo_name': source_repo.repo_name,
124 124 'source_refs_json': json.dumps(source_repo_data),
125 125 'target_repo_name': default_target_repo.repo_name,
126 126 'target_refs_json': json.dumps(target_repo_data),
127 127 }
128 128 c.default_source_ref = selected_source_ref
129 129
130 130 return render('/pullrequests/pullrequest.mako')
131 131
132 132 @LoginRequired()
133 133 @NotAnonymous()
134 134 @XHRRequired()
135 135 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
136 136 'repository.admin')
137 137 @jsonify
138 138 def get_repo_refs(self, repo_name, target_repo_name):
139 139 repo = Repository.get_by_repo_name(target_repo_name)
140 140 if not repo:
141 141 raise HTTPNotFound
142 142 return PullRequestModel().generate_repo_data(repo)
143 143
144 144 @LoginRequired()
145 145 @NotAnonymous()
146 146 @XHRRequired()
147 147 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
148 148 'repository.admin')
149 149 @jsonify
150 150 def get_repo_destinations(self, repo_name):
151 151 repo = Repository.get_by_repo_name(repo_name)
152 152 if not repo:
153 153 raise HTTPNotFound
154 154 filter_query = request.GET.get('query')
155 155
156 156 query = Repository.query() \
157 157 .order_by(func.length(Repository.repo_name)) \
158 158 .filter(or_(
159 159 Repository.repo_name == repo.repo_name,
160 160 Repository.fork_id == repo.repo_id))
161 161
162 162 if filter_query:
163 163 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
164 164 query = query.filter(
165 165 Repository.repo_name.ilike(ilike_expression))
166 166
167 167 add_parent = False
168 168 if repo.parent:
169 169 if filter_query in repo.parent.repo_name:
170 170 parent_vcs_obj = repo.parent.scm_instance()
171 171 if parent_vcs_obj and not parent_vcs_obj.is_empty():
172 172 add_parent = True
173 173
174 174 limit = 20 - 1 if add_parent else 20
175 175 all_repos = query.limit(limit).all()
176 176 if add_parent:
177 177 all_repos += [repo.parent]
178 178
179 179 repos = []
180 180 for obj in self.scm_model.get_repos(all_repos):
181 181 repos.append({
182 182 'id': obj['name'],
183 183 'text': obj['name'],
184 184 'type': 'repo',
185 185 'obj': obj['dbrepo']
186 186 })
187 187
188 188 data = {
189 189 'more': False,
190 190 'results': [{
191 191 'text': _('Repositories'),
192 192 'children': repos
193 193 }] if repos else []
194 194 }
195 195 return data
196 196
197 197 @LoginRequired()
198 198 @NotAnonymous()
199 199 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
200 200 'repository.admin')
201 201 @HasAcceptedRepoType('git', 'hg')
202 202 @auth.CSRFRequired()
203 203 def create(self, repo_name):
204 204 repo = Repository.get_by_repo_name(repo_name)
205 205 if not repo:
206 206 raise HTTPNotFound
207 207
208 208 controls = peppercorn.parse(request.POST.items())
209 209
210 210 try:
211 211 _form = PullRequestForm(repo.repo_id)().to_python(controls)
212 212 except formencode.Invalid as errors:
213 213 if errors.error_dict.get('revisions'):
214 214 msg = 'Revisions: %s' % errors.error_dict['revisions']
215 215 elif errors.error_dict.get('pullrequest_title'):
216 216 msg = _('Pull request requires a title with min. 3 chars')
217 217 else:
218 218 msg = _('Error creating pull request: {}').format(errors)
219 219 log.exception(msg)
220 220 h.flash(msg, 'error')
221 221
222 222 # would rather just go back to form ...
223 223 return redirect(url('pullrequest_home', repo_name=repo_name))
224 224
225 225 source_repo = _form['source_repo']
226 226 source_ref = _form['source_ref']
227 227 target_repo = _form['target_repo']
228 228 target_ref = _form['target_ref']
229 229 commit_ids = _form['revisions'][::-1]
230 230
231 231 # find the ancestor for this pr
232 232 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
233 233 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
234 234
235 235 source_scm = source_db_repo.scm_instance()
236 236 target_scm = target_db_repo.scm_instance()
237 237
238 238 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
239 239 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
240 240
241 241 ancestor = source_scm.get_common_ancestor(
242 242 source_commit.raw_id, target_commit.raw_id, target_scm)
243 243
244 244 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
245 245 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
246 246
247 247 pullrequest_title = _form['pullrequest_title']
248 248 title_source_ref = source_ref.split(':', 2)[1]
249 249 if not pullrequest_title:
250 250 pullrequest_title = PullRequestModel().generate_pullrequest_title(
251 251 source=source_repo,
252 252 source_ref=title_source_ref,
253 253 target=target_repo
254 254 )
255 255
256 256 description = _form['pullrequest_desc']
257 257
258 258 get_default_reviewers_data, validate_default_reviewers = \
259 259 PullRequestModel().get_reviewer_functions()
260 260
261 261 # recalculate reviewers logic, to make sure we can validate this
262 262 reviewer_rules = get_default_reviewers_data(
263 263 c.rhodecode_user.get_instance(), source_db_repo,
264 264 source_commit, target_db_repo, target_commit)
265 265
266 266 given_reviewers = _form['review_members']
267 267 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
268 268
269 269 try:
270 270 pull_request = PullRequestModel().create(
271 271 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
272 272 target_ref, commit_ids, reviewers, pullrequest_title,
273 273 description, reviewer_rules
274 274 )
275 275 Session().commit()
276 276 h.flash(_('Successfully opened new pull request'),
277 277 category='success')
278 278 except Exception as e:
279 279 msg = _('Error occurred during creation of this pull request.')
280 280 log.exception(msg)
281 281 h.flash(msg, category='error')
282 282 return redirect(url('pullrequest_home', repo_name=repo_name))
283 283
284 284 raise HTTPFound(
285 285 h.route_path('pullrequest_show', repo_name=target_repo,
286 286 pull_request_id=pull_request.pull_request_id))
287 287
288 288 @LoginRequired()
289 289 @NotAnonymous()
290 290 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
291 291 'repository.admin')
292 292 @auth.CSRFRequired()
293 293 @jsonify
294 294 def update(self, repo_name, pull_request_id):
295 295 pull_request_id = safe_int(pull_request_id)
296 296 pull_request = PullRequest.get_or_404(pull_request_id)
297 297 # only owner or admin can update it
298 298 allowed_to_update = PullRequestModel().check_user_update(
299 299 pull_request, c.rhodecode_user)
300 300 if allowed_to_update:
301 301 controls = peppercorn.parse(request.POST.items())
302 302
303 303 if 'review_members' in controls:
304 304 self._update_reviewers(
305 305 pull_request_id, controls['review_members'],
306 306 pull_request.reviewer_data)
307 307 elif str2bool(request.POST.get('update_commits', 'false')):
308 308 self._update_commits(pull_request)
309 309 elif str2bool(request.POST.get('edit_pull_request', 'false')):
310 310 self._edit_pull_request(pull_request)
311 311 else:
312 312 raise HTTPBadRequest()
313 313 return True
314 314 raise HTTPForbidden()
315 315
316 316 def _edit_pull_request(self, pull_request):
317 317 try:
318 318 PullRequestModel().edit(
319 319 pull_request, request.POST.get('title'),
320 320 request.POST.get('description'), c.rhodecode_user)
321 321 except ValueError:
322 322 msg = _(u'Cannot update closed pull requests.')
323 323 h.flash(msg, category='error')
324 324 return
325 325 else:
326 326 Session().commit()
327 327
328 328 msg = _(u'Pull request title & description updated.')
329 329 h.flash(msg, category='success')
330 330 return
331 331
332 332 def _update_commits(self, pull_request):
333 333 resp = PullRequestModel().update_commits(pull_request)
334 334
335 335 if resp.executed:
336 336
337 337 if resp.target_changed and resp.source_changed:
338 338 changed = 'target and source repositories'
339 339 elif resp.target_changed and not resp.source_changed:
340 340 changed = 'target repository'
341 341 elif not resp.target_changed and resp.source_changed:
342 342 changed = 'source repository'
343 343 else:
344 344 changed = 'nothing'
345 345
346 346 msg = _(
347 347 u'Pull request updated to "{source_commit_id}" with '
348 348 u'{count_added} added, {count_removed} removed commits. '
349 349 u'Source of changes: {change_source}')
350 350 msg = msg.format(
351 351 source_commit_id=pull_request.source_ref_parts.commit_id,
352 352 count_added=len(resp.changes.added),
353 353 count_removed=len(resp.changes.removed),
354 354 change_source=changed)
355 355 h.flash(msg, category='success')
356 356
357 357 registry = get_current_registry()
358 358 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
359 359 channelstream_config = rhodecode_plugins.get('channelstream', {})
360 360 if channelstream_config.get('enabled'):
361 361 message = msg + (
362 362 ' - <a onclick="window.location.reload()">'
363 363 '<strong>{}</strong></a>'.format(_('Reload page')))
364 364 channel = '/repo${}$/pr/{}'.format(
365 365 pull_request.target_repo.repo_name,
366 366 pull_request.pull_request_id
367 367 )
368 368 payload = {
369 369 'type': 'message',
370 370 'user': 'system',
371 371 'exclude_users': [request.user.username],
372 372 'channel': channel,
373 373 'message': {
374 374 'message': message,
375 375 'level': 'success',
376 376 'topic': '/notifications'
377 377 }
378 378 }
379 379 channelstream_request(
380 380 channelstream_config, [payload], '/message',
381 381 raise_exc=False)
382 382 else:
383 383 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
384 384 warning_reasons = [
385 385 UpdateFailureReason.NO_CHANGE,
386 386 UpdateFailureReason.WRONG_REF_TYPE,
387 387 ]
388 388 category = 'warning' if resp.reason in warning_reasons else 'error'
389 389 h.flash(msg, category=category)
390 390
391 391 @auth.CSRFRequired()
392 392 @LoginRequired()
393 393 @NotAnonymous()
394 394 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
395 395 'repository.admin')
396 396 def merge(self, repo_name, pull_request_id):
397 397 """
398 398 POST /{repo_name}/pull-request/{pull_request_id}
399 399
400 400 Merge will perform a server-side merge of the specified
401 401 pull request, if the pull request is approved and mergeable.
402 402 After successful merging, the pull request is automatically
403 403 closed, with a relevant comment.
404 404 """
405 405 pull_request_id = safe_int(pull_request_id)
406 406 pull_request = PullRequest.get_or_404(pull_request_id)
407 407 user = c.rhodecode_user
408 408
409 409 check = MergeCheck.validate(pull_request, user)
410 410 merge_possible = not check.failed
411 411
412 412 for err_type, error_msg in check.errors:
413 413 h.flash(error_msg, category=err_type)
414 414
415 415 if merge_possible:
416 416 log.debug("Pre-conditions checked, trying to merge.")
417 417 extras = vcs_operation_context(
418 418 request.environ, repo_name=pull_request.target_repo.repo_name,
419 419 username=user.username, action='push',
420 420 scm=pull_request.target_repo.repo_type)
421 421 self._merge_pull_request(pull_request, user, extras)
422 422
423 423 raise HTTPFound(
424 424 h.route_path('pullrequest_show',
425 425 repo_name=pull_request.target_repo.repo_name,
426 426 pull_request_id=pull_request.pull_request_id))
427 427
428 428 def _merge_pull_request(self, pull_request, user, extras):
429 429 merge_resp = PullRequestModel().merge(
430 430 pull_request, user, extras=extras)
431 431
432 432 if merge_resp.executed:
433 433 log.debug("The merge was successful, closing the pull request.")
434 434 PullRequestModel().close_pull_request(
435 435 pull_request.pull_request_id, user)
436 436 Session().commit()
437 437 msg = _('Pull request was successfully merged and closed.')
438 438 h.flash(msg, category='success')
439 439 else:
440 440 log.debug(
441 441 "The merge was not successful. Merge response: %s",
442 442 merge_resp)
443 443 msg = PullRequestModel().merge_status_message(
444 444 merge_resp.failure_reason)
445 445 h.flash(msg, category='error')
446 446
447 447 def _update_reviewers(self, pull_request_id, review_members, reviewer_rules):
448 448
449 449 get_default_reviewers_data, validate_default_reviewers = \
450 450 PullRequestModel().get_reviewer_functions()
451 451
452 452 try:
453 453 reviewers = validate_default_reviewers(review_members, reviewer_rules)
454 454 except ValueError as e:
455 455 log.error('Reviewers Validation: {}'.format(e))
456 456 h.flash(e, category='error')
457 457 return
458 458
459 459 PullRequestModel().update_reviewers(
460 460 pull_request_id, reviewers, c.rhodecode_user)
461 461 h.flash(_('Pull request reviewers updated.'), category='success')
462 462 Session().commit()
463 463
464 464 @LoginRequired()
465 465 @NotAnonymous()
466 466 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
467 467 'repository.admin')
468 468 @auth.CSRFRequired()
469 469 @jsonify
470 470 def delete(self, repo_name, pull_request_id):
471 471 pull_request_id = safe_int(pull_request_id)
472 472 pull_request = PullRequest.get_or_404(pull_request_id)
473 473
474 474 pr_closed = pull_request.is_closed()
475 475 allowed_to_delete = PullRequestModel().check_user_delete(
476 476 pull_request, c.rhodecode_user) and not pr_closed
477 477
478 478 # only owner can delete it !
479 479 if allowed_to_delete:
480 480 PullRequestModel().delete(pull_request, c.rhodecode_user)
481 481 Session().commit()
482 482 h.flash(_('Successfully deleted pull request'),
483 483 category='success')
484 484 return redirect(url('my_account_pullrequests'))
485 485
486 486 h.flash(_('Your are not allowed to delete this pull request'),
487 487 category='error')
488 488 raise HTTPForbidden()
489 489
490 490 def _get_pr_version(self, pull_request_id, version=None):
491 491 pull_request_id = safe_int(pull_request_id)
492 492 at_version = None
493 493
494 494 if version and version == 'latest':
495 495 pull_request_ver = PullRequest.get(pull_request_id)
496 496 pull_request_obj = pull_request_ver
497 497 _org_pull_request_obj = pull_request_obj
498 498 at_version = 'latest'
499 499 elif version:
500 500 pull_request_ver = PullRequestVersion.get_or_404(version)
501 501 pull_request_obj = pull_request_ver
502 502 _org_pull_request_obj = pull_request_ver.pull_request
503 503 at_version = pull_request_ver.pull_request_version_id
504 504 else:
505 505 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
506 506 pull_request_id)
507 507
508 508 pull_request_display_obj = PullRequest.get_pr_display_object(
509 509 pull_request_obj, _org_pull_request_obj)
510 510
511 511 return _org_pull_request_obj, pull_request_obj, \
512 512 pull_request_display_obj, at_version
513 513
514 514 def _get_diffset(
515 515 self, source_repo, source_ref_id, target_ref_id, target_commit,
516 516 source_commit, diff_limit, file_limit, display_inline_comments):
517 517 vcs_diff = PullRequestModel().get_diff(
518 518 source_repo, source_ref_id, target_ref_id)
519 519
520 520 diff_processor = diffs.DiffProcessor(
521 521 vcs_diff, format='newdiff', diff_limit=diff_limit,
522 522 file_limit=file_limit, show_full_diff=c.fulldiff)
523 523
524 524 _parsed = diff_processor.prepare()
525 525
526 526 def _node_getter(commit):
527 527 def get_node(fname):
528 528 try:
529 529 return commit.get_node(fname)
530 530 except NodeDoesNotExistError:
531 531 return None
532 532
533 533 return get_node
534 534
535 535 diffset = codeblocks.DiffSet(
536 536 repo_name=c.repo_name,
537 537 source_repo_name=c.source_repo.repo_name,
538 538 source_node_getter=_node_getter(target_commit),
539 539 target_node_getter=_node_getter(source_commit),
540 540 comments=display_inline_comments
541 541 )
542 542 diffset = diffset.render_patchset(
543 543 _parsed, target_commit.raw_id, source_commit.raw_id)
544 544
545 545 return diffset
546 546
547 547 @LoginRequired()
548 548 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
549 549 'repository.admin')
550 550 def show(self, repo_name, pull_request_id):
551 551 pull_request_id = safe_int(pull_request_id)
552 552 version = request.GET.get('version')
553 553 from_version = request.GET.get('from_version') or version
554 554 merge_checks = request.GET.get('merge_checks')
555 555 c.fulldiff = str2bool(request.GET.get('fulldiff'))
556 556
557 557 (pull_request_latest,
558 558 pull_request_at_ver,
559 559 pull_request_display_obj,
560 560 at_version) = self._get_pr_version(
561 561 pull_request_id, version=version)
562 562 pr_closed = pull_request_latest.is_closed()
563 563
564 564 if pr_closed and (version or from_version):
565 565 # not allow to browse versions
566 566 return redirect(h.url('pullrequest_show', repo_name=repo_name,
567 567 pull_request_id=pull_request_id))
568 568
569 569 versions = pull_request_display_obj.versions()
570 570
571 571 c.at_version = at_version
572 572 c.at_version_num = (at_version
573 573 if at_version and at_version != 'latest'
574 574 else None)
575 575 c.at_version_pos = ChangesetComment.get_index_from_version(
576 576 c.at_version_num, versions)
577 577
578 578 (prev_pull_request_latest,
579 579 prev_pull_request_at_ver,
580 580 prev_pull_request_display_obj,
581 581 prev_at_version) = self._get_pr_version(
582 582 pull_request_id, version=from_version)
583 583
584 584 c.from_version = prev_at_version
585 585 c.from_version_num = (prev_at_version
586 586 if prev_at_version and prev_at_version != 'latest'
587 587 else None)
588 588 c.from_version_pos = ChangesetComment.get_index_from_version(
589 589 c.from_version_num, versions)
590 590
591 591 # define if we're in COMPARE mode or VIEW at version mode
592 592 compare = at_version != prev_at_version
593 593
594 594 # pull_requests repo_name we opened it against
595 595 # ie. target_repo must match
596 596 if repo_name != pull_request_at_ver.target_repo.repo_name:
597 597 raise HTTPNotFound
598 598
599 599 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
600 600 pull_request_at_ver)
601 601
602 602 c.pull_request = pull_request_display_obj
603 603 c.pull_request_latest = pull_request_latest
604 604
605 605 if compare or (at_version and not at_version == 'latest'):
606 606 c.allowed_to_change_status = False
607 607 c.allowed_to_update = False
608 608 c.allowed_to_merge = False
609 609 c.allowed_to_delete = False
610 610 c.allowed_to_comment = False
611 611 c.allowed_to_close = False
612 612 else:
613 613 can_change_status = PullRequestModel().check_user_change_status(
614 614 pull_request_at_ver, c.rhodecode_user)
615 615 c.allowed_to_change_status = can_change_status and not pr_closed
616 616
617 617 c.allowed_to_update = PullRequestModel().check_user_update(
618 618 pull_request_latest, c.rhodecode_user) and not pr_closed
619 619 c.allowed_to_merge = PullRequestModel().check_user_merge(
620 620 pull_request_latest, c.rhodecode_user) and not pr_closed
621 621 c.allowed_to_delete = PullRequestModel().check_user_delete(
622 622 pull_request_latest, c.rhodecode_user) and not pr_closed
623 623 c.allowed_to_comment = not pr_closed
624 624 c.allowed_to_close = c.allowed_to_merge and not pr_closed
625 625
626 626 c.forbid_adding_reviewers = False
627 627 c.forbid_author_to_review = False
628 628 c.forbid_commit_author_to_review = False
629 629
630 630 if pull_request_latest.reviewer_data and \
631 631 'rules' in pull_request_latest.reviewer_data:
632 632 rules = pull_request_latest.reviewer_data['rules'] or {}
633 633 try:
634 634 c.forbid_adding_reviewers = rules.get(
635 635 'forbid_adding_reviewers')
636 636 c.forbid_author_to_review = rules.get(
637 637 'forbid_author_to_review')
638 638 c.forbid_commit_author_to_review = rules.get(
639 639 'forbid_commit_author_to_review')
640 640 except Exception:
641 641 pass
642 642
643 643 # check merge capabilities
644 644 _merge_check = MergeCheck.validate(
645 645 pull_request_latest, user=c.rhodecode_user)
646 646 c.pr_merge_errors = _merge_check.error_details
647 647 c.pr_merge_possible = not _merge_check.failed
648 648 c.pr_merge_message = _merge_check.merge_msg
649 649
650 650 c.pull_request_review_status = _merge_check.review_status
651 651 if merge_checks:
652 652 return render('/pullrequests/pullrequest_merge_checks.mako')
653 653
654 654 comments_model = CommentsModel()
655 655
656 656 # reviewers and statuses
657 657 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
658 658 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
659 659
660 660 # GENERAL COMMENTS with versions #
661 661 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
662 662 q = q.order_by(ChangesetComment.comment_id.asc())
663 663 general_comments = q
664 664
665 665 # pick comments we want to render at current version
666 666 c.comment_versions = comments_model.aggregate_comments(
667 667 general_comments, versions, c.at_version_num)
668 668 c.comments = c.comment_versions[c.at_version_num]['until']
669 669
670 670 # INLINE COMMENTS with versions #
671 671 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
672 672 q = q.order_by(ChangesetComment.comment_id.asc())
673 673 inline_comments = q
674 674
675 675 c.inline_versions = comments_model.aggregate_comments(
676 676 inline_comments, versions, c.at_version_num, inline=True)
677 677
678 678 # inject latest version
679 679 latest_ver = PullRequest.get_pr_display_object(
680 680 pull_request_latest, pull_request_latest)
681 681
682 682 c.versions = versions + [latest_ver]
683 683
684 684 # if we use version, then do not show later comments
685 685 # than current version
686 686 display_inline_comments = collections.defaultdict(
687 687 lambda: collections.defaultdict(list))
688 688 for co in inline_comments:
689 689 if c.at_version_num:
690 690 # pick comments that are at least UPTO given version, so we
691 691 # don't render comments for higher version
692 692 should_render = co.pull_request_version_id and \
693 693 co.pull_request_version_id <= c.at_version_num
694 694 else:
695 695 # showing all, for 'latest'
696 696 should_render = True
697 697
698 698 if should_render:
699 699 display_inline_comments[co.f_path][co.line_no].append(co)
700 700
701 701 # load diff data into template context, if we use compare mode then
702 702 # diff is calculated based on changes between versions of PR
703 703
704 704 source_repo = pull_request_at_ver.source_repo
705 705 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
706 706
707 707 target_repo = pull_request_at_ver.target_repo
708 708 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
709 709
710 710 if compare:
711 711 # in compare switch the diff base to latest commit from prev version
712 712 target_ref_id = prev_pull_request_display_obj.revisions[0]
713 713
714 714 # despite opening commits for bookmarks/branches/tags, we always
715 715 # convert this to rev to prevent changes after bookmark or branch change
716 716 c.source_ref_type = 'rev'
717 717 c.source_ref = source_ref_id
718 718
719 719 c.target_ref_type = 'rev'
720 720 c.target_ref = target_ref_id
721 721
722 722 c.source_repo = source_repo
723 723 c.target_repo = target_repo
724 724
725 725 # diff_limit is the old behavior, will cut off the whole diff
726 726 # if the limit is applied otherwise will just hide the
727 727 # big files from the front-end
728 728 diff_limit = self.cut_off_limit_diff
729 729 file_limit = self.cut_off_limit_file
730 730
731 731 c.commit_ranges = []
732 732 source_commit = EmptyCommit()
733 733 target_commit = EmptyCommit()
734 734 c.missing_requirements = False
735 735
736 736 source_scm = source_repo.scm_instance()
737 737 target_scm = target_repo.scm_instance()
738 738
739 739 # try first shadow repo, fallback to regular repo
740 740 try:
741 741 commits_source_repo = pull_request_latest.get_shadow_repo()
742 742 except Exception:
743 743 log.debug('Failed to get shadow repo', exc_info=True)
744 744 commits_source_repo = source_scm
745 745
746 746 c.commits_source_repo = commits_source_repo
747 747 commit_cache = {}
748 748 try:
749 749 pre_load = ["author", "branch", "date", "message"]
750 750 show_revs = pull_request_at_ver.revisions
751 751 for rev in show_revs:
752 752 comm = commits_source_repo.get_commit(
753 753 commit_id=rev, pre_load=pre_load)
754 754 c.commit_ranges.append(comm)
755 755 commit_cache[comm.raw_id] = comm
756 756
757 757 # Order here matters, we first need to get target, and then
758 758 # the source
759 759 target_commit = commits_source_repo.get_commit(
760 760 commit_id=safe_str(target_ref_id))
761 761
762 762 source_commit = commits_source_repo.get_commit(
763 763 commit_id=safe_str(source_ref_id))
764 764
765 765 except CommitDoesNotExistError:
766 766 log.warning(
767 767 'Failed to get commit from `{}` repo'.format(
768 768 commits_source_repo), exc_info=True)
769 769 except RepositoryRequirementError:
770 770 log.warning(
771 771 'Failed to get all required data from repo', exc_info=True)
772 772 c.missing_requirements = True
773 773
774 774 c.ancestor = None # set it to None, to hide it from PR view
775 775
776 776 try:
777 777 ancestor_id = source_scm.get_common_ancestor(
778 778 source_commit.raw_id, target_commit.raw_id, target_scm)
779 779 c.ancestor_commit = source_scm.get_commit(ancestor_id)
780 780 except Exception:
781 781 c.ancestor_commit = None
782 782
783 783 c.statuses = source_repo.statuses(
784 784 [x.raw_id for x in c.commit_ranges])
785 785
786 786 # auto collapse if we have more than limit
787 787 collapse_limit = diffs.DiffProcessor._collapse_commits_over
788 788 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
789 789 c.compare_mode = compare
790 790
791 791 c.missing_commits = False
792 792 if (c.missing_requirements or isinstance(source_commit, EmptyCommit)
793 793 or source_commit == target_commit):
794 794
795 795 c.missing_commits = True
796 796 else:
797 797
798 798 c.diffset = self._get_diffset(
799 799 commits_source_repo, source_ref_id, target_ref_id,
800 800 target_commit, source_commit,
801 801 diff_limit, file_limit, display_inline_comments)
802 802
803 803 c.limited_diff = c.diffset.limited_diff
804 804
805 805 # calculate removed files that are bound to comments
806 806 comment_deleted_files = [
807 807 fname for fname in display_inline_comments
808 808 if fname not in c.diffset.file_stats]
809 809
810 810 c.deleted_files_comments = collections.defaultdict(dict)
811 811 for fname, per_line_comments in display_inline_comments.items():
812 812 if fname in comment_deleted_files:
813 813 c.deleted_files_comments[fname]['stats'] = 0
814 814 c.deleted_files_comments[fname]['comments'] = list()
815 815 for lno, comments in per_line_comments.items():
816 816 c.deleted_files_comments[fname]['comments'].extend(
817 817 comments)
818 818
819 819 # this is a hack to properly display links, when creating PR, the
820 820 # compare view and others uses different notation, and
821 821 # compare_commits.mako renders links based on the target_repo.
822 822 # We need to swap that here to generate it properly on the html side
823 823 c.target_repo = c.source_repo
824 824
825 825 c.commit_statuses = ChangesetStatus.STATUSES
826 826
827 827 c.show_version_changes = not pr_closed
828 828 if c.show_version_changes:
829 829 cur_obj = pull_request_at_ver
830 830 prev_obj = prev_pull_request_at_ver
831 831
832 832 old_commit_ids = prev_obj.revisions
833 833 new_commit_ids = cur_obj.revisions
834 834 commit_changes = PullRequestModel()._calculate_commit_id_changes(
835 835 old_commit_ids, new_commit_ids)
836 836 c.commit_changes_summary = commit_changes
837 837
838 838 # calculate the diff for commits between versions
839 839 c.commit_changes = []
840 840 mark = lambda cs, fw: list(
841 841 h.itertools.izip_longest([], cs, fillvalue=fw))
842 842 for c_type, raw_id in mark(commit_changes.added, 'a') \
843 843 + mark(commit_changes.removed, 'r') \
844 844 + mark(commit_changes.common, 'c'):
845 845
846 846 if raw_id in commit_cache:
847 847 commit = commit_cache[raw_id]
848 848 else:
849 849 try:
850 850 commit = commits_source_repo.get_commit(raw_id)
851 851 except CommitDoesNotExistError:
852 852 # in case we fail extracting still use "dummy" commit
853 853 # for display in commit diff
854 854 commit = h.AttributeDict(
855 855 {'raw_id': raw_id,
856 856 'message': 'EMPTY or MISSING COMMIT'})
857 857 c.commit_changes.append([c_type, commit])
858 858
859 859 # current user review statuses for each version
860 860 c.review_versions = {}
861 861 if c.rhodecode_user.user_id in allowed_reviewers:
862 862 for co in general_comments:
863 863 if co.author.user_id == c.rhodecode_user.user_id:
864 864 # each comment has a status change
865 865 status = co.status_change
866 866 if status:
867 867 _ver_pr = status[0].comment.pull_request_version_id
868 868 c.review_versions[_ver_pr] = status[0]
869 869
870 870 return render('/pullrequests/pullrequest_show.mako')
871 871
872 872 @LoginRequired()
873 873 @NotAnonymous()
874 874 @HasRepoPermissionAnyDecorator(
875 875 'repository.read', 'repository.write', 'repository.admin')
876 876 @auth.CSRFRequired()
877 877 @jsonify
878 878 def comment(self, repo_name, pull_request_id):
879 879 pull_request_id = safe_int(pull_request_id)
880 880 pull_request = PullRequest.get_or_404(pull_request_id)
881 881 if pull_request.is_closed():
882 882 log.debug('comment: forbidden because pull request is closed')
883 883 raise HTTPForbidden()
884 884
885 885 status = request.POST.get('changeset_status', None)
886 886 text = request.POST.get('text')
887 887 comment_type = request.POST.get('comment_type')
888 888 resolves_comment_id = request.POST.get('resolves_comment_id', None)
889 889 close_pull_request = request.POST.get('close_pull_request')
890 890
891 891 # the logic here should work like following, if we submit close
892 892 # pr comment, use `close_pull_request_with_comment` function
893 893 # else handle regular comment logic
894 894 user = c.rhodecode_user
895 895 repo = c.rhodecode_db_repo
896 896
897 897 if close_pull_request:
898 898 # only owner or admin or person with write permissions
899 899 allowed_to_close = PullRequestModel().check_user_update(
900 900 pull_request, c.rhodecode_user)
901 901 if not allowed_to_close:
902 902 log.debug('comment: forbidden because not allowed to close '
903 903 'pull request %s', pull_request_id)
904 904 raise HTTPForbidden()
905 905 comment, status = PullRequestModel().close_pull_request_with_comment(
906 906 pull_request, user, repo, message=text)
907 907 Session().flush()
908 908 events.trigger(
909 909 events.PullRequestCommentEvent(pull_request, comment))
910 910
911 911 else:
912 912 # regular comment case, could be inline, or one with status.
913 913 # for that one we check also permissions
914 914
915 915 allowed_to_change_status = PullRequestModel().check_user_change_status(
916 916 pull_request, c.rhodecode_user)
917 917
918 918 if status and allowed_to_change_status:
919 919 message = (_('Status change %(transition_icon)s %(status)s')
920 920 % {'transition_icon': '>',
921 921 'status': ChangesetStatus.get_status_lbl(status)})
922 922 text = text or message
923 923
924 924 comment = CommentsModel().create(
925 925 text=text,
926 926 repo=c.rhodecode_db_repo.repo_id,
927 927 user=c.rhodecode_user.user_id,
928 928 pull_request=pull_request_id,
929 929 f_path=request.POST.get('f_path'),
930 930 line_no=request.POST.get('line'),
931 931 status_change=(ChangesetStatus.get_status_lbl(status)
932 932 if status and allowed_to_change_status else None),
933 933 status_change_type=(status
934 934 if status and allowed_to_change_status else None),
935 935 comment_type=comment_type,
936 936 resolves_comment_id=resolves_comment_id
937 937 )
938 938
939 939 if allowed_to_change_status:
940 940 # calculate old status before we change it
941 941 old_calculated_status = pull_request.calculated_review_status()
942 942
943 943 # get status if set !
944 944 if status:
945 945 ChangesetStatusModel().set_status(
946 946 c.rhodecode_db_repo.repo_id,
947 947 status,
948 948 c.rhodecode_user.user_id,
949 949 comment,
950 950 pull_request=pull_request_id
951 951 )
952 952
953 953 Session().flush()
954 954 events.trigger(
955 955 events.PullRequestCommentEvent(pull_request, comment))
956 956
957 957 # we now calculate the status of pull request, and based on that
958 958 # calculation we set the commits status
959 959 calculated_status = pull_request.calculated_review_status()
960 960 if old_calculated_status != calculated_status:
961 961 PullRequestModel()._trigger_pull_request_hook(
962 962 pull_request, c.rhodecode_user, 'review_status_change')
963 963
964 964 Session().commit()
965 965
966 966 if not request.is_xhr:
967 967 raise HTTPFound(
968 968 h.route_path('pullrequest_show',
969 969 repo_name=repo_name,
970 970 pull_request_id=pull_request_id))
971 971
972 972 data = {
973 973 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
974 974 }
975 975 if comment:
976 976 c.co = comment
977 977 rendered_comment = render('changeset/changeset_comment_block.mako')
978 978 data.update(comment.get_dict())
979 979 data.update({'rendered_text': rendered_comment})
980 980
981 981 return data
982 982
983 983 @LoginRequired()
984 984 @NotAnonymous()
985 985 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
986 986 'repository.admin')
987 987 @auth.CSRFRequired()
988 988 @jsonify
989 989 def delete_comment(self, repo_name, comment_id):
990 990 comment = ChangesetComment.get_or_404(safe_int(comment_id))
991 991 if not comment:
992 992 log.debug('Comment with id:%s not found, skipping', comment_id)
993 993 # comment already deleted in another call probably
994 994 return True
995 995
996 996 if comment.pull_request.is_closed():
997 997 # don't allow deleting comments on closed pull request
998 998 raise HTTPForbidden()
999 999
1000 1000 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1001 1001 super_admin = h.HasPermissionAny('hg.admin')()
1002 1002 comment_owner = comment.author.user_id == c.rhodecode_user.user_id
1003 1003 is_repo_comment = comment.repo.repo_name == c.repo_name
1004 1004 comment_repo_admin = is_repo_admin and is_repo_comment
1005 1005
1006 1006 if super_admin or comment_owner or comment_repo_admin:
1007 1007 old_calculated_status = comment.pull_request.calculated_review_status()
1008 1008 CommentsModel().delete(comment=comment, user=c.rhodecode_user)
1009 1009 Session().commit()
1010 1010 calculated_status = comment.pull_request.calculated_review_status()
1011 1011 if old_calculated_status != calculated_status:
1012 1012 PullRequestModel()._trigger_pull_request_hook(
1013 1013 comment.pull_request, c.rhodecode_user, 'review_status_change')
1014 1014 return True
1015 1015 else:
1016 raise HTTPForbidden()
1016 log.warning('No permissions for user %s to delete comment_id: %s',
1017 c.rhodecode_user, comment_id)
1018 raise HTTPNotFound()
General Comments 0
You need to be logged in to leave comments. Login now