##// END OF EJS Templates
commits: add protection against 500 errors on commit children/parents page.
marcink -
r2150:82423302 default
parent child Browse files
Show More
@@ -1,554 +1,563 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 import logging
23 23 import collections
24 24
25 25 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
26 26 from pyramid.view import view_config
27 27 from pyramid.renderers import render
28 28 from pyramid.response import Response
29 29
30 30 from rhodecode.apps._base import RepoAppView
31 31
32 32 from rhodecode.lib import diffs, codeblocks
33 33 from rhodecode.lib.auth import (
34 34 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
35 35
36 36 from rhodecode.lib.compat import OrderedDict
37 37 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
38 38 import rhodecode.lib.helpers as h
39 39 from rhodecode.lib.utils2 import safe_unicode
40 40 from rhodecode.lib.vcs.backends.base import EmptyCommit
41 41 from rhodecode.lib.vcs.exceptions import (
42 42 RepositoryError, CommitDoesNotExistError, NodeDoesNotExistError)
43 43 from rhodecode.model.db import ChangesetComment, ChangesetStatus
44 44 from rhodecode.model.changeset_status import ChangesetStatusModel
45 45 from rhodecode.model.comment import CommentsModel
46 46 from rhodecode.model.meta import Session
47 47
48 48
49 49 log = logging.getLogger(__name__)
50 50
51 51
52 52 def _update_with_GET(params, request):
53 53 for k in ['diff1', 'diff2', 'diff']:
54 54 params[k] += request.GET.getall(k)
55 55
56 56
57 57 def get_ignore_ws(fid, request):
58 58 ig_ws_global = request.GET.get('ignorews')
59 59 ig_ws = filter(lambda k: k.startswith('WS'), request.GET.getall(fid))
60 60 if ig_ws:
61 61 try:
62 62 return int(ig_ws[0].split(':')[-1])
63 63 except Exception:
64 64 pass
65 65 return ig_ws_global
66 66
67 67
68 68 def _ignorews_url(request, fileid=None):
69 69 _ = request.translate
70 70 fileid = str(fileid) if fileid else None
71 71 params = collections.defaultdict(list)
72 72 _update_with_GET(params, request)
73 73 label = _('Show whitespace')
74 74 tooltiplbl = _('Show whitespace for all diffs')
75 75 ig_ws = get_ignore_ws(fileid, request)
76 76 ln_ctx = get_line_ctx(fileid, request)
77 77
78 78 if ig_ws is None:
79 79 params['ignorews'] += [1]
80 80 label = _('Ignore whitespace')
81 81 tooltiplbl = _('Ignore whitespace for all diffs')
82 82 ctx_key = 'context'
83 83 ctx_val = ln_ctx
84 84
85 85 # if we have passed in ln_ctx pass it along to our params
86 86 if ln_ctx:
87 87 params[ctx_key] += [ctx_val]
88 88
89 89 if fileid:
90 90 params['anchor'] = 'a_' + fileid
91 91 return h.link_to(label, request.current_route_path(_query=params),
92 92 title=tooltiplbl, class_='tooltip')
93 93
94 94
95 95 def get_line_ctx(fid, request):
96 96 ln_ctx_global = request.GET.get('context')
97 97 if fid:
98 98 ln_ctx = filter(lambda k: k.startswith('C'), request.GET.getall(fid))
99 99 else:
100 100 _ln_ctx = filter(lambda k: k.startswith('C'), request.GET)
101 101 ln_ctx = request.GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
102 102 if ln_ctx:
103 103 ln_ctx = [ln_ctx]
104 104
105 105 if ln_ctx:
106 106 retval = ln_ctx[0].split(':')[-1]
107 107 else:
108 108 retval = ln_ctx_global
109 109
110 110 try:
111 111 return int(retval)
112 112 except Exception:
113 113 return 3
114 114
115 115
116 116 def _context_url(request, fileid=None):
117 117 """
118 118 Generates a url for context lines.
119 119
120 120 :param fileid:
121 121 """
122 122
123 123 _ = request.translate
124 124 fileid = str(fileid) if fileid else None
125 125 ig_ws = get_ignore_ws(fileid, request)
126 126 ln_ctx = (get_line_ctx(fileid, request) or 3) * 2
127 127
128 128 params = collections.defaultdict(list)
129 129 _update_with_GET(params, request)
130 130
131 131 if ln_ctx > 0:
132 132 params['context'] += [ln_ctx]
133 133
134 134 if ig_ws:
135 135 ig_ws_key = 'ignorews'
136 136 ig_ws_val = 1
137 137 params[ig_ws_key] += [ig_ws_val]
138 138
139 139 lbl = _('Increase context')
140 140 tooltiplbl = _('Increase context for all diffs')
141 141
142 142 if fileid:
143 143 params['anchor'] = 'a_' + fileid
144 144 return h.link_to(lbl, request.current_route_path(_query=params),
145 145 title=tooltiplbl, class_='tooltip')
146 146
147 147
148 148 class RepoCommitsView(RepoAppView):
149 149 def load_default_context(self):
150 150 c = self._get_local_tmpl_context(include_app_defaults=True)
151 151 c.rhodecode_repo = self.rhodecode_vcs_repo
152 152
153 153 self._register_global_c(c)
154 154 return c
155 155
156 156 def _commit(self, commit_id_range, method):
157 157 _ = self.request.translate
158 158 c = self.load_default_context()
159 159 c.ignorews_url = _ignorews_url
160 160 c.context_url = _context_url
161 161 c.fulldiff = self.request.GET.get('fulldiff')
162 162
163 163 # fetch global flags of ignore ws or context lines
164 164 context_lcl = get_line_ctx('', self.request)
165 165 ign_whitespace_lcl = get_ignore_ws('', self.request)
166 166
167 167 # diff_limit will cut off the whole diff if the limit is applied
168 168 # otherwise it will just hide the big files from the front-end
169 169 diff_limit = c.visual.cut_off_limit_diff
170 170 file_limit = c.visual.cut_off_limit_file
171 171
172 172 # get ranges of commit ids if preset
173 173 commit_range = commit_id_range.split('...')[:2]
174 174
175 175 try:
176 176 pre_load = ['affected_files', 'author', 'branch', 'date',
177 177 'message', 'parents']
178 178
179 179 if len(commit_range) == 2:
180 180 commits = self.rhodecode_vcs_repo.get_commits(
181 181 start_id=commit_range[0], end_id=commit_range[1],
182 182 pre_load=pre_load)
183 183 commits = list(commits)
184 184 else:
185 185 commits = [self.rhodecode_vcs_repo.get_commit(
186 186 commit_id=commit_id_range, pre_load=pre_load)]
187 187
188 188 c.commit_ranges = commits
189 189 if not c.commit_ranges:
190 190 raise RepositoryError(
191 191 'The commit range returned an empty result')
192 192 except CommitDoesNotExistError:
193 193 msg = _('No such commit exists for this repository')
194 194 h.flash(msg, category='error')
195 195 raise HTTPNotFound()
196 196 except Exception:
197 197 log.exception("General failure")
198 198 raise HTTPNotFound()
199 199
200 200 c.changes = OrderedDict()
201 201 c.lines_added = 0
202 202 c.lines_deleted = 0
203 203
204 204 # auto collapse if we have more than limit
205 205 collapse_limit = diffs.DiffProcessor._collapse_commits_over
206 206 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
207 207
208 208 c.commit_statuses = ChangesetStatus.STATUSES
209 209 c.inline_comments = []
210 210 c.files = []
211 211
212 212 c.statuses = []
213 213 c.comments = []
214 214 c.unresolved_comments = []
215 215 if len(c.commit_ranges) == 1:
216 216 commit = c.commit_ranges[0]
217 217 c.comments = CommentsModel().get_comments(
218 218 self.db_repo.repo_id,
219 219 revision=commit.raw_id)
220 220 c.statuses.append(ChangesetStatusModel().get_status(
221 221 self.db_repo.repo_id, commit.raw_id))
222 222 # comments from PR
223 223 statuses = ChangesetStatusModel().get_statuses(
224 224 self.db_repo.repo_id, commit.raw_id,
225 225 with_revisions=True)
226 226 prs = set(st.pull_request for st in statuses
227 227 if st.pull_request is not None)
228 228 # from associated statuses, check the pull requests, and
229 229 # show comments from them
230 230 for pr in prs:
231 231 c.comments.extend(pr.comments)
232 232
233 233 c.unresolved_comments = CommentsModel()\
234 234 .get_commit_unresolved_todos(commit.raw_id)
235 235
236 236 diff = None
237 237 # Iterate over ranges (default commit view is always one commit)
238 238 for commit in c.commit_ranges:
239 239 c.changes[commit.raw_id] = []
240 240
241 241 commit2 = commit
242 242 commit1 = commit.parents[0] if commit.parents else EmptyCommit()
243 243
244 244 _diff = self.rhodecode_vcs_repo.get_diff(
245 245 commit1, commit2,
246 246 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
247 247 diff_processor = diffs.DiffProcessor(
248 248 _diff, format='newdiff', diff_limit=diff_limit,
249 249 file_limit=file_limit, show_full_diff=c.fulldiff)
250 250
251 251 commit_changes = OrderedDict()
252 252 if method == 'show':
253 253 _parsed = diff_processor.prepare()
254 254 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
255 255
256 256 _parsed = diff_processor.prepare()
257 257
258 258 def _node_getter(commit):
259 259 def get_node(fname):
260 260 try:
261 261 return commit.get_node(fname)
262 262 except NodeDoesNotExistError:
263 263 return None
264 264 return get_node
265 265
266 266 inline_comments = CommentsModel().get_inline_comments(
267 267 self.db_repo.repo_id, revision=commit.raw_id)
268 268 c.inline_cnt = CommentsModel().get_inline_comments_count(
269 269 inline_comments)
270 270
271 271 diffset = codeblocks.DiffSet(
272 272 repo_name=self.db_repo_name,
273 273 source_node_getter=_node_getter(commit1),
274 274 target_node_getter=_node_getter(commit2),
275 275 comments=inline_comments)
276 276 diffset = diffset.render_patchset(
277 277 _parsed, commit1.raw_id, commit2.raw_id)
278 278
279 279 c.changes[commit.raw_id] = diffset
280 280 else:
281 281 # downloads/raw we only need RAW diff nothing else
282 282 diff = diff_processor.as_raw()
283 283 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
284 284
285 285 # sort comments by how they were generated
286 286 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
287 287
288 288 if len(c.commit_ranges) == 1:
289 289 c.commit = c.commit_ranges[0]
290 290 c.parent_tmpl = ''.join(
291 291 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
292 292
293 293 if method == 'download':
294 294 response = Response(diff)
295 295 response.content_type = 'text/plain'
296 296 response.content_disposition = (
297 297 'attachment; filename=%s.diff' % commit_id_range[:12])
298 298 return response
299 299 elif method == 'patch':
300 300 c.diff = safe_unicode(diff)
301 301 patch = render(
302 302 'rhodecode:templates/changeset/patch_changeset.mako',
303 303 self._get_template_context(c), self.request)
304 304 response = Response(patch)
305 305 response.content_type = 'text/plain'
306 306 return response
307 307 elif method == 'raw':
308 308 response = Response(diff)
309 309 response.content_type = 'text/plain'
310 310 return response
311 311 elif method == 'show':
312 312 if len(c.commit_ranges) == 1:
313 313 html = render(
314 314 'rhodecode:templates/changeset/changeset.mako',
315 315 self._get_template_context(c), self.request)
316 316 return Response(html)
317 317 else:
318 318 c.ancestor = None
319 319 c.target_repo = self.db_repo
320 320 html = render(
321 321 'rhodecode:templates/changeset/changeset_range.mako',
322 322 self._get_template_context(c), self.request)
323 323 return Response(html)
324 324
325 325 raise HTTPBadRequest()
326 326
327 327 @LoginRequired()
328 328 @HasRepoPermissionAnyDecorator(
329 329 'repository.read', 'repository.write', 'repository.admin')
330 330 @view_config(
331 331 route_name='repo_commit', request_method='GET',
332 332 renderer=None)
333 333 def repo_commit_show(self):
334 334 commit_id = self.request.matchdict['commit_id']
335 335 return self._commit(commit_id, method='show')
336 336
337 337 @LoginRequired()
338 338 @HasRepoPermissionAnyDecorator(
339 339 'repository.read', 'repository.write', 'repository.admin')
340 340 @view_config(
341 341 route_name='repo_commit_raw', request_method='GET',
342 342 renderer=None)
343 343 @view_config(
344 344 route_name='repo_commit_raw_deprecated', request_method='GET',
345 345 renderer=None)
346 346 def repo_commit_raw(self):
347 347 commit_id = self.request.matchdict['commit_id']
348 348 return self._commit(commit_id, method='raw')
349 349
350 350 @LoginRequired()
351 351 @HasRepoPermissionAnyDecorator(
352 352 'repository.read', 'repository.write', 'repository.admin')
353 353 @view_config(
354 354 route_name='repo_commit_patch', request_method='GET',
355 355 renderer=None)
356 356 def repo_commit_patch(self):
357 357 commit_id = self.request.matchdict['commit_id']
358 358 return self._commit(commit_id, method='patch')
359 359
360 360 @LoginRequired()
361 361 @HasRepoPermissionAnyDecorator(
362 362 'repository.read', 'repository.write', 'repository.admin')
363 363 @view_config(
364 364 route_name='repo_commit_download', request_method='GET',
365 365 renderer=None)
366 366 def repo_commit_download(self):
367 367 commit_id = self.request.matchdict['commit_id']
368 368 return self._commit(commit_id, method='download')
369 369
370 370 @LoginRequired()
371 371 @NotAnonymous()
372 372 @HasRepoPermissionAnyDecorator(
373 373 'repository.read', 'repository.write', 'repository.admin')
374 374 @CSRFRequired()
375 375 @view_config(
376 376 route_name='repo_commit_comment_create', request_method='POST',
377 377 renderer='json_ext')
378 378 def repo_commit_comment_create(self):
379 379 _ = self.request.translate
380 380 commit_id = self.request.matchdict['commit_id']
381 381
382 382 c = self.load_default_context()
383 383 status = self.request.POST.get('changeset_status', None)
384 384 text = self.request.POST.get('text')
385 385 comment_type = self.request.POST.get('comment_type')
386 386 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
387 387
388 388 if status:
389 389 text = text or (_('Status change %(transition_icon)s %(status)s')
390 390 % {'transition_icon': '>',
391 391 'status': ChangesetStatus.get_status_lbl(status)})
392 392
393 393 multi_commit_ids = []
394 394 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
395 395 if _commit_id not in ['', None, EmptyCommit.raw_id]:
396 396 if _commit_id not in multi_commit_ids:
397 397 multi_commit_ids.append(_commit_id)
398 398
399 399 commit_ids = multi_commit_ids or [commit_id]
400 400
401 401 comment = None
402 402 for current_id in filter(None, commit_ids):
403 403 comment = CommentsModel().create(
404 404 text=text,
405 405 repo=self.db_repo.repo_id,
406 406 user=self._rhodecode_db_user.user_id,
407 407 commit_id=current_id,
408 408 f_path=self.request.POST.get('f_path'),
409 409 line_no=self.request.POST.get('line'),
410 410 status_change=(ChangesetStatus.get_status_lbl(status)
411 411 if status else None),
412 412 status_change_type=status,
413 413 comment_type=comment_type,
414 414 resolves_comment_id=resolves_comment_id
415 415 )
416 416
417 417 # get status if set !
418 418 if status:
419 419 # if latest status was from pull request and it's closed
420 420 # disallow changing status !
421 421 # dont_allow_on_closed_pull_request = True !
422 422
423 423 try:
424 424 ChangesetStatusModel().set_status(
425 425 self.db_repo.repo_id,
426 426 status,
427 427 self._rhodecode_db_user.user_id,
428 428 comment,
429 429 revision=current_id,
430 430 dont_allow_on_closed_pull_request=True
431 431 )
432 432 except StatusChangeOnClosedPullRequestError:
433 433 msg = _('Changing the status of a commit associated with '
434 434 'a closed pull request is not allowed')
435 435 log.exception(msg)
436 436 h.flash(msg, category='warning')
437 437 raise HTTPFound(h.route_path(
438 438 'repo_commit', repo_name=self.db_repo_name,
439 439 commit_id=current_id))
440 440
441 441 # finalize, commit and redirect
442 442 Session().commit()
443 443
444 444 data = {
445 445 'target_id': h.safeid(h.safe_unicode(
446 446 self.request.POST.get('f_path'))),
447 447 }
448 448 if comment:
449 449 c.co = comment
450 450 rendered_comment = render(
451 451 'rhodecode:templates/changeset/changeset_comment_block.mako',
452 452 self._get_template_context(c), self.request)
453 453
454 454 data.update(comment.get_dict())
455 455 data.update({'rendered_text': rendered_comment})
456 456
457 457 return data
458 458
459 459 @LoginRequired()
460 460 @NotAnonymous()
461 461 @HasRepoPermissionAnyDecorator(
462 462 'repository.read', 'repository.write', 'repository.admin')
463 463 @CSRFRequired()
464 464 @view_config(
465 465 route_name='repo_commit_comment_preview', request_method='POST',
466 466 renderer='string', xhr=True)
467 467 def repo_commit_comment_preview(self):
468 468 # Technically a CSRF token is not needed as no state changes with this
469 469 # call. However, as this is a POST is better to have it, so automated
470 470 # tools don't flag it as potential CSRF.
471 471 # Post is required because the payload could be bigger than the maximum
472 472 # allowed by GET.
473 473
474 474 text = self.request.POST.get('text')
475 475 renderer = self.request.POST.get('renderer') or 'rst'
476 476 if text:
477 477 return h.render(text, renderer=renderer, mentions=True)
478 478 return ''
479 479
480 480 @LoginRequired()
481 481 @NotAnonymous()
482 482 @HasRepoPermissionAnyDecorator(
483 483 'repository.read', 'repository.write', 'repository.admin')
484 484 @CSRFRequired()
485 485 @view_config(
486 486 route_name='repo_commit_comment_delete', request_method='POST',
487 487 renderer='json_ext')
488 488 def repo_commit_comment_delete(self):
489 489 commit_id = self.request.matchdict['commit_id']
490 490 comment_id = self.request.matchdict['comment_id']
491 491
492 492 comment = ChangesetComment.get_or_404(comment_id)
493 493 if not comment:
494 494 log.debug('Comment with id:%s not found, skipping', comment_id)
495 495 # comment already deleted in another call probably
496 496 return True
497 497
498 498 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
499 499 super_admin = h.HasPermissionAny('hg.admin')()
500 500 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
501 501 is_repo_comment = comment.repo.repo_name == self.db_repo_name
502 502 comment_repo_admin = is_repo_admin and is_repo_comment
503 503
504 504 if super_admin or comment_owner or comment_repo_admin:
505 505 CommentsModel().delete(comment=comment, user=self._rhodecode_db_user)
506 506 Session().commit()
507 507 return True
508 508 else:
509 509 log.warning('No permissions for user %s to delete comment_id: %s',
510 510 self._rhodecode_db_user, comment_id)
511 511 raise HTTPNotFound()
512 512
513 513 @LoginRequired()
514 514 @HasRepoPermissionAnyDecorator(
515 515 'repository.read', 'repository.write', 'repository.admin')
516 516 @view_config(
517 517 route_name='repo_commit_data', request_method='GET',
518 518 renderer='json_ext', xhr=True)
519 519 def repo_commit_data(self):
520 520 commit_id = self.request.matchdict['commit_id']
521 521 self.load_default_context()
522 522
523 523 try:
524 524 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
525 525 except CommitDoesNotExistError as e:
526 526 return EmptyCommit(message=str(e))
527 527
528 528 @LoginRequired()
529 529 @HasRepoPermissionAnyDecorator(
530 530 'repository.read', 'repository.write', 'repository.admin')
531 531 @view_config(
532 532 route_name='repo_commit_children', request_method='GET',
533 533 renderer='json_ext', xhr=True)
534 534 def repo_commit_children(self):
535 535 commit_id = self.request.matchdict['commit_id']
536 536 self.load_default_context()
537 537
538 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
539 result = {"results": commit.children}
538 try:
539 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
540 children = commit.children
541 except CommitDoesNotExistError:
542 children = []
543
544 result = {"results": children}
540 545 return result
541 546
542 547 @LoginRequired()
543 548 @HasRepoPermissionAnyDecorator(
544 549 'repository.read', 'repository.write', 'repository.admin')
545 550 @view_config(
546 551 route_name='repo_commit_parents', request_method='GET',
547 552 renderer='json_ext')
548 553 def repo_commit_parents(self):
549 554 commit_id = self.request.matchdict['commit_id']
550 555 self.load_default_context()
551 556
552 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
553 result = {"results": commit.parents}
557 try:
558 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
559 parents = commit.parents
560 except CommitDoesNotExistError:
561 parents = []
562 result = {"results": parents}
554 563 return result
General Comments 0
You need to be logged in to leave comments. Login now