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