##// END OF EJS Templates
path-permissions: Initial support for path-based permissions
idlsoft -
r2618:940ad8b4 default
parent child Browse files
Show More
@@ -1,564 +1,616 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-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 import time
22 22 import logging
23 23 import operator
24 24
25 from pyramid.httpexceptions import HTTPFound
25 from pyramid.httpexceptions import HTTPFound, HTTPForbidden
26 26
27 from rhodecode.lib import helpers as h
27 from rhodecode.lib import helpers as h, diffs
28 28 from rhodecode.lib.utils2 import StrictAttributeDict, safe_int, datetime_to_time
29 29 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
30 30 from rhodecode.model import repo
31 31 from rhodecode.model import repo_group
32 32 from rhodecode.model import user_group
33 33 from rhodecode.model import user
34 34 from rhodecode.model.db import User
35 35 from rhodecode.model.scm import ScmModel
36 36
37 37 log = logging.getLogger(__name__)
38 38
39 39
40 40 ADMIN_PREFIX = '/_admin'
41 41 STATIC_FILE_PREFIX = '/_static'
42 42
43 43 URL_NAME_REQUIREMENTS = {
44 44 # group name can have a slash in them, but they must not end with a slash
45 45 'group_name': r'.*?[^/]',
46 46 'repo_group_name': r'.*?[^/]',
47 47 # repo names can have a slash in them, but they must not end with a slash
48 48 'repo_name': r'.*?[^/]',
49 49 # file path eats up everything at the end
50 50 'f_path': r'.*',
51 51 # reference types
52 52 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
53 53 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
54 54 }
55 55
56 56
57 57 def add_route_with_slash(config,name, pattern, **kw):
58 58 config.add_route(name, pattern, **kw)
59 59 if not pattern.endswith('/'):
60 60 config.add_route(name + '_slash', pattern + '/', **kw)
61 61
62 62
63 63 def add_route_requirements(route_path, requirements=URL_NAME_REQUIREMENTS):
64 64 """
65 65 Adds regex requirements to pyramid routes using a mapping dict
66 66 e.g::
67 67 add_route_requirements('{repo_name}/settings')
68 68 """
69 69 for key, regex in requirements.items():
70 70 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
71 71 return route_path
72 72
73 73
74 74 def get_format_ref_id(repo):
75 75 """Returns a `repo` specific reference formatter function"""
76 76 if h.is_svn(repo):
77 77 return _format_ref_id_svn
78 78 else:
79 79 return _format_ref_id
80 80
81 81
82 82 def _format_ref_id(name, raw_id):
83 83 """Default formatting of a given reference `name`"""
84 84 return name
85 85
86 86
87 87 def _format_ref_id_svn(name, raw_id):
88 88 """Special way of formatting a reference for Subversion including path"""
89 89 return '%s@%s' % (name, raw_id)
90 90
91 91
92 92 class TemplateArgs(StrictAttributeDict):
93 93 pass
94 94
95 95
96 96 class BaseAppView(object):
97 97
98 98 def __init__(self, context, request):
99 99 self.request = request
100 100 self.context = context
101 101 self.session = request.session
102 102 self._rhodecode_user = request.user # auth user
103 103 self._rhodecode_db_user = self._rhodecode_user.get_instance()
104 104 self._maybe_needs_password_change(
105 105 request.matched_route.name, self._rhodecode_db_user)
106 106
107 107 def _maybe_needs_password_change(self, view_name, user_obj):
108 108 log.debug('Checking if user %s needs password change on view %s',
109 109 user_obj, view_name)
110 110 skip_user_views = [
111 111 'logout', 'login',
112 112 'my_account_password', 'my_account_password_update'
113 113 ]
114 114
115 115 if not user_obj:
116 116 return
117 117
118 118 if user_obj.username == User.DEFAULT_USER:
119 119 return
120 120
121 121 now = time.time()
122 122 should_change = user_obj.user_data.get('force_password_change')
123 123 change_after = safe_int(should_change) or 0
124 124 if should_change and now > change_after:
125 125 log.debug('User %s requires password change', user_obj)
126 126 h.flash('You are required to change your password', 'warning',
127 127 ignore_duplicate=True)
128 128
129 129 if view_name not in skip_user_views:
130 130 raise HTTPFound(
131 131 self.request.route_path('my_account_password'))
132 132
133 133 def _log_creation_exception(self, e, repo_name):
134 134 _ = self.request.translate
135 135 reason = None
136 136 if len(e.args) == 2:
137 137 reason = e.args[1]
138 138
139 139 if reason == 'INVALID_CERTIFICATE':
140 140 log.exception(
141 141 'Exception creating a repository: invalid certificate')
142 142 msg = (_('Error creating repository %s: invalid certificate')
143 143 % repo_name)
144 144 else:
145 145 log.exception("Exception creating a repository")
146 146 msg = (_('Error creating repository %s')
147 147 % repo_name)
148 148 return msg
149 149
150 150 def _get_local_tmpl_context(self, include_app_defaults=True):
151 151 c = TemplateArgs()
152 152 c.auth_user = self.request.user
153 153 # TODO(marcink): migrate the usage of c.rhodecode_user to c.auth_user
154 154 c.rhodecode_user = self.request.user
155 155
156 156 if include_app_defaults:
157 157 from rhodecode.lib.base import attach_context_attributes
158 158 attach_context_attributes(c, self.request, self.request.user.user_id)
159 159
160 160 return c
161 161
162 162 def _get_template_context(self, tmpl_args, **kwargs):
163 163
164 164 local_tmpl_args = {
165 165 'defaults': {},
166 166 'errors': {},
167 167 'c': tmpl_args
168 168 }
169 169 local_tmpl_args.update(kwargs)
170 170 return local_tmpl_args
171 171
172 172 def load_default_context(self):
173 173 """
174 174 example:
175 175
176 176 def load_default_context(self):
177 177 c = self._get_local_tmpl_context()
178 178 c.custom_var = 'foobar'
179 179
180 180 return c
181 181 """
182 182 raise NotImplementedError('Needs implementation in view class')
183 183
184 184
185 185 class RepoAppView(BaseAppView):
186 186
187 187 def __init__(self, context, request):
188 188 super(RepoAppView, self).__init__(context, request)
189 189 self.db_repo = request.db_repo
190 190 self.db_repo_name = self.db_repo.repo_name
191 191 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
192 192
193 193 def _handle_missing_requirements(self, error):
194 194 log.error(
195 195 'Requirements are missing for repository %s: %s',
196 196 self.db_repo_name, error.message)
197 197
198 198 def _get_local_tmpl_context(self, include_app_defaults=True):
199 199 _ = self.request.translate
200 200 c = super(RepoAppView, self)._get_local_tmpl_context(
201 201 include_app_defaults=include_app_defaults)
202 202
203 203 # register common vars for this type of view
204 204 c.rhodecode_db_repo = self.db_repo
205 205 c.repo_name = self.db_repo_name
206 206 c.repository_pull_requests = self.db_repo_pull_requests
207 207
208 208 c.repository_requirements_missing = False
209 209 try:
210 210 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
211 self.path_filter = PathFilter(self.rhodecode_vcs_repo.get_path_permissions(c.auth_user.username))
211 212 except RepositoryRequirementError as e:
212 213 c.repository_requirements_missing = True
213 214 self._handle_missing_requirements(e)
214 215 self.rhodecode_vcs_repo = None
216 self.path_filter = None
217
218 c.path_filter = self.path_filter # used by atom_feed_entry.mako
215 219
216 220 if (not c.repository_requirements_missing
217 221 and self.rhodecode_vcs_repo is None):
218 222 # unable to fetch this repo as vcs instance, report back to user
219 223 h.flash(_(
220 224 "The repository `%(repo_name)s` cannot be loaded in filesystem. "
221 225 "Please check if it exist, or is not damaged.") %
222 226 {'repo_name': c.repo_name},
223 227 category='error', ignore_duplicate=True)
224 228 raise HTTPFound(h.route_path('home'))
225 229
226 230 return c
227 231
228 232 def _get_f_path(self, matchdict, default=None):
229 233 f_path = matchdict.get('f_path')
230 234 if f_path:
231 235 # fix for multiple initial slashes that causes errors for GIT
232 return f_path.lstrip('/')
236 return self.path_filter.assert_path_permissions(f_path.lstrip('/'))
237
238 return self.path_filter.assert_path_permissions(default)
239
240
241 class PathFilter(object):
242
243 # Expects and instance of BasePathPermissionChecker or None
244 def __init__(self, permission_checker):
245 self.permission_checker = permission_checker
246
247 def assert_path_permissions(self, path):
248 if path and self.permission_checker and not self.permission_checker.has_access(path):
249 raise HTTPForbidden()
250 return path
233 251
234 return default
252 def filter_patchset(self, patchset):
253 if not self.permission_checker or not patchset:
254 return patchset, False
255 had_filtered = False
256 filtered_patchset = []
257 for patch in patchset:
258 filename = patch.get('filename', None)
259 if not filename or self.permission_checker.has_access(filename):
260 filtered_patchset.append(patch)
261 else:
262 had_filtered = True
263 if had_filtered:
264 if isinstance(patchset, diffs.LimitedDiffContainer):
265 filtered_patchset = diffs.LimitedDiffContainer(patchset.diff_limit, patchset.cur_diff_size, filtered_patchset)
266 return filtered_patchset, True
267 else:
268 return patchset, False
269
270 def render_patchset_filtered(self, diffset, patchset, source_ref=None, target_ref=None):
271 filtered_patchset, has_hidden_changes = self.filter_patchset(patchset)
272 result = diffset.render_patchset(filtered_patchset, source_ref=source_ref, target_ref=target_ref)
273 result.has_hidden_changes = has_hidden_changes
274 return result
275
276 def get_raw_patch(self, diff_processor):
277 if self.permission_checker is None:
278 return diff_processor.as_raw()
279 elif self.permission_checker.has_full_access:
280 return diff_processor.as_raw()
281 else:
282 return '# Repository has user-specific filters, raw patch generation is disabled.'
283
284 @property
285 def is_enabled(self):
286 return self.permission_checker is not None
235 287
236 288
237 289 class RepoGroupAppView(BaseAppView):
238 290 def __init__(self, context, request):
239 291 super(RepoGroupAppView, self).__init__(context, request)
240 292 self.db_repo_group = request.db_repo_group
241 293 self.db_repo_group_name = self.db_repo_group.group_name
242 294
243 295 def _revoke_perms_on_yourself(self, form_result):
244 296 _updates = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
245 297 form_result['perm_updates'])
246 298 _additions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
247 299 form_result['perm_additions'])
248 300 _deletions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
249 301 form_result['perm_deletions'])
250 302 admin_perm = 'group.admin'
251 303 if _updates and _updates[0][1] != admin_perm or \
252 304 _additions and _additions[0][1] != admin_perm or \
253 305 _deletions and _deletions[0][1] != admin_perm:
254 306 return True
255 307 return False
256 308
257 309
258 310 class UserGroupAppView(BaseAppView):
259 311 def __init__(self, context, request):
260 312 super(UserGroupAppView, self).__init__(context, request)
261 313 self.db_user_group = request.db_user_group
262 314 self.db_user_group_name = self.db_user_group.users_group_name
263 315
264 316
265 317 class UserAppView(BaseAppView):
266 318 def __init__(self, context, request):
267 319 super(UserAppView, self).__init__(context, request)
268 320 self.db_user = request.db_user
269 321 self.db_user_id = self.db_user.user_id
270 322
271 323 _ = self.request.translate
272 324 if not request.db_user_supports_default:
273 325 if self.db_user.username == User.DEFAULT_USER:
274 326 h.flash(_("Editing user `{}` is disabled.".format(
275 327 User.DEFAULT_USER)), category='warning')
276 328 raise HTTPFound(h.route_path('users'))
277 329
278 330
279 331 class DataGridAppView(object):
280 332 """
281 333 Common class to have re-usable grid rendering components
282 334 """
283 335
284 336 def _extract_ordering(self, request, column_map=None):
285 337 column_map = column_map or {}
286 338 column_index = safe_int(request.GET.get('order[0][column]'))
287 339 order_dir = request.GET.get(
288 340 'order[0][dir]', 'desc')
289 341 order_by = request.GET.get(
290 342 'columns[%s][data][sort]' % column_index, 'name_raw')
291 343
292 344 # translate datatable to DB columns
293 345 order_by = column_map.get(order_by) or order_by
294 346
295 347 search_q = request.GET.get('search[value]')
296 348 return search_q, order_by, order_dir
297 349
298 350 def _extract_chunk(self, request):
299 351 start = safe_int(request.GET.get('start'), 0)
300 352 length = safe_int(request.GET.get('length'), 25)
301 353 draw = safe_int(request.GET.get('draw'))
302 354 return draw, start, length
303 355
304 356 def _get_order_col(self, order_by, model):
305 357 if isinstance(order_by, basestring):
306 358 try:
307 359 return operator.attrgetter(order_by)(model)
308 360 except AttributeError:
309 361 return None
310 362 else:
311 363 return order_by
312 364
313 365
314 366 class BaseReferencesView(RepoAppView):
315 367 """
316 368 Base for reference view for branches, tags and bookmarks.
317 369 """
318 370 def load_default_context(self):
319 371 c = self._get_local_tmpl_context()
320 372
321 373
322 374 return c
323 375
324 376 def load_refs_context(self, ref_items, partials_template):
325 377 _render = self.request.get_partial_renderer(partials_template)
326 378 pre_load = ["author", "date", "message"]
327 379
328 380 is_svn = h.is_svn(self.rhodecode_vcs_repo)
329 381 is_hg = h.is_hg(self.rhodecode_vcs_repo)
330 382
331 383 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
332 384
333 385 closed_refs = {}
334 386 if is_hg:
335 387 closed_refs = self.rhodecode_vcs_repo.branches_closed
336 388
337 389 data = []
338 390 for ref_name, commit_id in ref_items:
339 391 commit = self.rhodecode_vcs_repo.get_commit(
340 392 commit_id=commit_id, pre_load=pre_load)
341 393 closed = ref_name in closed_refs
342 394
343 395 # TODO: johbo: Unify generation of reference links
344 396 use_commit_id = '/' in ref_name or is_svn
345 397
346 398 if use_commit_id:
347 399 files_url = h.route_path(
348 400 'repo_files',
349 401 repo_name=self.db_repo_name,
350 402 f_path=ref_name if is_svn else '',
351 403 commit_id=commit_id)
352 404
353 405 else:
354 406 files_url = h.route_path(
355 407 'repo_files',
356 408 repo_name=self.db_repo_name,
357 409 f_path=ref_name if is_svn else '',
358 410 commit_id=ref_name,
359 411 _query=dict(at=ref_name))
360 412
361 413 data.append({
362 414 "name": _render('name', ref_name, files_url, closed),
363 415 "name_raw": ref_name,
364 416 "date": _render('date', commit.date),
365 417 "date_raw": datetime_to_time(commit.date),
366 418 "author": _render('author', commit.author),
367 419 "commit": _render(
368 420 'commit', commit.message, commit.raw_id, commit.idx),
369 421 "commit_raw": commit.idx,
370 422 "compare": _render(
371 423 'compare', format_ref_id(ref_name, commit.raw_id)),
372 424 })
373 425
374 426 return data
375 427
376 428
377 429 class RepoRoutePredicate(object):
378 430 def __init__(self, val, config):
379 431 self.val = val
380 432
381 433 def text(self):
382 434 return 'repo_route = %s' % self.val
383 435
384 436 phash = text
385 437
386 438 def __call__(self, info, request):
387 439
388 440 if hasattr(request, 'vcs_call'):
389 441 # skip vcs calls
390 442 return
391 443
392 444 repo_name = info['match']['repo_name']
393 445 repo_model = repo.RepoModel()
394 446 by_name_match = repo_model.get_by_repo_name(repo_name, cache=True)
395 447
396 448 def redirect_if_creating(db_repo):
397 449 if db_repo.repo_state in [repo.Repository.STATE_PENDING]:
398 450 raise HTTPFound(
399 451 request.route_path('repo_creating',
400 452 repo_name=db_repo.repo_name))
401 453
402 454 if by_name_match:
403 455 # register this as request object we can re-use later
404 456 request.db_repo = by_name_match
405 457 redirect_if_creating(by_name_match)
406 458 return True
407 459
408 460 by_id_match = repo_model.get_repo_by_id(repo_name)
409 461 if by_id_match:
410 462 request.db_repo = by_id_match
411 463 redirect_if_creating(by_id_match)
412 464 return True
413 465
414 466 return False
415 467
416 468
417 469 class RepoTypeRoutePredicate(object):
418 470 def __init__(self, val, config):
419 471 self.val = val or ['hg', 'git', 'svn']
420 472
421 473 def text(self):
422 474 return 'repo_accepted_type = %s' % self.val
423 475
424 476 phash = text
425 477
426 478 def __call__(self, info, request):
427 479 if hasattr(request, 'vcs_call'):
428 480 # skip vcs calls
429 481 return
430 482
431 483 rhodecode_db_repo = request.db_repo
432 484
433 485 log.debug(
434 486 '%s checking repo type for %s in %s',
435 487 self.__class__.__name__, rhodecode_db_repo.repo_type, self.val)
436 488
437 489 if rhodecode_db_repo.repo_type in self.val:
438 490 return True
439 491 else:
440 492 log.warning('Current view is not supported for repo type:%s',
441 493 rhodecode_db_repo.repo_type)
442 494 #
443 495 # h.flash(h.literal(
444 496 # _('Action not supported for %s.' % rhodecode_repo.alias)),
445 497 # category='warning')
446 498 # return redirect(
447 499 # route_path('repo_summary', repo_name=cls.rhodecode_db_repo.repo_name))
448 500
449 501 return False
450 502
451 503
452 504 class RepoGroupRoutePredicate(object):
453 505 def __init__(self, val, config):
454 506 self.val = val
455 507
456 508 def text(self):
457 509 return 'repo_group_route = %s' % self.val
458 510
459 511 phash = text
460 512
461 513 def __call__(self, info, request):
462 514 if hasattr(request, 'vcs_call'):
463 515 # skip vcs calls
464 516 return
465 517
466 518 repo_group_name = info['match']['repo_group_name']
467 519 repo_group_model = repo_group.RepoGroupModel()
468 520 by_name_match = repo_group_model.get_by_group_name(
469 521 repo_group_name, cache=True)
470 522
471 523 if by_name_match:
472 524 # register this as request object we can re-use later
473 525 request.db_repo_group = by_name_match
474 526 return True
475 527
476 528 return False
477 529
478 530
479 531 class UserGroupRoutePredicate(object):
480 532 def __init__(self, val, config):
481 533 self.val = val
482 534
483 535 def text(self):
484 536 return 'user_group_route = %s' % self.val
485 537
486 538 phash = text
487 539
488 540 def __call__(self, info, request):
489 541 if hasattr(request, 'vcs_call'):
490 542 # skip vcs calls
491 543 return
492 544
493 545 user_group_id = info['match']['user_group_id']
494 546 user_group_model = user_group.UserGroup()
495 547 by_id_match = user_group_model.get(
496 548 user_group_id, cache=True)
497 549
498 550 if by_id_match:
499 551 # register this as request object we can re-use later
500 552 request.db_user_group = by_id_match
501 553 return True
502 554
503 555 return False
504 556
505 557
506 558 class UserRoutePredicateBase(object):
507 559 supports_default = None
508 560
509 561 def __init__(self, val, config):
510 562 self.val = val
511 563
512 564 def text(self):
513 565 raise NotImplementedError()
514 566
515 567 def __call__(self, info, request):
516 568 if hasattr(request, 'vcs_call'):
517 569 # skip vcs calls
518 570 return
519 571
520 572 user_id = info['match']['user_id']
521 573 user_model = user.User()
522 574 by_id_match = user_model.get(
523 575 user_id, cache=True)
524 576
525 577 if by_id_match:
526 578 # register this as request object we can re-use later
527 579 request.db_user = by_id_match
528 580 request.db_user_supports_default = self.supports_default
529 581 return True
530 582
531 583 return False
532 584
533 585
534 586 class UserRoutePredicate(UserRoutePredicateBase):
535 587 supports_default = False
536 588
537 589 def text(self):
538 590 return 'user_route = %s' % self.val
539 591
540 592 phash = text
541 593
542 594
543 595 class UserRouteWithDefaultPredicate(UserRoutePredicateBase):
544 596 supports_default = True
545 597
546 598 def text(self):
547 599 return 'user_with_default_route = %s' % self.val
548 600
549 601 phash = text
550 602
551 603
552 604 def includeme(config):
553 605 config.add_route_predicate(
554 606 'repo_route', RepoRoutePredicate)
555 607 config.add_route_predicate(
556 608 'repo_accepted_types', RepoTypeRoutePredicate)
557 609 config.add_route_predicate(
558 610 'repo_group_route', RepoGroupRoutePredicate)
559 611 config.add_route_predicate(
560 612 'user_group_route', UserGroupRoutePredicate)
561 613 config.add_route_predicate(
562 614 'user_route_with_default', UserRouteWithDefaultPredicate)
563 615 config.add_route_predicate(
564 616 'user_route', UserRoutePredicate) No newline at end of file
@@ -1,562 +1,562 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.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 return c
154 154
155 155 def _commit(self, commit_id_range, method):
156 156 _ = self.request.translate
157 157 c = self.load_default_context()
158 158 c.ignorews_url = _ignorews_url
159 159 c.context_url = _context_url
160 160 c.fulldiff = self.request.GET.get('fulldiff')
161 161
162 162 # fetch global flags of ignore ws or context lines
163 163 context_lcl = get_line_ctx('', self.request)
164 164 ign_whitespace_lcl = get_ignore_ws('', self.request)
165 165
166 166 # diff_limit will cut off the whole diff if the limit is applied
167 167 # otherwise it will just hide the big files from the front-end
168 168 diff_limit = c.visual.cut_off_limit_diff
169 169 file_limit = c.visual.cut_off_limit_file
170 170
171 171 # get ranges of commit ids if preset
172 172 commit_range = commit_id_range.split('...')[:2]
173 173
174 174 try:
175 175 pre_load = ['affected_files', 'author', 'branch', 'date',
176 176 'message', 'parents']
177 177
178 178 if len(commit_range) == 2:
179 179 commits = self.rhodecode_vcs_repo.get_commits(
180 180 start_id=commit_range[0], end_id=commit_range[1],
181 181 pre_load=pre_load)
182 182 commits = list(commits)
183 183 else:
184 184 commits = [self.rhodecode_vcs_repo.get_commit(
185 185 commit_id=commit_id_range, pre_load=pre_load)]
186 186
187 187 c.commit_ranges = commits
188 188 if not c.commit_ranges:
189 189 raise RepositoryError(
190 190 'The commit range returned an empty result')
191 191 except CommitDoesNotExistError:
192 192 msg = _('No such commit exists for this repository')
193 193 h.flash(msg, category='error')
194 194 raise HTTPNotFound()
195 195 except Exception:
196 196 log.exception("General failure")
197 197 raise HTTPNotFound()
198 198
199 199 c.changes = OrderedDict()
200 200 c.lines_added = 0
201 201 c.lines_deleted = 0
202 202
203 203 # auto collapse if we have more than limit
204 204 collapse_limit = diffs.DiffProcessor._collapse_commits_over
205 205 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
206 206
207 207 c.commit_statuses = ChangesetStatus.STATUSES
208 208 c.inline_comments = []
209 209 c.files = []
210 210
211 211 c.statuses = []
212 212 c.comments = []
213 213 c.unresolved_comments = []
214 214 if len(c.commit_ranges) == 1:
215 215 commit = c.commit_ranges[0]
216 216 c.comments = CommentsModel().get_comments(
217 217 self.db_repo.repo_id,
218 218 revision=commit.raw_id)
219 219 c.statuses.append(ChangesetStatusModel().get_status(
220 220 self.db_repo.repo_id, commit.raw_id))
221 221 # comments from PR
222 222 statuses = ChangesetStatusModel().get_statuses(
223 223 self.db_repo.repo_id, commit.raw_id,
224 224 with_revisions=True)
225 225 prs = set(st.pull_request for st in statuses
226 226 if st.pull_request is not None)
227 227 # from associated statuses, check the pull requests, and
228 228 # show comments from them
229 229 for pr in prs:
230 230 c.comments.extend(pr.comments)
231 231
232 232 c.unresolved_comments = CommentsModel()\
233 233 .get_commit_unresolved_todos(commit.raw_id)
234 234
235 235 diff = None
236 236 # Iterate over ranges (default commit view is always one commit)
237 237 for commit in c.commit_ranges:
238 238 c.changes[commit.raw_id] = []
239 239
240 240 commit2 = commit
241 241 commit1 = commit.parents[0] if commit.parents else EmptyCommit()
242 242
243 243 _diff = self.rhodecode_vcs_repo.get_diff(
244 244 commit1, commit2,
245 245 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
246 246 diff_processor = diffs.DiffProcessor(
247 247 _diff, format='newdiff', diff_limit=diff_limit,
248 248 file_limit=file_limit, show_full_diff=c.fulldiff)
249 249
250 250 commit_changes = OrderedDict()
251 251 if method == 'show':
252 252 _parsed = diff_processor.prepare()
253 253 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
254 254
255 255 _parsed = diff_processor.prepare()
256 256
257 257 def _node_getter(commit):
258 258 def get_node(fname):
259 259 try:
260 260 return commit.get_node(fname)
261 261 except NodeDoesNotExistError:
262 262 return None
263 263 return get_node
264 264
265 265 inline_comments = CommentsModel().get_inline_comments(
266 266 self.db_repo.repo_id, revision=commit.raw_id)
267 267 c.inline_cnt = CommentsModel().get_inline_comments_count(
268 268 inline_comments)
269 269
270 270 diffset = codeblocks.DiffSet(
271 271 repo_name=self.db_repo_name,
272 272 source_node_getter=_node_getter(commit1),
273 273 target_node_getter=_node_getter(commit2),
274 274 comments=inline_comments)
275 diffset = diffset.render_patchset(
276 _parsed, commit1.raw_id, commit2.raw_id)
275 diffset = self.path_filter.render_patchset_filtered(
276 diffset, _parsed, commit1.raw_id, commit2.raw_id)
277 277
278 278 c.changes[commit.raw_id] = diffset
279 279 else:
280 280 # downloads/raw we only need RAW diff nothing else
281 diff = diff_processor.as_raw()
281 diff = self.path_filter.get_raw_patch(diff_processor)
282 282 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
283 283
284 284 # sort comments by how they were generated
285 285 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
286 286
287 287 if len(c.commit_ranges) == 1:
288 288 c.commit = c.commit_ranges[0]
289 289 c.parent_tmpl = ''.join(
290 290 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
291 291
292 292 if method == 'download':
293 293 response = Response(diff)
294 294 response.content_type = 'text/plain'
295 295 response.content_disposition = (
296 296 'attachment; filename=%s.diff' % commit_id_range[:12])
297 297 return response
298 298 elif method == 'patch':
299 299 c.diff = safe_unicode(diff)
300 300 patch = render(
301 301 'rhodecode:templates/changeset/patch_changeset.mako',
302 302 self._get_template_context(c), self.request)
303 303 response = Response(patch)
304 304 response.content_type = 'text/plain'
305 305 return response
306 306 elif method == 'raw':
307 307 response = Response(diff)
308 308 response.content_type = 'text/plain'
309 309 return response
310 310 elif method == 'show':
311 311 if len(c.commit_ranges) == 1:
312 312 html = render(
313 313 'rhodecode:templates/changeset/changeset.mako',
314 314 self._get_template_context(c), self.request)
315 315 return Response(html)
316 316 else:
317 317 c.ancestor = None
318 318 c.target_repo = self.db_repo
319 319 html = render(
320 320 'rhodecode:templates/changeset/changeset_range.mako',
321 321 self._get_template_context(c), self.request)
322 322 return Response(html)
323 323
324 324 raise HTTPBadRequest()
325 325
326 326 @LoginRequired()
327 327 @HasRepoPermissionAnyDecorator(
328 328 'repository.read', 'repository.write', 'repository.admin')
329 329 @view_config(
330 330 route_name='repo_commit', request_method='GET',
331 331 renderer=None)
332 332 def repo_commit_show(self):
333 333 commit_id = self.request.matchdict['commit_id']
334 334 return self._commit(commit_id, method='show')
335 335
336 336 @LoginRequired()
337 337 @HasRepoPermissionAnyDecorator(
338 338 'repository.read', 'repository.write', 'repository.admin')
339 339 @view_config(
340 340 route_name='repo_commit_raw', request_method='GET',
341 341 renderer=None)
342 342 @view_config(
343 343 route_name='repo_commit_raw_deprecated', request_method='GET',
344 344 renderer=None)
345 345 def repo_commit_raw(self):
346 346 commit_id = self.request.matchdict['commit_id']
347 347 return self._commit(commit_id, method='raw')
348 348
349 349 @LoginRequired()
350 350 @HasRepoPermissionAnyDecorator(
351 351 'repository.read', 'repository.write', 'repository.admin')
352 352 @view_config(
353 353 route_name='repo_commit_patch', request_method='GET',
354 354 renderer=None)
355 355 def repo_commit_patch(self):
356 356 commit_id = self.request.matchdict['commit_id']
357 357 return self._commit(commit_id, method='patch')
358 358
359 359 @LoginRequired()
360 360 @HasRepoPermissionAnyDecorator(
361 361 'repository.read', 'repository.write', 'repository.admin')
362 362 @view_config(
363 363 route_name='repo_commit_download', request_method='GET',
364 364 renderer=None)
365 365 def repo_commit_download(self):
366 366 commit_id = self.request.matchdict['commit_id']
367 367 return self._commit(commit_id, method='download')
368 368
369 369 @LoginRequired()
370 370 @NotAnonymous()
371 371 @HasRepoPermissionAnyDecorator(
372 372 'repository.read', 'repository.write', 'repository.admin')
373 373 @CSRFRequired()
374 374 @view_config(
375 375 route_name='repo_commit_comment_create', request_method='POST',
376 376 renderer='json_ext')
377 377 def repo_commit_comment_create(self):
378 378 _ = self.request.translate
379 379 commit_id = self.request.matchdict['commit_id']
380 380
381 381 c = self.load_default_context()
382 382 status = self.request.POST.get('changeset_status', None)
383 383 text = self.request.POST.get('text')
384 384 comment_type = self.request.POST.get('comment_type')
385 385 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
386 386
387 387 if status:
388 388 text = text or (_('Status change %(transition_icon)s %(status)s')
389 389 % {'transition_icon': '>',
390 390 'status': ChangesetStatus.get_status_lbl(status)})
391 391
392 392 multi_commit_ids = []
393 393 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
394 394 if _commit_id not in ['', None, EmptyCommit.raw_id]:
395 395 if _commit_id not in multi_commit_ids:
396 396 multi_commit_ids.append(_commit_id)
397 397
398 398 commit_ids = multi_commit_ids or [commit_id]
399 399
400 400 comment = None
401 401 for current_id in filter(None, commit_ids):
402 402 comment = CommentsModel().create(
403 403 text=text,
404 404 repo=self.db_repo.repo_id,
405 405 user=self._rhodecode_db_user.user_id,
406 406 commit_id=current_id,
407 407 f_path=self.request.POST.get('f_path'),
408 408 line_no=self.request.POST.get('line'),
409 409 status_change=(ChangesetStatus.get_status_lbl(status)
410 410 if status else None),
411 411 status_change_type=status,
412 412 comment_type=comment_type,
413 413 resolves_comment_id=resolves_comment_id
414 414 )
415 415
416 416 # get status if set !
417 417 if status:
418 418 # if latest status was from pull request and it's closed
419 419 # disallow changing status !
420 420 # dont_allow_on_closed_pull_request = True !
421 421
422 422 try:
423 423 ChangesetStatusModel().set_status(
424 424 self.db_repo.repo_id,
425 425 status,
426 426 self._rhodecode_db_user.user_id,
427 427 comment,
428 428 revision=current_id,
429 429 dont_allow_on_closed_pull_request=True
430 430 )
431 431 except StatusChangeOnClosedPullRequestError:
432 432 msg = _('Changing the status of a commit associated with '
433 433 'a closed pull request is not allowed')
434 434 log.exception(msg)
435 435 h.flash(msg, category='warning')
436 436 raise HTTPFound(h.route_path(
437 437 'repo_commit', repo_name=self.db_repo_name,
438 438 commit_id=current_id))
439 439
440 440 # finalize, commit and redirect
441 441 Session().commit()
442 442
443 443 data = {
444 444 'target_id': h.safeid(h.safe_unicode(
445 445 self.request.POST.get('f_path'))),
446 446 }
447 447 if comment:
448 448 c.co = comment
449 449 rendered_comment = render(
450 450 'rhodecode:templates/changeset/changeset_comment_block.mako',
451 451 self._get_template_context(c), self.request)
452 452
453 453 data.update(comment.get_dict())
454 454 data.update({'rendered_text': rendered_comment})
455 455
456 456 return data
457 457
458 458 @LoginRequired()
459 459 @NotAnonymous()
460 460 @HasRepoPermissionAnyDecorator(
461 461 'repository.read', 'repository.write', 'repository.admin')
462 462 @CSRFRequired()
463 463 @view_config(
464 464 route_name='repo_commit_comment_preview', request_method='POST',
465 465 renderer='string', xhr=True)
466 466 def repo_commit_comment_preview(self):
467 467 # Technically a CSRF token is not needed as no state changes with this
468 468 # call. However, as this is a POST is better to have it, so automated
469 469 # tools don't flag it as potential CSRF.
470 470 # Post is required because the payload could be bigger than the maximum
471 471 # allowed by GET.
472 472
473 473 text = self.request.POST.get('text')
474 474 renderer = self.request.POST.get('renderer') or 'rst'
475 475 if text:
476 476 return h.render(text, renderer=renderer, mentions=True)
477 477 return ''
478 478
479 479 @LoginRequired()
480 480 @NotAnonymous()
481 481 @HasRepoPermissionAnyDecorator(
482 482 'repository.read', 'repository.write', 'repository.admin')
483 483 @CSRFRequired()
484 484 @view_config(
485 485 route_name='repo_commit_comment_delete', request_method='POST',
486 486 renderer='json_ext')
487 487 def repo_commit_comment_delete(self):
488 488 commit_id = self.request.matchdict['commit_id']
489 489 comment_id = self.request.matchdict['comment_id']
490 490
491 491 comment = ChangesetComment.get_or_404(comment_id)
492 492 if not comment:
493 493 log.debug('Comment with id:%s not found, skipping', comment_id)
494 494 # comment already deleted in another call probably
495 495 return True
496 496
497 497 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
498 498 super_admin = h.HasPermissionAny('hg.admin')()
499 499 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
500 500 is_repo_comment = comment.repo.repo_name == self.db_repo_name
501 501 comment_repo_admin = is_repo_admin and is_repo_comment
502 502
503 503 if super_admin or comment_owner or comment_repo_admin:
504 504 CommentsModel().delete(comment=comment, user=self._rhodecode_db_user)
505 505 Session().commit()
506 506 return True
507 507 else:
508 508 log.warning('No permissions for user %s to delete comment_id: %s',
509 509 self._rhodecode_db_user, comment_id)
510 510 raise HTTPNotFound()
511 511
512 512 @LoginRequired()
513 513 @HasRepoPermissionAnyDecorator(
514 514 'repository.read', 'repository.write', 'repository.admin')
515 515 @view_config(
516 516 route_name='repo_commit_data', request_method='GET',
517 517 renderer='json_ext', xhr=True)
518 518 def repo_commit_data(self):
519 519 commit_id = self.request.matchdict['commit_id']
520 520 self.load_default_context()
521 521
522 522 try:
523 523 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
524 524 except CommitDoesNotExistError as e:
525 525 return EmptyCommit(message=str(e))
526 526
527 527 @LoginRequired()
528 528 @HasRepoPermissionAnyDecorator(
529 529 'repository.read', 'repository.write', 'repository.admin')
530 530 @view_config(
531 531 route_name='repo_commit_children', request_method='GET',
532 532 renderer='json_ext', xhr=True)
533 533 def repo_commit_children(self):
534 534 commit_id = self.request.matchdict['commit_id']
535 535 self.load_default_context()
536 536
537 537 try:
538 538 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
539 539 children = commit.children
540 540 except CommitDoesNotExistError:
541 541 children = []
542 542
543 543 result = {"results": children}
544 544 return result
545 545
546 546 @LoginRequired()
547 547 @HasRepoPermissionAnyDecorator(
548 548 'repository.read', 'repository.write', 'repository.admin')
549 549 @view_config(
550 550 route_name='repo_commit_parents', request_method='GET',
551 551 renderer='json_ext')
552 552 def repo_commit_parents(self):
553 553 commit_id = self.request.matchdict['commit_id']
554 554 self.load_default_context()
555 555
556 556 try:
557 557 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
558 558 parents = commit.parents
559 559 except CommitDoesNotExistError:
560 560 parents = []
561 561 result = {"results": parents}
562 562 return result
@@ -1,322 +1,322 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-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
24 24 from pyramid.httpexceptions import HTTPBadRequest, HTTPNotFound, HTTPFound
25 25 from pyramid.view import view_config
26 26 from pyramid.renderers import render
27 27 from pyramid.response import Response
28 28
29 29 from rhodecode.apps._base import RepoAppView
30 30 from rhodecode.controllers.utils import parse_path_ref, get_commit_from_ref_name
31 31 from rhodecode.lib import helpers as h
32 32 from rhodecode.lib import diffs, codeblocks
33 33 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
34 34 from rhodecode.lib.utils import safe_str
35 35 from rhodecode.lib.utils2 import safe_unicode, str2bool
36 36 from rhodecode.lib.vcs.exceptions import (
37 37 EmptyRepositoryError, RepositoryError, RepositoryRequirementError,
38 38 NodeDoesNotExistError)
39 39 from rhodecode.model.db import Repository, ChangesetStatus
40 40
41 41 log = logging.getLogger(__name__)
42 42
43 43
44 44 class RepoCompareView(RepoAppView):
45 45 def load_default_context(self):
46 46 c = self._get_local_tmpl_context(include_app_defaults=True)
47 47
48 48 c.rhodecode_repo = self.rhodecode_vcs_repo
49 49
50 50
51 51 return c
52 52
53 53 def _get_commit_or_redirect(
54 54 self, ref, ref_type, repo, redirect_after=True, partial=False):
55 55 """
56 56 This is a safe way to get a commit. If an error occurs it
57 57 redirects to a commit with a proper message. If partial is set
58 58 then it does not do redirect raise and throws an exception instead.
59 59 """
60 60 _ = self.request.translate
61 61 try:
62 62 return get_commit_from_ref_name(repo, safe_str(ref), ref_type)
63 63 except EmptyRepositoryError:
64 64 if not redirect_after:
65 65 return repo.scm_instance().EMPTY_COMMIT
66 66 h.flash(h.literal(_('There are no commits yet')),
67 67 category='warning')
68 68 if not partial:
69 69 raise HTTPFound(
70 70 h.route_path('repo_summary', repo_name=repo.repo_name))
71 71 raise HTTPBadRequest()
72 72
73 73 except RepositoryError as e:
74 74 log.exception(safe_str(e))
75 75 h.flash(safe_str(h.escape(e)), category='warning')
76 76 if not partial:
77 77 raise HTTPFound(
78 78 h.route_path('repo_summary', repo_name=repo.repo_name))
79 79 raise HTTPBadRequest()
80 80
81 81 @LoginRequired()
82 82 @HasRepoPermissionAnyDecorator(
83 83 'repository.read', 'repository.write', 'repository.admin')
84 84 @view_config(
85 85 route_name='repo_compare_select', request_method='GET',
86 86 renderer='rhodecode:templates/compare/compare_diff.mako')
87 87 def compare_select(self):
88 88 _ = self.request.translate
89 89 c = self.load_default_context()
90 90
91 91 source_repo = self.db_repo_name
92 92 target_repo = self.request.GET.get('target_repo', source_repo)
93 93 c.source_repo = Repository.get_by_repo_name(source_repo)
94 94 c.target_repo = Repository.get_by_repo_name(target_repo)
95 95
96 96 if c.source_repo is None or c.target_repo is None:
97 97 raise HTTPNotFound()
98 98
99 99 c.compare_home = True
100 100 c.commit_ranges = []
101 101 c.collapse_all_commits = False
102 102 c.diffset = None
103 103 c.limited_diff = False
104 104 c.source_ref = c.target_ref = _('Select commit')
105 105 c.source_ref_type = ""
106 106 c.target_ref_type = ""
107 107 c.commit_statuses = ChangesetStatus.STATUSES
108 108 c.preview_mode = False
109 109 c.file_path = None
110 110
111 111 return self._get_template_context(c)
112 112
113 113 @LoginRequired()
114 114 @HasRepoPermissionAnyDecorator(
115 115 'repository.read', 'repository.write', 'repository.admin')
116 116 @view_config(
117 117 route_name='repo_compare', request_method='GET',
118 118 renderer=None)
119 119 def compare(self):
120 120 _ = self.request.translate
121 121 c = self.load_default_context()
122 122
123 123 source_ref_type = self.request.matchdict['source_ref_type']
124 124 source_ref = self.request.matchdict['source_ref']
125 125 target_ref_type = self.request.matchdict['target_ref_type']
126 126 target_ref = self.request.matchdict['target_ref']
127 127
128 128 # source_ref will be evaluated in source_repo
129 129 source_repo_name = self.db_repo_name
130 130 source_path, source_id = parse_path_ref(source_ref)
131 131
132 132 # target_ref will be evaluated in target_repo
133 133 target_repo_name = self.request.GET.get('target_repo', source_repo_name)
134 134 target_path, target_id = parse_path_ref(
135 135 target_ref, default_path=self.request.GET.get('f_path', ''))
136 136
137 137 # if merge is True
138 138 # Show what changes since the shared ancestor commit of target/source
139 139 # the source would get if it was merged with target. Only commits
140 140 # which are in target but not in source will be shown.
141 141 merge = str2bool(self.request.GET.get('merge'))
142 142 # if merge is False
143 143 # Show a raw diff of source/target refs even if no ancestor exists
144 144
145 145 # c.fulldiff disables cut_off_limit
146 146 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
147 147
148 148 c.file_path = target_path
149 149 c.commit_statuses = ChangesetStatus.STATUSES
150 150
151 151 # if partial, returns just compare_commits.html (commits log)
152 152 partial = self.request.is_xhr
153 153
154 154 # swap url for compare_diff page
155 155 c.swap_url = h.route_path(
156 156 'repo_compare',
157 157 repo_name=target_repo_name,
158 158 source_ref_type=target_ref_type,
159 159 source_ref=target_ref,
160 160 target_repo=source_repo_name,
161 161 target_ref_type=source_ref_type,
162 162 target_ref=source_ref,
163 163 _query=dict(merge=merge and '1' or '', f_path=target_path))
164 164
165 165 source_repo = Repository.get_by_repo_name(source_repo_name)
166 166 target_repo = Repository.get_by_repo_name(target_repo_name)
167 167
168 168 if source_repo is None:
169 169 log.error('Could not find the source repo: {}'
170 170 .format(source_repo_name))
171 171 h.flash(_('Could not find the source repo: `{}`')
172 172 .format(h.escape(source_repo_name)), category='error')
173 173 raise HTTPFound(
174 174 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
175 175
176 176 if target_repo is None:
177 177 log.error('Could not find the target repo: {}'
178 178 .format(source_repo_name))
179 179 h.flash(_('Could not find the target repo: `{}`')
180 180 .format(h.escape(target_repo_name)), category='error')
181 181 raise HTTPFound(
182 182 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
183 183
184 184 source_scm = source_repo.scm_instance()
185 185 target_scm = target_repo.scm_instance()
186 186
187 187 source_alias = source_scm.alias
188 188 target_alias = target_scm.alias
189 189 if source_alias != target_alias:
190 190 msg = _('The comparison of two different kinds of remote repos '
191 191 'is not available')
192 192 log.error(msg)
193 193 h.flash(msg, category='error')
194 194 raise HTTPFound(
195 195 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
196 196
197 197 source_commit = self._get_commit_or_redirect(
198 198 ref=source_id, ref_type=source_ref_type, repo=source_repo,
199 199 partial=partial)
200 200 target_commit = self._get_commit_or_redirect(
201 201 ref=target_id, ref_type=target_ref_type, repo=target_repo,
202 202 partial=partial)
203 203
204 204 c.compare_home = False
205 205 c.source_repo = source_repo
206 206 c.target_repo = target_repo
207 207 c.source_ref = source_ref
208 208 c.target_ref = target_ref
209 209 c.source_ref_type = source_ref_type
210 210 c.target_ref_type = target_ref_type
211 211
212 212 pre_load = ["author", "branch", "date", "message"]
213 213 c.ancestor = None
214 214
215 215 if c.file_path:
216 216 if source_commit == target_commit:
217 217 c.commit_ranges = []
218 218 else:
219 219 c.commit_ranges = [target_commit]
220 220 else:
221 221 try:
222 222 c.commit_ranges = source_scm.compare(
223 223 source_commit.raw_id, target_commit.raw_id,
224 224 target_scm, merge, pre_load=pre_load)
225 225 if merge:
226 226 c.ancestor = source_scm.get_common_ancestor(
227 227 source_commit.raw_id, target_commit.raw_id, target_scm)
228 228 except RepositoryRequirementError:
229 229 msg = _('Could not compare repos with different '
230 230 'large file settings')
231 231 log.error(msg)
232 232 if partial:
233 233 return Response(msg)
234 234 h.flash(msg, category='error')
235 235 raise HTTPFound(
236 236 h.route_path('repo_compare_select',
237 237 repo_name=self.db_repo_name))
238 238
239 239 c.statuses = self.db_repo.statuses(
240 240 [x.raw_id for x in c.commit_ranges])
241 241
242 242 # auto collapse if we have more than limit
243 243 collapse_limit = diffs.DiffProcessor._collapse_commits_over
244 244 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
245 245
246 246 if partial: # for PR ajax commits loader
247 247 if not c.ancestor:
248 248 return Response('') # cannot merge if there is no ancestor
249 249
250 250 html = render(
251 251 'rhodecode:templates/compare/compare_commits.mako',
252 252 self._get_template_context(c), self.request)
253 253 return Response(html)
254 254
255 255 if c.ancestor:
256 256 # case we want a simple diff without incoming commits,
257 257 # previewing what will be merged.
258 258 # Make the diff on target repo (which is known to have target_ref)
259 259 log.debug('Using ancestor %s as source_ref instead of %s'
260 260 % (c.ancestor, source_ref))
261 261 source_repo = target_repo
262 262 source_commit = target_repo.get_commit(commit_id=c.ancestor)
263 263
264 264 # diff_limit will cut off the whole diff if the limit is applied
265 265 # otherwise it will just hide the big files from the front-end
266 266 diff_limit = c.visual.cut_off_limit_diff
267 267 file_limit = c.visual.cut_off_limit_file
268 268
269 269 log.debug('calculating diff between '
270 270 'source_ref:%s and target_ref:%s for repo `%s`',
271 271 source_commit, target_commit,
272 272 safe_unicode(source_repo.scm_instance().path))
273 273
274 274 if source_commit.repository != target_commit.repository:
275 275 msg = _(
276 276 "Repositories unrelated. "
277 277 "Cannot compare commit %(commit1)s from repository %(repo1)s "
278 278 "with commit %(commit2)s from repository %(repo2)s.") % {
279 279 'commit1': h.show_id(source_commit),
280 280 'repo1': source_repo.repo_name,
281 281 'commit2': h.show_id(target_commit),
282 282 'repo2': target_repo.repo_name,
283 283 }
284 284 h.flash(msg, category='error')
285 285 raise HTTPFound(
286 286 h.route_path('repo_compare_select',
287 287 repo_name=self.db_repo_name))
288 288
289 289 txt_diff = source_repo.scm_instance().get_diff(
290 290 commit1=source_commit, commit2=target_commit,
291 291 path=target_path, path1=source_path)
292 292
293 293 diff_processor = diffs.DiffProcessor(
294 294 txt_diff, format='newdiff', diff_limit=diff_limit,
295 295 file_limit=file_limit, show_full_diff=c.fulldiff)
296 296 _parsed = diff_processor.prepare()
297 297
298 298 def _node_getter(commit):
299 299 """ Returns a function that returns a node for a commit or None """
300 300 def get_node(fname):
301 301 try:
302 302 return commit.get_node(fname)
303 303 except NodeDoesNotExistError:
304 304 return None
305 305 return get_node
306 306
307 307 diffset = codeblocks.DiffSet(
308 308 repo_name=source_repo.repo_name,
309 309 source_node_getter=_node_getter(source_commit),
310 310 target_node_getter=_node_getter(target_commit),
311 311 )
312 c.diffset = diffset.render_patchset(
313 _parsed, source_ref, target_ref)
312 c.diffset = self.path_filter.render_patchset_filtered(
313 diffset, _parsed, source_ref, target_ref)
314 314
315 315 c.preview_mode = merge
316 316 c.source_commit = source_commit
317 317 c.target_commit = target_commit
318 318
319 319 html = render(
320 320 'rhodecode:templates/compare/compare_diff.mako',
321 321 self._get_template_context(c), self.request)
322 322 return Response(html) No newline at end of file
@@ -1,205 +1,218 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2017-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 import pytz
22 22 import logging
23 23
24 24 from beaker.cache import cache_region
25 25 from pyramid.view import view_config
26 26 from pyramid.response import Response
27 27 from webhelpers.feedgenerator import Rss201rev2Feed, Atom1Feed
28 28
29 29 from rhodecode.apps._base import RepoAppView
30 30 from rhodecode.lib import audit_logger
31 31 from rhodecode.lib import helpers as h
32 32 from rhodecode.lib.auth import (
33 33 LoginRequired, HasRepoPermissionAnyDecorator)
34 34 from rhodecode.lib.diffs import DiffProcessor, LimitedDiffContainer
35 35 from rhodecode.lib.utils2 import str2bool, safe_int, md5_safe
36 36 from rhodecode.model.db import UserApiKeys, CacheKey
37 37
38 38 log = logging.getLogger(__name__)
39 39
40 40
41 41 class RepoFeedView(RepoAppView):
42 42 def load_default_context(self):
43 43 c = self._get_local_tmpl_context()
44 44
45 45
46 46 self._load_defaults()
47 47 return c
48 48
49 49 def _get_config(self):
50 50 import rhodecode
51 51 config = rhodecode.CONFIG
52 52
53 53 return {
54 54 'language': 'en-us',
55 55 'feed_ttl': '5', # TTL of feed,
56 56 'feed_include_diff':
57 57 str2bool(config.get('rss_include_diff', False)),
58 58 'feed_items_per_page':
59 59 safe_int(config.get('rss_items_per_page', 20)),
60 60 'feed_diff_limit':
61 61 # we need to protect from parsing huge diffs here other way
62 62 # we can kill the server
63 63 safe_int(config.get('rss_cut_off_limit', 32 * 1024)),
64 64 }
65 65
66 66 def _load_defaults(self):
67 67 _ = self.request.translate
68 68 config = self._get_config()
69 69 # common values for feeds
70 70 self.description = _('Changes on %s repository')
71 71 self.title = self.title = _('%s %s feed') % (self.db_repo_name, '%s')
72 72 self.language = config["language"]
73 73 self.ttl = config["feed_ttl"]
74 74 self.feed_include_diff = config['feed_include_diff']
75 75 self.feed_diff_limit = config['feed_diff_limit']
76 76 self.feed_items_per_page = config['feed_items_per_page']
77 77
78 78 def _changes(self, commit):
79 79 diff_processor = DiffProcessor(
80 80 commit.diff(), diff_limit=self.feed_diff_limit)
81 81 _parsed = diff_processor.prepare(inline_diff=False)
82 82 limited_diff = isinstance(_parsed, LimitedDiffContainer)
83 83
84 84 return diff_processor, _parsed, limited_diff
85 85
86 86 def _get_title(self, commit):
87 87 return h.shorter(commit.message, 160)
88 88
89 89 def _get_description(self, commit):
90 90 _renderer = self.request.get_partial_renderer(
91 91 'rhodecode:templates/feed/atom_feed_entry.mako')
92 92 diff_processor, parsed_diff, limited_diff = self._changes(commit)
93 filtered_parsed_diff, has_hidden_changes = self.path_filter.filter_patchset(parsed_diff)
93 94 return _renderer(
94 95 'body',
95 96 commit=commit,
96 parsed_diff=parsed_diff,
97 parsed_diff=filtered_parsed_diff,
97 98 limited_diff=limited_diff,
98 99 feed_include_diff=self.feed_include_diff,
99 100 diff_processor=diff_processor,
101 has_hidden_changes=has_hidden_changes
100 102 )
101 103
102 104 def _set_timezone(self, date, tzinfo=pytz.utc):
103 105 if not getattr(date, "tzinfo", None):
104 106 date.replace(tzinfo=tzinfo)
105 107 return date
106 108
107 109 def _get_commits(self):
108 110 return list(self.rhodecode_vcs_repo[-self.feed_items_per_page:])
109 111
110 112 def uid(self, repo_id, commit_id):
111 113 return '{}:{}'.format(md5_safe(repo_id), md5_safe(commit_id))
112 114
113 115 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
114 116 @HasRepoPermissionAnyDecorator(
115 117 'repository.read', 'repository.write', 'repository.admin')
116 118 @view_config(
117 119 route_name='atom_feed_home', request_method='GET',
118 120 renderer=None)
119 121 def atom(self):
120 122 """
121 123 Produce an atom-1.0 feed via feedgenerator module
122 124 """
123 125 self.load_default_context()
124 126
125 @cache_region('long_term')
126 def _generate_feed(cache_key):
127 def _generate_feed():
127 128 feed = Atom1Feed(
128 129 title=self.title % self.db_repo_name,
129 130 link=h.route_url('repo_summary', repo_name=self.db_repo_name),
130 131 description=self.description % self.db_repo_name,
131 132 language=self.language,
132 133 ttl=self.ttl
133 134 )
134 135
135 136 for commit in reversed(self._get_commits()):
136 137 date = self._set_timezone(commit.date)
137 138 feed.add_item(
138 139 unique_id=self.uid(self.db_repo.repo_id, commit.raw_id),
139 140 title=self._get_title(commit),
140 141 author_name=commit.author,
141 142 description=self._get_description(commit),
142 143 link=h.route_url(
143 144 'repo_commit', repo_name=self.db_repo_name,
144 145 commit_id=commit.raw_id),
145 146 pubdate=date,)
146 147
147 148 return feed.mime_type, feed.writeString('utf-8')
148 149
150 @cache_region('long_term')
151 def _generate_feed_and_cache(cache_key):
152 return _generate_feed()
153
154 if self.path_filter.is_enabled:
149 155 invalidator_context = CacheKey.repo_context_cache(
150 _generate_feed, self.db_repo_name, CacheKey.CACHE_TYPE_ATOM)
151
156 _generate_feed_and_cache, self.db_repo_name, CacheKey.CACHE_TYPE_ATOM)
152 157 with invalidator_context as context:
153 158 context.invalidate()
154 159 mime_type, feed = context.compute()
160 else:
161 mime_type, feed = _generate_feed()
155 162
156 163 response = Response(feed)
157 164 response.content_type = mime_type
158 165 return response
159 166
160 167 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
161 168 @HasRepoPermissionAnyDecorator(
162 169 'repository.read', 'repository.write', 'repository.admin')
163 170 @view_config(
164 171 route_name='rss_feed_home', request_method='GET',
165 172 renderer=None)
166 173 def rss(self):
167 174 """
168 175 Produce an rss2 feed via feedgenerator module
169 176 """
170 177 self.load_default_context()
171 178
172 @cache_region('long_term')
173 def _generate_feed(cache_key):
179 def _generate_feed():
174 180 feed = Rss201rev2Feed(
175 181 title=self.title % self.db_repo_name,
176 182 link=h.route_url('repo_summary', repo_name=self.db_repo_name),
177 183 description=self.description % self.db_repo_name,
178 184 language=self.language,
179 185 ttl=self.ttl
180 186 )
181 187
182 188 for commit in reversed(self._get_commits()):
183 189 date = self._set_timezone(commit.date)
184 190 feed.add_item(
185 191 unique_id=self.uid(self.db_repo.repo_id, commit.raw_id),
186 192 title=self._get_title(commit),
187 193 author_name=commit.author,
188 194 description=self._get_description(commit),
189 195 link=h.route_url(
190 196 'repo_commit', repo_name=self.db_repo_name,
191 197 commit_id=commit.raw_id),
192 198 pubdate=date,)
193 199
194 200 return feed.mime_type, feed.writeString('utf-8')
195 201
202 @cache_region('long_term')
203 def _generate_feed_and_cache(cache_key):
204 return _generate_feed()
205
206 if self.path_filter.is_enabled:
196 207 invalidator_context = CacheKey.repo_context_cache(
197 _generate_feed, self.db_repo_name, CacheKey.CACHE_TYPE_RSS)
208 _generate_feed_and_cache, self.db_repo_name, CacheKey.CACHE_TYPE_RSS)
198 209
199 210 with invalidator_context as context:
200 211 context.invalidate()
201 212 mime_type, feed = context.compute()
213 else:
214 mime_type, feed = _generate_feed()
202 215
203 216 response = Response(feed)
204 217 response.content_type = mime_type
205 218 return response
@@ -1,1292 +1,1292 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-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 import itertools
22 22 import logging
23 23 import os
24 24 import shutil
25 25 import tempfile
26 26 import collections
27 27
28 28 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
29 29 from pyramid.view import view_config
30 30 from pyramid.renderers import render
31 31 from pyramid.response import Response
32 32
33 33 from rhodecode.apps._base import RepoAppView
34 34
35 35 from rhodecode.controllers.utils import parse_path_ref
36 36 from rhodecode.lib import diffs, helpers as h, caches
37 37 from rhodecode.lib import audit_logger
38 38 from rhodecode.lib.exceptions import NonRelativePathError
39 39 from rhodecode.lib.codeblocks import (
40 40 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
41 41 from rhodecode.lib.utils2 import (
42 42 convert_line_endings, detect_mode, safe_str, str2bool)
43 43 from rhodecode.lib.auth import (
44 44 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
45 45 from rhodecode.lib.vcs import path as vcspath
46 46 from rhodecode.lib.vcs.backends.base import EmptyCommit
47 47 from rhodecode.lib.vcs.conf import settings
48 48 from rhodecode.lib.vcs.nodes import FileNode
49 49 from rhodecode.lib.vcs.exceptions import (
50 50 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
51 51 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
52 52 NodeDoesNotExistError, CommitError, NodeError)
53 53
54 54 from rhodecode.model.scm import ScmModel
55 55 from rhodecode.model.db import Repository
56 56
57 57 log = logging.getLogger(__name__)
58 58
59 59
60 60 class RepoFilesView(RepoAppView):
61 61
62 62 @staticmethod
63 63 def adjust_file_path_for_svn(f_path, repo):
64 64 """
65 65 Computes the relative path of `f_path`.
66 66
67 67 This is mainly based on prefix matching of the recognized tags and
68 68 branches in the underlying repository.
69 69 """
70 70 tags_and_branches = itertools.chain(
71 71 repo.branches.iterkeys(),
72 72 repo.tags.iterkeys())
73 73 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
74 74
75 75 for name in tags_and_branches:
76 76 if f_path.startswith('{}/'.format(name)):
77 77 f_path = vcspath.relpath(f_path, name)
78 78 break
79 79 return f_path
80 80
81 81 def load_default_context(self):
82 82 c = self._get_local_tmpl_context(include_app_defaults=True)
83 83
84 84 c.rhodecode_repo = self.rhodecode_vcs_repo
85 85
86 86
87 87 return c
88 88
89 89 def _ensure_not_locked(self):
90 90 _ = self.request.translate
91 91
92 92 repo = self.db_repo
93 93 if repo.enable_locking and repo.locked[0]:
94 94 h.flash(_('This repository has been locked by %s on %s')
95 95 % (h.person_by_id(repo.locked[0]),
96 96 h.format_date(h.time_to_datetime(repo.locked[1]))),
97 97 'warning')
98 98 files_url = h.route_path(
99 99 'repo_files:default_path',
100 100 repo_name=self.db_repo_name, commit_id='tip')
101 101 raise HTTPFound(files_url)
102 102
103 103 def _get_commit_and_path(self):
104 104 default_commit_id = self.db_repo.landing_rev[1]
105 105 default_f_path = '/'
106 106
107 107 commit_id = self.request.matchdict.get(
108 108 'commit_id', default_commit_id)
109 109 f_path = self._get_f_path(self.request.matchdict, default_f_path)
110 110 return commit_id, f_path
111 111
112 112 def _get_default_encoding(self, c):
113 113 enc_list = getattr(c, 'default_encodings', [])
114 114 return enc_list[0] if enc_list else 'UTF-8'
115 115
116 116 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
117 117 """
118 118 This is a safe way to get commit. If an error occurs it redirects to
119 119 tip with proper message
120 120
121 121 :param commit_id: id of commit to fetch
122 122 :param redirect_after: toggle redirection
123 123 """
124 124 _ = self.request.translate
125 125
126 126 try:
127 127 return self.rhodecode_vcs_repo.get_commit(commit_id)
128 128 except EmptyRepositoryError:
129 129 if not redirect_after:
130 130 return None
131 131
132 132 _url = h.route_path(
133 133 'repo_files_add_file',
134 134 repo_name=self.db_repo_name, commit_id=0, f_path='',
135 135 _anchor='edit')
136 136
137 137 if h.HasRepoPermissionAny(
138 138 'repository.write', 'repository.admin')(self.db_repo_name):
139 139 add_new = h.link_to(
140 140 _('Click here to add a new file.'), _url, class_="alert-link")
141 141 else:
142 142 add_new = ""
143 143
144 144 h.flash(h.literal(
145 145 _('There are no files yet. %s') % add_new), category='warning')
146 146 raise HTTPFound(
147 147 h.route_path('repo_summary', repo_name=self.db_repo_name))
148 148
149 149 except (CommitDoesNotExistError, LookupError):
150 150 msg = _('No such commit exists for this repository')
151 151 h.flash(msg, category='error')
152 152 raise HTTPNotFound()
153 153 except RepositoryError as e:
154 154 h.flash(safe_str(h.escape(e)), category='error')
155 155 raise HTTPNotFound()
156 156
157 157 def _get_filenode_or_redirect(self, commit_obj, path):
158 158 """
159 159 Returns file_node, if error occurs or given path is directory,
160 160 it'll redirect to top level path
161 161 """
162 162 _ = self.request.translate
163 163
164 164 try:
165 165 file_node = commit_obj.get_node(path)
166 166 if file_node.is_dir():
167 167 raise RepositoryError('The given path is a directory')
168 168 except CommitDoesNotExistError:
169 169 log.exception('No such commit exists for this repository')
170 170 h.flash(_('No such commit exists for this repository'), category='error')
171 171 raise HTTPNotFound()
172 172 except RepositoryError as e:
173 173 log.warning('Repository error while fetching '
174 174 'filenode `%s`. Err:%s', path, e)
175 175 h.flash(safe_str(h.escape(e)), category='error')
176 176 raise HTTPNotFound()
177 177
178 178 return file_node
179 179
180 180 def _is_valid_head(self, commit_id, repo):
181 181 # check if commit is a branch identifier- basically we cannot
182 182 # create multiple heads via file editing
183 183 valid_heads = repo.branches.keys() + repo.branches.values()
184 184
185 185 if h.is_svn(repo) and not repo.is_empty():
186 186 # Note: Subversion only has one head, we add it here in case there
187 187 # is no branch matched.
188 188 valid_heads.append(repo.get_commit(commit_idx=-1).raw_id)
189 189
190 190 # check if commit is a branch name or branch hash
191 191 return commit_id in valid_heads
192 192
193 193 def _get_tree_cache_manager(self, namespace_type):
194 194 _namespace = caches.get_repo_namespace_key(
195 195 namespace_type, self.db_repo_name)
196 196 return caches.get_cache_manager('repo_cache_long', _namespace)
197 197
198 198 def _get_tree_at_commit(
199 199 self, c, commit_id, f_path, full_load=False, force=False):
200 200 def _cached_tree():
201 201 log.debug('Generating cached file tree for %s, %s, %s',
202 202 self.db_repo_name, commit_id, f_path)
203 203
204 204 c.full_load = full_load
205 205 return render(
206 206 'rhodecode:templates/files/files_browser_tree.mako',
207 207 self._get_template_context(c), self.request)
208 208
209 209 cache_manager = self._get_tree_cache_manager(caches.FILE_TREE)
210 210
211 211 cache_key = caches.compute_key_from_params(
212 212 self.db_repo_name, commit_id, f_path)
213 213
214 214 if force:
215 215 # we want to force recompute of caches
216 216 cache_manager.remove_value(cache_key)
217 217
218 218 return cache_manager.get(cache_key, createfunc=_cached_tree)
219 219
220 220 def _get_archive_spec(self, fname):
221 221 log.debug('Detecting archive spec for: `%s`', fname)
222 222
223 223 fileformat = None
224 224 ext = None
225 225 content_type = None
226 226 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
227 227 content_type, extension = ext_data
228 228
229 229 if fname.endswith(extension):
230 230 fileformat = a_type
231 231 log.debug('archive is of type: %s', fileformat)
232 232 ext = extension
233 233 break
234 234
235 235 if not fileformat:
236 236 raise ValueError()
237 237
238 238 # left over part of whole fname is the commit
239 239 commit_id = fname[:-len(ext)]
240 240
241 241 return commit_id, ext, fileformat, content_type
242 242
243 243 @LoginRequired()
244 244 @HasRepoPermissionAnyDecorator(
245 245 'repository.read', 'repository.write', 'repository.admin')
246 246 @view_config(
247 247 route_name='repo_archivefile', request_method='GET',
248 248 renderer=None)
249 249 def repo_archivefile(self):
250 250 # archive cache config
251 251 from rhodecode import CONFIG
252 252 _ = self.request.translate
253 253 self.load_default_context()
254 254
255 255 fname = self.request.matchdict['fname']
256 256 subrepos = self.request.GET.get('subrepos') == 'true'
257 257
258 258 if not self.db_repo.enable_downloads:
259 259 return Response(_('Downloads disabled'))
260 260
261 261 try:
262 262 commit_id, ext, fileformat, content_type = \
263 263 self._get_archive_spec(fname)
264 264 except ValueError:
265 265 return Response(_('Unknown archive type for: `{}`').format(
266 266 h.escape(fname)))
267 267
268 268 try:
269 269 commit = self.rhodecode_vcs_repo.get_commit(commit_id)
270 270 except CommitDoesNotExistError:
271 271 return Response(_('Unknown commit_id {}').format(
272 272 h.escape(commit_id)))
273 273 except EmptyRepositoryError:
274 274 return Response(_('Empty repository'))
275 275
276 276 archive_name = '%s-%s%s%s' % (
277 277 safe_str(self.db_repo_name.replace('/', '_')),
278 278 '-sub' if subrepos else '',
279 279 safe_str(commit.short_id), ext)
280 280
281 281 use_cached_archive = False
282 282 archive_cache_enabled = CONFIG.get(
283 283 'archive_cache_dir') and not self.request.GET.get('no_cache')
284 284
285 285 if archive_cache_enabled:
286 286 # check if we it's ok to write
287 287 if not os.path.isdir(CONFIG['archive_cache_dir']):
288 288 os.makedirs(CONFIG['archive_cache_dir'])
289 289 cached_archive_path = os.path.join(
290 290 CONFIG['archive_cache_dir'], archive_name)
291 291 if os.path.isfile(cached_archive_path):
292 292 log.debug('Found cached archive in %s', cached_archive_path)
293 293 fd, archive = None, cached_archive_path
294 294 use_cached_archive = True
295 295 else:
296 296 log.debug('Archive %s is not yet cached', archive_name)
297 297
298 298 if not use_cached_archive:
299 299 # generate new archive
300 300 fd, archive = tempfile.mkstemp()
301 301 log.debug('Creating new temp archive in %s', archive)
302 302 try:
303 303 commit.archive_repo(archive, kind=fileformat, subrepos=subrepos)
304 304 except ImproperArchiveTypeError:
305 305 return _('Unknown archive type')
306 306 if archive_cache_enabled:
307 307 # if we generated the archive and we have cache enabled
308 308 # let's use this for future
309 309 log.debug('Storing new archive in %s', cached_archive_path)
310 310 shutil.move(archive, cached_archive_path)
311 311 archive = cached_archive_path
312 312
313 313 # store download action
314 314 audit_logger.store_web(
315 315 'repo.archive.download', action_data={
316 316 'user_agent': self.request.user_agent,
317 317 'archive_name': archive_name,
318 318 'archive_spec': fname,
319 319 'archive_cached': use_cached_archive},
320 320 user=self._rhodecode_user,
321 321 repo=self.db_repo,
322 322 commit=True
323 323 )
324 324
325 325 def get_chunked_archive(archive):
326 326 with open(archive, 'rb') as stream:
327 327 while True:
328 328 data = stream.read(16 * 1024)
329 329 if not data:
330 330 if fd: # fd means we used temporary file
331 331 os.close(fd)
332 332 if not archive_cache_enabled:
333 333 log.debug('Destroying temp archive %s', archive)
334 334 os.remove(archive)
335 335 break
336 336 yield data
337 337
338 338 response = Response(app_iter=get_chunked_archive(archive))
339 339 response.content_disposition = str(
340 340 'attachment; filename=%s' % archive_name)
341 341 response.content_type = str(content_type)
342 342
343 343 return response
344 344
345 345 def _get_file_node(self, commit_id, f_path):
346 346 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
347 347 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
348 348 try:
349 349 node = commit.get_node(f_path)
350 350 if node.is_dir():
351 351 raise NodeError('%s path is a %s not a file'
352 352 % (node, type(node)))
353 353 except NodeDoesNotExistError:
354 354 commit = EmptyCommit(
355 355 commit_id=commit_id,
356 356 idx=commit.idx,
357 357 repo=commit.repository,
358 358 alias=commit.repository.alias,
359 359 message=commit.message,
360 360 author=commit.author,
361 361 date=commit.date)
362 362 node = FileNode(f_path, '', commit=commit)
363 363 else:
364 364 commit = EmptyCommit(
365 365 repo=self.rhodecode_vcs_repo,
366 366 alias=self.rhodecode_vcs_repo.alias)
367 367 node = FileNode(f_path, '', commit=commit)
368 368 return node
369 369
370 370 @LoginRequired()
371 371 @HasRepoPermissionAnyDecorator(
372 372 'repository.read', 'repository.write', 'repository.admin')
373 373 @view_config(
374 374 route_name='repo_files_diff', request_method='GET',
375 375 renderer=None)
376 376 def repo_files_diff(self):
377 377 c = self.load_default_context()
378 378 f_path = self._get_f_path(self.request.matchdict)
379 379 diff1 = self.request.GET.get('diff1', '')
380 380 diff2 = self.request.GET.get('diff2', '')
381 381
382 382 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
383 383
384 384 ignore_whitespace = str2bool(self.request.GET.get('ignorews'))
385 385 line_context = self.request.GET.get('context', 3)
386 386
387 387 if not any((diff1, diff2)):
388 388 h.flash(
389 389 'Need query parameter "diff1" or "diff2" to generate a diff.',
390 390 category='error')
391 391 raise HTTPBadRequest()
392 392
393 393 c.action = self.request.GET.get('diff')
394 394 if c.action not in ['download', 'raw']:
395 395 compare_url = h.route_path(
396 396 'repo_compare',
397 397 repo_name=self.db_repo_name,
398 398 source_ref_type='rev',
399 399 source_ref=diff1,
400 400 target_repo=self.db_repo_name,
401 401 target_ref_type='rev',
402 402 target_ref=diff2,
403 403 _query=dict(f_path=f_path))
404 404 # redirect to new view if we render diff
405 405 raise HTTPFound(compare_url)
406 406
407 407 try:
408 408 node1 = self._get_file_node(diff1, path1)
409 409 node2 = self._get_file_node(diff2, f_path)
410 410 except (RepositoryError, NodeError):
411 411 log.exception("Exception while trying to get node from repository")
412 412 raise HTTPFound(
413 413 h.route_path('repo_files', repo_name=self.db_repo_name,
414 414 commit_id='tip', f_path=f_path))
415 415
416 416 if all(isinstance(node.commit, EmptyCommit)
417 417 for node in (node1, node2)):
418 418 raise HTTPNotFound()
419 419
420 420 c.commit_1 = node1.commit
421 421 c.commit_2 = node2.commit
422 422
423 423 if c.action == 'download':
424 424 _diff = diffs.get_gitdiff(node1, node2,
425 425 ignore_whitespace=ignore_whitespace,
426 426 context=line_context)
427 427 diff = diffs.DiffProcessor(_diff, format='gitdiff')
428 428
429 response = Response(diff.as_raw())
429 response = Response(self.path_filter.get_raw_patch(diff))
430 430 response.content_type = 'text/plain'
431 431 response.content_disposition = (
432 432 'attachment; filename=%s_%s_vs_%s.diff' % (f_path, diff1, diff2)
433 433 )
434 434 charset = self._get_default_encoding(c)
435 435 if charset:
436 436 response.charset = charset
437 437 return response
438 438
439 439 elif c.action == 'raw':
440 440 _diff = diffs.get_gitdiff(node1, node2,
441 441 ignore_whitespace=ignore_whitespace,
442 442 context=line_context)
443 443 diff = diffs.DiffProcessor(_diff, format='gitdiff')
444 444
445 response = Response(diff.as_raw())
445 response = Response(self.path_filter.get_raw_patch(diff))
446 446 response.content_type = 'text/plain'
447 447 charset = self._get_default_encoding(c)
448 448 if charset:
449 449 response.charset = charset
450 450 return response
451 451
452 452 # in case we ever end up here
453 453 raise HTTPNotFound()
454 454
455 455 @LoginRequired()
456 456 @HasRepoPermissionAnyDecorator(
457 457 'repository.read', 'repository.write', 'repository.admin')
458 458 @view_config(
459 459 route_name='repo_files_diff_2way_redirect', request_method='GET',
460 460 renderer=None)
461 461 def repo_files_diff_2way_redirect(self):
462 462 """
463 463 Kept only to make OLD links work
464 464 """
465 465 f_path = self._get_f_path(self.request.matchdict)
466 466 diff1 = self.request.GET.get('diff1', '')
467 467 diff2 = self.request.GET.get('diff2', '')
468 468
469 469 if not any((diff1, diff2)):
470 470 h.flash(
471 471 'Need query parameter "diff1" or "diff2" to generate a diff.',
472 472 category='error')
473 473 raise HTTPBadRequest()
474 474
475 475 compare_url = h.route_path(
476 476 'repo_compare',
477 477 repo_name=self.db_repo_name,
478 478 source_ref_type='rev',
479 479 source_ref=diff1,
480 480 target_ref_type='rev',
481 481 target_ref=diff2,
482 482 _query=dict(f_path=f_path, diffmode='sideside',
483 483 target_repo=self.db_repo_name,))
484 484 raise HTTPFound(compare_url)
485 485
486 486 @LoginRequired()
487 487 @HasRepoPermissionAnyDecorator(
488 488 'repository.read', 'repository.write', 'repository.admin')
489 489 @view_config(
490 490 route_name='repo_files', request_method='GET',
491 491 renderer=None)
492 492 @view_config(
493 493 route_name='repo_files:default_path', request_method='GET',
494 494 renderer=None)
495 495 @view_config(
496 496 route_name='repo_files:default_commit', request_method='GET',
497 497 renderer=None)
498 498 @view_config(
499 499 route_name='repo_files:rendered', request_method='GET',
500 500 renderer=None)
501 501 @view_config(
502 502 route_name='repo_files:annotated', request_method='GET',
503 503 renderer=None)
504 504 def repo_files(self):
505 505 c = self.load_default_context()
506 506
507 507 view_name = getattr(self.request.matched_route, 'name', None)
508 508
509 509 c.annotate = view_name == 'repo_files:annotated'
510 510 # default is false, but .rst/.md files later are auto rendered, we can
511 511 # overwrite auto rendering by setting this GET flag
512 512 c.renderer = view_name == 'repo_files:rendered' or \
513 513 not self.request.GET.get('no-render', False)
514 514
515 515 # redirect to given commit_id from form if given
516 516 get_commit_id = self.request.GET.get('at_rev', None)
517 517 if get_commit_id:
518 518 self._get_commit_or_redirect(get_commit_id)
519 519
520 520 commit_id, f_path = self._get_commit_and_path()
521 521 c.commit = self._get_commit_or_redirect(commit_id)
522 522 c.branch = self.request.GET.get('branch', None)
523 523 c.f_path = f_path
524 524
525 525 # prev link
526 526 try:
527 527 prev_commit = c.commit.prev(c.branch)
528 528 c.prev_commit = prev_commit
529 529 c.url_prev = h.route_path(
530 530 'repo_files', repo_name=self.db_repo_name,
531 531 commit_id=prev_commit.raw_id, f_path=f_path)
532 532 if c.branch:
533 533 c.url_prev += '?branch=%s' % c.branch
534 534 except (CommitDoesNotExistError, VCSError):
535 535 c.url_prev = '#'
536 536 c.prev_commit = EmptyCommit()
537 537
538 538 # next link
539 539 try:
540 540 next_commit = c.commit.next(c.branch)
541 541 c.next_commit = next_commit
542 542 c.url_next = h.route_path(
543 543 'repo_files', repo_name=self.db_repo_name,
544 544 commit_id=next_commit.raw_id, f_path=f_path)
545 545 if c.branch:
546 546 c.url_next += '?branch=%s' % c.branch
547 547 except (CommitDoesNotExistError, VCSError):
548 548 c.url_next = '#'
549 549 c.next_commit = EmptyCommit()
550 550
551 551 # files or dirs
552 552 try:
553 553 c.file = c.commit.get_node(f_path)
554 554 c.file_author = True
555 555 c.file_tree = ''
556 556
557 557 # load file content
558 558 if c.file.is_file():
559 559 c.lf_node = c.file.get_largefile_node()
560 560
561 561 c.file_source_page = 'true'
562 562 c.file_last_commit = c.file.last_commit
563 563 if c.file.size < c.visual.cut_off_limit_diff:
564 564 if c.annotate: # annotation has precedence over renderer
565 565 c.annotated_lines = filenode_as_annotated_lines_tokens(
566 566 c.file
567 567 )
568 568 else:
569 569 c.renderer = (
570 570 c.renderer and h.renderer_from_filename(c.file.path)
571 571 )
572 572 if not c.renderer:
573 573 c.lines = filenode_as_lines_tokens(c.file)
574 574
575 575 c.on_branch_head = self._is_valid_head(
576 576 commit_id, self.rhodecode_vcs_repo)
577 577
578 578 branch = c.commit.branch if (
579 579 c.commit.branch and '/' not in c.commit.branch) else None
580 580 c.branch_or_raw_id = branch or c.commit.raw_id
581 581 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
582 582
583 583 author = c.file_last_commit.author
584 584 c.authors = [[
585 585 h.email(author),
586 586 h.person(author, 'username_or_name_or_email'),
587 587 1
588 588 ]]
589 589
590 590 else: # load tree content at path
591 591 c.file_source_page = 'false'
592 592 c.authors = []
593 593 # this loads a simple tree without metadata to speed things up
594 594 # later via ajax we call repo_nodetree_full and fetch whole
595 595 c.file_tree = self._get_tree_at_commit(
596 596 c, c.commit.raw_id, f_path)
597 597
598 598 except RepositoryError as e:
599 599 h.flash(safe_str(h.escape(e)), category='error')
600 600 raise HTTPNotFound()
601 601
602 602 if self.request.environ.get('HTTP_X_PJAX'):
603 603 html = render('rhodecode:templates/files/files_pjax.mako',
604 604 self._get_template_context(c), self.request)
605 605 else:
606 606 html = render('rhodecode:templates/files/files.mako',
607 607 self._get_template_context(c), self.request)
608 608 return Response(html)
609 609
610 610 @HasRepoPermissionAnyDecorator(
611 611 'repository.read', 'repository.write', 'repository.admin')
612 612 @view_config(
613 613 route_name='repo_files:annotated_previous', request_method='GET',
614 614 renderer=None)
615 615 def repo_files_annotated_previous(self):
616 616 self.load_default_context()
617 617
618 618 commit_id, f_path = self._get_commit_and_path()
619 619 commit = self._get_commit_or_redirect(commit_id)
620 620 prev_commit_id = commit.raw_id
621 621 line_anchor = self.request.GET.get('line_anchor')
622 622 is_file = False
623 623 try:
624 624 _file = commit.get_node(f_path)
625 625 is_file = _file.is_file()
626 626 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
627 627 pass
628 628
629 629 if is_file:
630 630 history = commit.get_file_history(f_path)
631 631 prev_commit_id = history[1].raw_id \
632 632 if len(history) > 1 else prev_commit_id
633 633 prev_url = h.route_path(
634 634 'repo_files:annotated', repo_name=self.db_repo_name,
635 635 commit_id=prev_commit_id, f_path=f_path,
636 636 _anchor='L{}'.format(line_anchor))
637 637
638 638 raise HTTPFound(prev_url)
639 639
640 640 @LoginRequired()
641 641 @HasRepoPermissionAnyDecorator(
642 642 'repository.read', 'repository.write', 'repository.admin')
643 643 @view_config(
644 644 route_name='repo_nodetree_full', request_method='GET',
645 645 renderer=None, xhr=True)
646 646 @view_config(
647 647 route_name='repo_nodetree_full:default_path', request_method='GET',
648 648 renderer=None, xhr=True)
649 649 def repo_nodetree_full(self):
650 650 """
651 651 Returns rendered html of file tree that contains commit date,
652 652 author, commit_id for the specified combination of
653 653 repo, commit_id and file path
654 654 """
655 655 c = self.load_default_context()
656 656
657 657 commit_id, f_path = self._get_commit_and_path()
658 658 commit = self._get_commit_or_redirect(commit_id)
659 659 try:
660 660 dir_node = commit.get_node(f_path)
661 661 except RepositoryError as e:
662 662 return Response('error: {}'.format(h.escape(safe_str(e))))
663 663
664 664 if dir_node.is_file():
665 665 return Response('')
666 666
667 667 c.file = dir_node
668 668 c.commit = commit
669 669
670 670 # using force=True here, make a little trick. We flush the cache and
671 671 # compute it using the same key as without previous full_load, so now
672 672 # the fully loaded tree is now returned instead of partial,
673 673 # and we store this in caches
674 674 html = self._get_tree_at_commit(
675 675 c, commit.raw_id, dir_node.path, full_load=True, force=True)
676 676
677 677 return Response(html)
678 678
679 679 def _get_attachement_disposition(self, f_path):
680 680 return 'attachment; filename=%s' % \
681 681 safe_str(f_path.split(Repository.NAME_SEP)[-1])
682 682
683 683 @LoginRequired()
684 684 @HasRepoPermissionAnyDecorator(
685 685 'repository.read', 'repository.write', 'repository.admin')
686 686 @view_config(
687 687 route_name='repo_file_raw', request_method='GET',
688 688 renderer=None)
689 689 def repo_file_raw(self):
690 690 """
691 691 Action for show as raw, some mimetypes are "rendered",
692 692 those include images, icons.
693 693 """
694 694 c = self.load_default_context()
695 695
696 696 commit_id, f_path = self._get_commit_and_path()
697 697 commit = self._get_commit_or_redirect(commit_id)
698 698 file_node = self._get_filenode_or_redirect(commit, f_path)
699 699
700 700 raw_mimetype_mapping = {
701 701 # map original mimetype to a mimetype used for "show as raw"
702 702 # you can also provide a content-disposition to override the
703 703 # default "attachment" disposition.
704 704 # orig_type: (new_type, new_dispo)
705 705
706 706 # show images inline:
707 707 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
708 708 # for example render an SVG with javascript inside or even render
709 709 # HTML.
710 710 'image/x-icon': ('image/x-icon', 'inline'),
711 711 'image/png': ('image/png', 'inline'),
712 712 'image/gif': ('image/gif', 'inline'),
713 713 'image/jpeg': ('image/jpeg', 'inline'),
714 714 'application/pdf': ('application/pdf', 'inline'),
715 715 }
716 716
717 717 mimetype = file_node.mimetype
718 718 try:
719 719 mimetype, disposition = raw_mimetype_mapping[mimetype]
720 720 except KeyError:
721 721 # we don't know anything special about this, handle it safely
722 722 if file_node.is_binary:
723 723 # do same as download raw for binary files
724 724 mimetype, disposition = 'application/octet-stream', 'attachment'
725 725 else:
726 726 # do not just use the original mimetype, but force text/plain,
727 727 # otherwise it would serve text/html and that might be unsafe.
728 728 # Note: underlying vcs library fakes text/plain mimetype if the
729 729 # mimetype can not be determined and it thinks it is not
730 730 # binary.This might lead to erroneous text display in some
731 731 # cases, but helps in other cases, like with text files
732 732 # without extension.
733 733 mimetype, disposition = 'text/plain', 'inline'
734 734
735 735 if disposition == 'attachment':
736 736 disposition = self._get_attachement_disposition(f_path)
737 737
738 738 def stream_node():
739 739 yield file_node.raw_bytes
740 740
741 741 response = Response(app_iter=stream_node())
742 742 response.content_disposition = disposition
743 743 response.content_type = mimetype
744 744
745 745 charset = self._get_default_encoding(c)
746 746 if charset:
747 747 response.charset = charset
748 748
749 749 return response
750 750
751 751 @LoginRequired()
752 752 @HasRepoPermissionAnyDecorator(
753 753 'repository.read', 'repository.write', 'repository.admin')
754 754 @view_config(
755 755 route_name='repo_file_download', request_method='GET',
756 756 renderer=None)
757 757 @view_config(
758 758 route_name='repo_file_download:legacy', request_method='GET',
759 759 renderer=None)
760 760 def repo_file_download(self):
761 761 c = self.load_default_context()
762 762
763 763 commit_id, f_path = self._get_commit_and_path()
764 764 commit = self._get_commit_or_redirect(commit_id)
765 765 file_node = self._get_filenode_or_redirect(commit, f_path)
766 766
767 767 if self.request.GET.get('lf'):
768 768 # only if lf get flag is passed, we download this file
769 769 # as LFS/Largefile
770 770 lf_node = file_node.get_largefile_node()
771 771 if lf_node:
772 772 # overwrite our pointer with the REAL large-file
773 773 file_node = lf_node
774 774
775 775 disposition = self._get_attachement_disposition(f_path)
776 776
777 777 def stream_node():
778 778 yield file_node.raw_bytes
779 779
780 780 response = Response(app_iter=stream_node())
781 781 response.content_disposition = disposition
782 782 response.content_type = file_node.mimetype
783 783
784 784 charset = self._get_default_encoding(c)
785 785 if charset:
786 786 response.charset = charset
787 787
788 788 return response
789 789
790 790 def _get_nodelist_at_commit(self, repo_name, commit_id, f_path):
791 791 def _cached_nodes():
792 792 log.debug('Generating cached nodelist for %s, %s, %s',
793 793 repo_name, commit_id, f_path)
794 794 try:
795 795 _d, _f = ScmModel().get_nodes(
796 796 repo_name, commit_id, f_path, flat=False)
797 797 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
798 798 log.exception(safe_str(e))
799 799 h.flash(safe_str(h.escape(e)), category='error')
800 800 raise HTTPFound(h.route_path(
801 801 'repo_files', repo_name=self.db_repo_name,
802 802 commit_id='tip', f_path='/'))
803 803 return _d + _f
804 804
805 805 cache_manager = self._get_tree_cache_manager(
806 806 caches.FILE_SEARCH_TREE_META)
807 807
808 808 cache_key = caches.compute_key_from_params(
809 809 repo_name, commit_id, f_path)
810 810 return cache_manager.get(cache_key, createfunc=_cached_nodes)
811 811
812 812 @LoginRequired()
813 813 @HasRepoPermissionAnyDecorator(
814 814 'repository.read', 'repository.write', 'repository.admin')
815 815 @view_config(
816 816 route_name='repo_files_nodelist', request_method='GET',
817 817 renderer='json_ext', xhr=True)
818 818 def repo_nodelist(self):
819 819 self.load_default_context()
820 820
821 821 commit_id, f_path = self._get_commit_and_path()
822 822 commit = self._get_commit_or_redirect(commit_id)
823 823
824 824 metadata = self._get_nodelist_at_commit(
825 825 self.db_repo_name, commit.raw_id, f_path)
826 826 return {'nodes': metadata}
827 827
828 828 def _create_references(
829 829 self, branches_or_tags, symbolic_reference, f_path):
830 830 items = []
831 831 for name, commit_id in branches_or_tags.items():
832 832 sym_ref = symbolic_reference(commit_id, name, f_path)
833 833 items.append((sym_ref, name))
834 834 return items
835 835
836 836 def _symbolic_reference(self, commit_id, name, f_path):
837 837 return commit_id
838 838
839 839 def _symbolic_reference_svn(self, commit_id, name, f_path):
840 840 new_f_path = vcspath.join(name, f_path)
841 841 return u'%s@%s' % (new_f_path, commit_id)
842 842
843 843 def _get_node_history(self, commit_obj, f_path, commits=None):
844 844 """
845 845 get commit history for given node
846 846
847 847 :param commit_obj: commit to calculate history
848 848 :param f_path: path for node to calculate history for
849 849 :param commits: if passed don't calculate history and take
850 850 commits defined in this list
851 851 """
852 852 _ = self.request.translate
853 853
854 854 # calculate history based on tip
855 855 tip = self.rhodecode_vcs_repo.get_commit()
856 856 if commits is None:
857 857 pre_load = ["author", "branch"]
858 858 try:
859 859 commits = tip.get_file_history(f_path, pre_load=pre_load)
860 860 except (NodeDoesNotExistError, CommitError):
861 861 # this node is not present at tip!
862 862 commits = commit_obj.get_file_history(f_path, pre_load=pre_load)
863 863
864 864 history = []
865 865 commits_group = ([], _("Changesets"))
866 866 for commit in commits:
867 867 branch = ' (%s)' % commit.branch if commit.branch else ''
868 868 n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch)
869 869 commits_group[0].append((commit.raw_id, n_desc,))
870 870 history.append(commits_group)
871 871
872 872 symbolic_reference = self._symbolic_reference
873 873
874 874 if self.rhodecode_vcs_repo.alias == 'svn':
875 875 adjusted_f_path = RepoFilesView.adjust_file_path_for_svn(
876 876 f_path, self.rhodecode_vcs_repo)
877 877 if adjusted_f_path != f_path:
878 878 log.debug(
879 879 'Recognized svn tag or branch in file "%s", using svn '
880 880 'specific symbolic references', f_path)
881 881 f_path = adjusted_f_path
882 882 symbolic_reference = self._symbolic_reference_svn
883 883
884 884 branches = self._create_references(
885 885 self.rhodecode_vcs_repo.branches, symbolic_reference, f_path)
886 886 branches_group = (branches, _("Branches"))
887 887
888 888 tags = self._create_references(
889 889 self.rhodecode_vcs_repo.tags, symbolic_reference, f_path)
890 890 tags_group = (tags, _("Tags"))
891 891
892 892 history.append(branches_group)
893 893 history.append(tags_group)
894 894
895 895 return history, commits
896 896
897 897 @LoginRequired()
898 898 @HasRepoPermissionAnyDecorator(
899 899 'repository.read', 'repository.write', 'repository.admin')
900 900 @view_config(
901 901 route_name='repo_file_history', request_method='GET',
902 902 renderer='json_ext')
903 903 def repo_file_history(self):
904 904 self.load_default_context()
905 905
906 906 commit_id, f_path = self._get_commit_and_path()
907 907 commit = self._get_commit_or_redirect(commit_id)
908 908 file_node = self._get_filenode_or_redirect(commit, f_path)
909 909
910 910 if file_node.is_file():
911 911 file_history, _hist = self._get_node_history(commit, f_path)
912 912
913 913 res = []
914 914 for obj in file_history:
915 915 res.append({
916 916 'text': obj[1],
917 917 'children': [{'id': o[0], 'text': o[1]} for o in obj[0]]
918 918 })
919 919
920 920 data = {
921 921 'more': False,
922 922 'results': res
923 923 }
924 924 return data
925 925
926 926 log.warning('Cannot fetch history for directory')
927 927 raise HTTPBadRequest()
928 928
929 929 @LoginRequired()
930 930 @HasRepoPermissionAnyDecorator(
931 931 'repository.read', 'repository.write', 'repository.admin')
932 932 @view_config(
933 933 route_name='repo_file_authors', request_method='GET',
934 934 renderer='rhodecode:templates/files/file_authors_box.mako')
935 935 def repo_file_authors(self):
936 936 c = self.load_default_context()
937 937
938 938 commit_id, f_path = self._get_commit_and_path()
939 939 commit = self._get_commit_or_redirect(commit_id)
940 940 file_node = self._get_filenode_or_redirect(commit, f_path)
941 941
942 942 if not file_node.is_file():
943 943 raise HTTPBadRequest()
944 944
945 945 c.file_last_commit = file_node.last_commit
946 946 if self.request.GET.get('annotate') == '1':
947 947 # use _hist from annotation if annotation mode is on
948 948 commit_ids = set(x[1] for x in file_node.annotate)
949 949 _hist = (
950 950 self.rhodecode_vcs_repo.get_commit(commit_id)
951 951 for commit_id in commit_ids)
952 952 else:
953 953 _f_history, _hist = self._get_node_history(commit, f_path)
954 954 c.file_author = False
955 955
956 956 unique = collections.OrderedDict()
957 957 for commit in _hist:
958 958 author = commit.author
959 959 if author not in unique:
960 960 unique[commit.author] = [
961 961 h.email(author),
962 962 h.person(author, 'username_or_name_or_email'),
963 963 1 # counter
964 964 ]
965 965
966 966 else:
967 967 # increase counter
968 968 unique[commit.author][2] += 1
969 969
970 970 c.authors = [val for val in unique.values()]
971 971
972 972 return self._get_template_context(c)
973 973
974 974 @LoginRequired()
975 975 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
976 976 @view_config(
977 977 route_name='repo_files_remove_file', request_method='GET',
978 978 renderer='rhodecode:templates/files/files_delete.mako')
979 979 def repo_files_remove_file(self):
980 980 _ = self.request.translate
981 981 c = self.load_default_context()
982 982 commit_id, f_path = self._get_commit_and_path()
983 983
984 984 self._ensure_not_locked()
985 985
986 986 if not self._is_valid_head(commit_id, self.rhodecode_vcs_repo):
987 987 h.flash(_('You can only delete files with commit '
988 988 'being a valid branch '), category='warning')
989 989 raise HTTPFound(
990 990 h.route_path('repo_files',
991 991 repo_name=self.db_repo_name, commit_id='tip',
992 992 f_path=f_path))
993 993
994 994 c.commit = self._get_commit_or_redirect(commit_id)
995 995 c.file = self._get_filenode_or_redirect(c.commit, f_path)
996 996
997 997 c.default_message = _(
998 998 'Deleted file {} via RhodeCode Enterprise').format(f_path)
999 999 c.f_path = f_path
1000 1000
1001 1001 return self._get_template_context(c)
1002 1002
1003 1003 @LoginRequired()
1004 1004 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1005 1005 @CSRFRequired()
1006 1006 @view_config(
1007 1007 route_name='repo_files_delete_file', request_method='POST',
1008 1008 renderer=None)
1009 1009 def repo_files_delete_file(self):
1010 1010 _ = self.request.translate
1011 1011
1012 1012 c = self.load_default_context()
1013 1013 commit_id, f_path = self._get_commit_and_path()
1014 1014
1015 1015 self._ensure_not_locked()
1016 1016
1017 1017 if not self._is_valid_head(commit_id, self.rhodecode_vcs_repo):
1018 1018 h.flash(_('You can only delete files with commit '
1019 1019 'being a valid branch '), category='warning')
1020 1020 raise HTTPFound(
1021 1021 h.route_path('repo_files',
1022 1022 repo_name=self.db_repo_name, commit_id='tip',
1023 1023 f_path=f_path))
1024 1024
1025 1025 c.commit = self._get_commit_or_redirect(commit_id)
1026 1026 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1027 1027
1028 1028 c.default_message = _(
1029 1029 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1030 1030 c.f_path = f_path
1031 1031 node_path = f_path
1032 1032 author = self._rhodecode_db_user.full_contact
1033 1033 message = self.request.POST.get('message') or c.default_message
1034 1034 try:
1035 1035 nodes = {
1036 1036 node_path: {
1037 1037 'content': ''
1038 1038 }
1039 1039 }
1040 1040 ScmModel().delete_nodes(
1041 1041 user=self._rhodecode_db_user.user_id, repo=self.db_repo,
1042 1042 message=message,
1043 1043 nodes=nodes,
1044 1044 parent_commit=c.commit,
1045 1045 author=author,
1046 1046 )
1047 1047
1048 1048 h.flash(
1049 1049 _('Successfully deleted file `{}`').format(
1050 1050 h.escape(f_path)), category='success')
1051 1051 except Exception:
1052 1052 log.exception('Error during commit operation')
1053 1053 h.flash(_('Error occurred during commit'), category='error')
1054 1054 raise HTTPFound(
1055 1055 h.route_path('repo_commit', repo_name=self.db_repo_name,
1056 1056 commit_id='tip'))
1057 1057
1058 1058 @LoginRequired()
1059 1059 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1060 1060 @view_config(
1061 1061 route_name='repo_files_edit_file', request_method='GET',
1062 1062 renderer='rhodecode:templates/files/files_edit.mako')
1063 1063 def repo_files_edit_file(self):
1064 1064 _ = self.request.translate
1065 1065 c = self.load_default_context()
1066 1066 commit_id, f_path = self._get_commit_and_path()
1067 1067
1068 1068 self._ensure_not_locked()
1069 1069
1070 1070 if not self._is_valid_head(commit_id, self.rhodecode_vcs_repo):
1071 1071 h.flash(_('You can only edit files with commit '
1072 1072 'being a valid branch '), category='warning')
1073 1073 raise HTTPFound(
1074 1074 h.route_path('repo_files',
1075 1075 repo_name=self.db_repo_name, commit_id='tip',
1076 1076 f_path=f_path))
1077 1077
1078 1078 c.commit = self._get_commit_or_redirect(commit_id)
1079 1079 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1080 1080
1081 1081 if c.file.is_binary:
1082 1082 files_url = h.route_path(
1083 1083 'repo_files',
1084 1084 repo_name=self.db_repo_name,
1085 1085 commit_id=c.commit.raw_id, f_path=f_path)
1086 1086 raise HTTPFound(files_url)
1087 1087
1088 1088 c.default_message = _(
1089 1089 'Edited file {} via RhodeCode Enterprise').format(f_path)
1090 1090 c.f_path = f_path
1091 1091
1092 1092 return self._get_template_context(c)
1093 1093
1094 1094 @LoginRequired()
1095 1095 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1096 1096 @CSRFRequired()
1097 1097 @view_config(
1098 1098 route_name='repo_files_update_file', request_method='POST',
1099 1099 renderer=None)
1100 1100 def repo_files_update_file(self):
1101 1101 _ = self.request.translate
1102 1102 c = self.load_default_context()
1103 1103 commit_id, f_path = self._get_commit_and_path()
1104 1104
1105 1105 self._ensure_not_locked()
1106 1106
1107 1107 if not self._is_valid_head(commit_id, self.rhodecode_vcs_repo):
1108 1108 h.flash(_('You can only edit files with commit '
1109 1109 'being a valid branch '), category='warning')
1110 1110 raise HTTPFound(
1111 1111 h.route_path('repo_files',
1112 1112 repo_name=self.db_repo_name, commit_id='tip',
1113 1113 f_path=f_path))
1114 1114
1115 1115 c.commit = self._get_commit_or_redirect(commit_id)
1116 1116 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1117 1117
1118 1118 if c.file.is_binary:
1119 1119 raise HTTPFound(
1120 1120 h.route_path('repo_files',
1121 1121 repo_name=self.db_repo_name,
1122 1122 commit_id=c.commit.raw_id,
1123 1123 f_path=f_path))
1124 1124
1125 1125 c.default_message = _(
1126 1126 'Edited file {} via RhodeCode Enterprise').format(f_path)
1127 1127 c.f_path = f_path
1128 1128 old_content = c.file.content
1129 1129 sl = old_content.splitlines(1)
1130 1130 first_line = sl[0] if sl else ''
1131 1131
1132 1132 r_post = self.request.POST
1133 1133 # modes: 0 - Unix, 1 - Mac, 2 - DOS
1134 1134 mode = detect_mode(first_line, 0)
1135 1135 content = convert_line_endings(r_post.get('content', ''), mode)
1136 1136
1137 1137 message = r_post.get('message') or c.default_message
1138 1138 org_f_path = c.file.unicode_path
1139 1139 filename = r_post['filename']
1140 1140 org_filename = c.file.name
1141 1141
1142 1142 if content == old_content and filename == org_filename:
1143 1143 h.flash(_('No changes'), category='warning')
1144 1144 raise HTTPFound(
1145 1145 h.route_path('repo_commit', repo_name=self.db_repo_name,
1146 1146 commit_id='tip'))
1147 1147 try:
1148 1148 mapping = {
1149 1149 org_f_path: {
1150 1150 'org_filename': org_f_path,
1151 1151 'filename': os.path.join(c.file.dir_path, filename),
1152 1152 'content': content,
1153 1153 'lexer': '',
1154 1154 'op': 'mod',
1155 1155 }
1156 1156 }
1157 1157
1158 1158 ScmModel().update_nodes(
1159 1159 user=self._rhodecode_db_user.user_id,
1160 1160 repo=self.db_repo,
1161 1161 message=message,
1162 1162 nodes=mapping,
1163 1163 parent_commit=c.commit,
1164 1164 )
1165 1165
1166 1166 h.flash(
1167 1167 _('Successfully committed changes to file `{}`').format(
1168 1168 h.escape(f_path)), category='success')
1169 1169 except Exception:
1170 1170 log.exception('Error occurred during commit')
1171 1171 h.flash(_('Error occurred during commit'), category='error')
1172 1172 raise HTTPFound(
1173 1173 h.route_path('repo_commit', repo_name=self.db_repo_name,
1174 1174 commit_id='tip'))
1175 1175
1176 1176 @LoginRequired()
1177 1177 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1178 1178 @view_config(
1179 1179 route_name='repo_files_add_file', request_method='GET',
1180 1180 renderer='rhodecode:templates/files/files_add.mako')
1181 1181 def repo_files_add_file(self):
1182 1182 _ = self.request.translate
1183 1183 c = self.load_default_context()
1184 1184 commit_id, f_path = self._get_commit_and_path()
1185 1185
1186 1186 self._ensure_not_locked()
1187 1187
1188 1188 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1189 1189 if c.commit is None:
1190 1190 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1191 1191 c.default_message = (_('Added file via RhodeCode Enterprise'))
1192 1192 c.f_path = f_path.lstrip('/') # ensure not relative path
1193 1193
1194 1194 return self._get_template_context(c)
1195 1195
1196 1196 @LoginRequired()
1197 1197 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1198 1198 @CSRFRequired()
1199 1199 @view_config(
1200 1200 route_name='repo_files_create_file', request_method='POST',
1201 1201 renderer=None)
1202 1202 def repo_files_create_file(self):
1203 1203 _ = self.request.translate
1204 1204 c = self.load_default_context()
1205 1205 commit_id, f_path = self._get_commit_and_path()
1206 1206
1207 1207 self._ensure_not_locked()
1208 1208
1209 1209 r_post = self.request.POST
1210 1210
1211 1211 c.commit = self._get_commit_or_redirect(
1212 1212 commit_id, redirect_after=False)
1213 1213 if c.commit is None:
1214 1214 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1215 1215 c.default_message = (_('Added file via RhodeCode Enterprise'))
1216 1216 c.f_path = f_path
1217 1217 unix_mode = 0
1218 1218 content = convert_line_endings(r_post.get('content', ''), unix_mode)
1219 1219
1220 1220 message = r_post.get('message') or c.default_message
1221 1221 filename = r_post.get('filename')
1222 1222 location = r_post.get('location', '') # dir location
1223 1223 file_obj = r_post.get('upload_file', None)
1224 1224
1225 1225 if file_obj is not None and hasattr(file_obj, 'filename'):
1226 1226 filename = r_post.get('filename_upload')
1227 1227 content = file_obj.file
1228 1228
1229 1229 if hasattr(content, 'file'):
1230 1230 # non posix systems store real file under file attr
1231 1231 content = content.file
1232 1232
1233 1233 if self.rhodecode_vcs_repo.is_empty:
1234 1234 default_redirect_url = h.route_path(
1235 1235 'repo_summary', repo_name=self.db_repo_name)
1236 1236 else:
1237 1237 default_redirect_url = h.route_path(
1238 1238 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1239 1239
1240 1240 # If there's no commit, redirect to repo summary
1241 1241 if type(c.commit) is EmptyCommit:
1242 1242 redirect_url = h.route_path(
1243 1243 'repo_summary', repo_name=self.db_repo_name)
1244 1244 else:
1245 1245 redirect_url = default_redirect_url
1246 1246
1247 1247 if not filename:
1248 1248 h.flash(_('No filename'), category='warning')
1249 1249 raise HTTPFound(redirect_url)
1250 1250
1251 1251 # extract the location from filename,
1252 1252 # allows using foo/bar.txt syntax to create subdirectories
1253 1253 subdir_loc = filename.rsplit('/', 1)
1254 1254 if len(subdir_loc) == 2:
1255 1255 location = os.path.join(location, subdir_loc[0])
1256 1256
1257 1257 # strip all crap out of file, just leave the basename
1258 1258 filename = os.path.basename(filename)
1259 1259 node_path = os.path.join(location, filename)
1260 1260 author = self._rhodecode_db_user.full_contact
1261 1261
1262 1262 try:
1263 1263 nodes = {
1264 1264 node_path: {
1265 1265 'content': content
1266 1266 }
1267 1267 }
1268 1268 ScmModel().create_nodes(
1269 1269 user=self._rhodecode_db_user.user_id,
1270 1270 repo=self.db_repo,
1271 1271 message=message,
1272 1272 nodes=nodes,
1273 1273 parent_commit=c.commit,
1274 1274 author=author,
1275 1275 )
1276 1276
1277 1277 h.flash(
1278 1278 _('Successfully committed new file `{}`').format(
1279 1279 h.escape(node_path)), category='success')
1280 1280 except NonRelativePathError:
1281 1281 log.exception('Non Relative path found')
1282 1282 h.flash(_(
1283 1283 'The location specified must be a relative path and must not '
1284 1284 'contain .. in the path'), category='warning')
1285 1285 raise HTTPFound(default_redirect_url)
1286 1286 except (NodeError, NodeAlreadyExistsError) as e:
1287 1287 h.flash(_(h.escape(e)), category='error')
1288 1288 except Exception:
1289 1289 log.exception('Error occurred during commit')
1290 1290 h.flash(_('Error occurred during commit'), category='error')
1291 1291
1292 1292 raise HTTPFound(default_redirect_url)
@@ -1,1242 +1,1242 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-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 import logging
22 22 import collections
23 23
24 24 import formencode
25 25 import formencode.htmlfill
26 26 import peppercorn
27 27 from pyramid.httpexceptions import (
28 28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
29 29 from pyramid.view import view_config
30 30 from pyramid.renderers import render
31 31
32 32 from rhodecode import events
33 33 from rhodecode.apps._base import RepoAppView, DataGridAppView
34 34
35 35 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
36 36 from rhodecode.lib.base import vcs_operation_context
37 37 from rhodecode.lib.ext_json import json
38 38 from rhodecode.lib.auth import (
39 39 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
40 40 NotAnonymous, CSRFRequired)
41 41 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
42 42 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
43 43 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
44 44 RepositoryRequirementError, NodeDoesNotExistError, EmptyRepositoryError)
45 45 from rhodecode.model.changeset_status import ChangesetStatusModel
46 46 from rhodecode.model.comment import CommentsModel
47 47 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
48 48 ChangesetComment, ChangesetStatus, Repository)
49 49 from rhodecode.model.forms import PullRequestForm
50 50 from rhodecode.model.meta import Session
51 51 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
52 52 from rhodecode.model.scm import ScmModel
53 53
54 54 log = logging.getLogger(__name__)
55 55
56 56
57 57 class RepoPullRequestsView(RepoAppView, DataGridAppView):
58 58
59 59 def load_default_context(self):
60 60 c = self._get_local_tmpl_context(include_app_defaults=True)
61 61 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
62 62 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
63 63
64 64 return c
65 65
66 66 def _get_pull_requests_list(
67 67 self, repo_name, source, filter_type, opened_by, statuses):
68 68
69 69 draw, start, limit = self._extract_chunk(self.request)
70 70 search_q, order_by, order_dir = self._extract_ordering(self.request)
71 71 _render = self.request.get_partial_renderer(
72 72 'rhodecode:templates/data_table/_dt_elements.mako')
73 73
74 74 # pagination
75 75
76 76 if filter_type == 'awaiting_review':
77 77 pull_requests = PullRequestModel().get_awaiting_review(
78 78 repo_name, source=source, opened_by=opened_by,
79 79 statuses=statuses, offset=start, length=limit,
80 80 order_by=order_by, order_dir=order_dir)
81 81 pull_requests_total_count = PullRequestModel().count_awaiting_review(
82 82 repo_name, source=source, statuses=statuses,
83 83 opened_by=opened_by)
84 84 elif filter_type == 'awaiting_my_review':
85 85 pull_requests = PullRequestModel().get_awaiting_my_review(
86 86 repo_name, source=source, opened_by=opened_by,
87 87 user_id=self._rhodecode_user.user_id, statuses=statuses,
88 88 offset=start, length=limit, order_by=order_by,
89 89 order_dir=order_dir)
90 90 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
91 91 repo_name, source=source, user_id=self._rhodecode_user.user_id,
92 92 statuses=statuses, opened_by=opened_by)
93 93 else:
94 94 pull_requests = PullRequestModel().get_all(
95 95 repo_name, source=source, opened_by=opened_by,
96 96 statuses=statuses, offset=start, length=limit,
97 97 order_by=order_by, order_dir=order_dir)
98 98 pull_requests_total_count = PullRequestModel().count_all(
99 99 repo_name, source=source, statuses=statuses,
100 100 opened_by=opened_by)
101 101
102 102 data = []
103 103 comments_model = CommentsModel()
104 104 for pr in pull_requests:
105 105 comments = comments_model.get_all_comments(
106 106 self.db_repo.repo_id, pull_request=pr)
107 107
108 108 data.append({
109 109 'name': _render('pullrequest_name',
110 110 pr.pull_request_id, pr.target_repo.repo_name),
111 111 'name_raw': pr.pull_request_id,
112 112 'status': _render('pullrequest_status',
113 113 pr.calculated_review_status()),
114 114 'title': _render(
115 115 'pullrequest_title', pr.title, pr.description),
116 116 'description': h.escape(pr.description),
117 117 'updated_on': _render('pullrequest_updated_on',
118 118 h.datetime_to_time(pr.updated_on)),
119 119 'updated_on_raw': h.datetime_to_time(pr.updated_on),
120 120 'created_on': _render('pullrequest_updated_on',
121 121 h.datetime_to_time(pr.created_on)),
122 122 'created_on_raw': h.datetime_to_time(pr.created_on),
123 123 'author': _render('pullrequest_author',
124 124 pr.author.full_contact, ),
125 125 'author_raw': pr.author.full_name,
126 126 'comments': _render('pullrequest_comments', len(comments)),
127 127 'comments_raw': len(comments),
128 128 'closed': pr.is_closed(),
129 129 })
130 130
131 131 data = ({
132 132 'draw': draw,
133 133 'data': data,
134 134 'recordsTotal': pull_requests_total_count,
135 135 'recordsFiltered': pull_requests_total_count,
136 136 })
137 137 return data
138 138
139 139 @LoginRequired()
140 140 @HasRepoPermissionAnyDecorator(
141 141 'repository.read', 'repository.write', 'repository.admin')
142 142 @view_config(
143 143 route_name='pullrequest_show_all', request_method='GET',
144 144 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
145 145 def pull_request_list(self):
146 146 c = self.load_default_context()
147 147
148 148 req_get = self.request.GET
149 149 c.source = str2bool(req_get.get('source'))
150 150 c.closed = str2bool(req_get.get('closed'))
151 151 c.my = str2bool(req_get.get('my'))
152 152 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
153 153 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
154 154
155 155 c.active = 'open'
156 156 if c.my:
157 157 c.active = 'my'
158 158 if c.closed:
159 159 c.active = 'closed'
160 160 if c.awaiting_review and not c.source:
161 161 c.active = 'awaiting'
162 162 if c.source and not c.awaiting_review:
163 163 c.active = 'source'
164 164 if c.awaiting_my_review:
165 165 c.active = 'awaiting_my'
166 166
167 167 return self._get_template_context(c)
168 168
169 169 @LoginRequired()
170 170 @HasRepoPermissionAnyDecorator(
171 171 'repository.read', 'repository.write', 'repository.admin')
172 172 @view_config(
173 173 route_name='pullrequest_show_all_data', request_method='GET',
174 174 renderer='json_ext', xhr=True)
175 175 def pull_request_list_data(self):
176 176 self.load_default_context()
177 177
178 178 # additional filters
179 179 req_get = self.request.GET
180 180 source = str2bool(req_get.get('source'))
181 181 closed = str2bool(req_get.get('closed'))
182 182 my = str2bool(req_get.get('my'))
183 183 awaiting_review = str2bool(req_get.get('awaiting_review'))
184 184 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
185 185
186 186 filter_type = 'awaiting_review' if awaiting_review \
187 187 else 'awaiting_my_review' if awaiting_my_review \
188 188 else None
189 189
190 190 opened_by = None
191 191 if my:
192 192 opened_by = [self._rhodecode_user.user_id]
193 193
194 194 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
195 195 if closed:
196 196 statuses = [PullRequest.STATUS_CLOSED]
197 197
198 198 data = self._get_pull_requests_list(
199 199 repo_name=self.db_repo_name, source=source,
200 200 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
201 201
202 202 return data
203 203
204 204 def _get_diffset(self, source_repo_name, source_repo,
205 205 source_ref_id, target_ref_id,
206 206 target_commit, source_commit, diff_limit, fulldiff,
207 207 file_limit, display_inline_comments):
208 208
209 209 vcs_diff = PullRequestModel().get_diff(
210 210 source_repo, source_ref_id, target_ref_id)
211 211
212 212 diff_processor = diffs.DiffProcessor(
213 213 vcs_diff, format='newdiff', diff_limit=diff_limit,
214 214 file_limit=file_limit, show_full_diff=fulldiff)
215 215
216 216 _parsed = diff_processor.prepare()
217 217
218 218 def _node_getter(commit):
219 219 def get_node(fname):
220 220 try:
221 221 return commit.get_node(fname)
222 222 except NodeDoesNotExistError:
223 223 return None
224 224
225 225 return get_node
226 226
227 227 diffset = codeblocks.DiffSet(
228 228 repo_name=self.db_repo_name,
229 229 source_repo_name=source_repo_name,
230 230 source_node_getter=_node_getter(target_commit),
231 231 target_node_getter=_node_getter(source_commit),
232 232 comments=display_inline_comments
233 233 )
234 diffset = diffset.render_patchset(
235 _parsed, target_commit.raw_id, source_commit.raw_id)
234 diffset = self.path_filter.render_patchset_filtered(
235 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
236 236
237 237 return diffset
238 238
239 239 @LoginRequired()
240 240 @HasRepoPermissionAnyDecorator(
241 241 'repository.read', 'repository.write', 'repository.admin')
242 242 @view_config(
243 243 route_name='pullrequest_show', request_method='GET',
244 244 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
245 245 def pull_request_show(self):
246 246 pull_request_id = self.request.matchdict['pull_request_id']
247 247
248 248 c = self.load_default_context()
249 249
250 250 version = self.request.GET.get('version')
251 251 from_version = self.request.GET.get('from_version') or version
252 252 merge_checks = self.request.GET.get('merge_checks')
253 253 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
254 254
255 255 (pull_request_latest,
256 256 pull_request_at_ver,
257 257 pull_request_display_obj,
258 258 at_version) = PullRequestModel().get_pr_version(
259 259 pull_request_id, version=version)
260 260 pr_closed = pull_request_latest.is_closed()
261 261
262 262 if pr_closed and (version or from_version):
263 263 # not allow to browse versions
264 264 raise HTTPFound(h.route_path(
265 265 'pullrequest_show', repo_name=self.db_repo_name,
266 266 pull_request_id=pull_request_id))
267 267
268 268 versions = pull_request_display_obj.versions()
269 269
270 270 c.at_version = at_version
271 271 c.at_version_num = (at_version
272 272 if at_version and at_version != 'latest'
273 273 else None)
274 274 c.at_version_pos = ChangesetComment.get_index_from_version(
275 275 c.at_version_num, versions)
276 276
277 277 (prev_pull_request_latest,
278 278 prev_pull_request_at_ver,
279 279 prev_pull_request_display_obj,
280 280 prev_at_version) = PullRequestModel().get_pr_version(
281 281 pull_request_id, version=from_version)
282 282
283 283 c.from_version = prev_at_version
284 284 c.from_version_num = (prev_at_version
285 285 if prev_at_version and prev_at_version != 'latest'
286 286 else None)
287 287 c.from_version_pos = ChangesetComment.get_index_from_version(
288 288 c.from_version_num, versions)
289 289
290 290 # define if we're in COMPARE mode or VIEW at version mode
291 291 compare = at_version != prev_at_version
292 292
293 293 # pull_requests repo_name we opened it against
294 294 # ie. target_repo must match
295 295 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
296 296 raise HTTPNotFound()
297 297
298 298 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
299 299 pull_request_at_ver)
300 300
301 301 c.pull_request = pull_request_display_obj
302 302 c.pull_request_latest = pull_request_latest
303 303
304 304 if compare or (at_version and not at_version == 'latest'):
305 305 c.allowed_to_change_status = False
306 306 c.allowed_to_update = False
307 307 c.allowed_to_merge = False
308 308 c.allowed_to_delete = False
309 309 c.allowed_to_comment = False
310 310 c.allowed_to_close = False
311 311 else:
312 312 can_change_status = PullRequestModel().check_user_change_status(
313 313 pull_request_at_ver, self._rhodecode_user)
314 314 c.allowed_to_change_status = can_change_status and not pr_closed
315 315
316 316 c.allowed_to_update = PullRequestModel().check_user_update(
317 317 pull_request_latest, self._rhodecode_user) and not pr_closed
318 318 c.allowed_to_merge = PullRequestModel().check_user_merge(
319 319 pull_request_latest, self._rhodecode_user) and not pr_closed
320 320 c.allowed_to_delete = PullRequestModel().check_user_delete(
321 321 pull_request_latest, self._rhodecode_user) and not pr_closed
322 322 c.allowed_to_comment = not pr_closed
323 323 c.allowed_to_close = c.allowed_to_merge and not pr_closed
324 324
325 325 c.forbid_adding_reviewers = False
326 326 c.forbid_author_to_review = False
327 327 c.forbid_commit_author_to_review = False
328 328
329 329 if pull_request_latest.reviewer_data and \
330 330 'rules' in pull_request_latest.reviewer_data:
331 331 rules = pull_request_latest.reviewer_data['rules'] or {}
332 332 try:
333 333 c.forbid_adding_reviewers = rules.get(
334 334 'forbid_adding_reviewers')
335 335 c.forbid_author_to_review = rules.get(
336 336 'forbid_author_to_review')
337 337 c.forbid_commit_author_to_review = rules.get(
338 338 'forbid_commit_author_to_review')
339 339 except Exception:
340 340 pass
341 341
342 342 # check merge capabilities
343 343 _merge_check = MergeCheck.validate(
344 344 pull_request_latest, user=self._rhodecode_user,
345 345 translator=self.request.translate)
346 346 c.pr_merge_errors = _merge_check.error_details
347 347 c.pr_merge_possible = not _merge_check.failed
348 348 c.pr_merge_message = _merge_check.merge_msg
349 349
350 350 c.pr_merge_info = MergeCheck.get_merge_conditions(
351 351 pull_request_latest, translator=self.request.translate)
352 352
353 353 c.pull_request_review_status = _merge_check.review_status
354 354 if merge_checks:
355 355 self.request.override_renderer = \
356 356 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
357 357 return self._get_template_context(c)
358 358
359 359 comments_model = CommentsModel()
360 360
361 361 # reviewers and statuses
362 362 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
363 363 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
364 364
365 365 # GENERAL COMMENTS with versions #
366 366 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
367 367 q = q.order_by(ChangesetComment.comment_id.asc())
368 368 general_comments = q
369 369
370 370 # pick comments we want to render at current version
371 371 c.comment_versions = comments_model.aggregate_comments(
372 372 general_comments, versions, c.at_version_num)
373 373 c.comments = c.comment_versions[c.at_version_num]['until']
374 374
375 375 # INLINE COMMENTS with versions #
376 376 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
377 377 q = q.order_by(ChangesetComment.comment_id.asc())
378 378 inline_comments = q
379 379
380 380 c.inline_versions = comments_model.aggregate_comments(
381 381 inline_comments, versions, c.at_version_num, inline=True)
382 382
383 383 # inject latest version
384 384 latest_ver = PullRequest.get_pr_display_object(
385 385 pull_request_latest, pull_request_latest)
386 386
387 387 c.versions = versions + [latest_ver]
388 388
389 389 # if we use version, then do not show later comments
390 390 # than current version
391 391 display_inline_comments = collections.defaultdict(
392 392 lambda: collections.defaultdict(list))
393 393 for co in inline_comments:
394 394 if c.at_version_num:
395 395 # pick comments that are at least UPTO given version, so we
396 396 # don't render comments for higher version
397 397 should_render = co.pull_request_version_id and \
398 398 co.pull_request_version_id <= c.at_version_num
399 399 else:
400 400 # showing all, for 'latest'
401 401 should_render = True
402 402
403 403 if should_render:
404 404 display_inline_comments[co.f_path][co.line_no].append(co)
405 405
406 406 # load diff data into template context, if we use compare mode then
407 407 # diff is calculated based on changes between versions of PR
408 408
409 409 source_repo = pull_request_at_ver.source_repo
410 410 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
411 411
412 412 target_repo = pull_request_at_ver.target_repo
413 413 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
414 414
415 415 if compare:
416 416 # in compare switch the diff base to latest commit from prev version
417 417 target_ref_id = prev_pull_request_display_obj.revisions[0]
418 418
419 419 # despite opening commits for bookmarks/branches/tags, we always
420 420 # convert this to rev to prevent changes after bookmark or branch change
421 421 c.source_ref_type = 'rev'
422 422 c.source_ref = source_ref_id
423 423
424 424 c.target_ref_type = 'rev'
425 425 c.target_ref = target_ref_id
426 426
427 427 c.source_repo = source_repo
428 428 c.target_repo = target_repo
429 429
430 430 c.commit_ranges = []
431 431 source_commit = EmptyCommit()
432 432 target_commit = EmptyCommit()
433 433 c.missing_requirements = False
434 434
435 435 source_scm = source_repo.scm_instance()
436 436 target_scm = target_repo.scm_instance()
437 437
438 438 # try first shadow repo, fallback to regular repo
439 439 try:
440 440 commits_source_repo = pull_request_latest.get_shadow_repo()
441 441 except Exception:
442 442 log.debug('Failed to get shadow repo', exc_info=True)
443 443 commits_source_repo = source_scm
444 444
445 445 c.commits_source_repo = commits_source_repo
446 446 commit_cache = {}
447 447 try:
448 448 pre_load = ["author", "branch", "date", "message"]
449 449 show_revs = pull_request_at_ver.revisions
450 450 for rev in show_revs:
451 451 comm = commits_source_repo.get_commit(
452 452 commit_id=rev, pre_load=pre_load)
453 453 c.commit_ranges.append(comm)
454 454 commit_cache[comm.raw_id] = comm
455 455
456 456 # Order here matters, we first need to get target, and then
457 457 # the source
458 458 target_commit = commits_source_repo.get_commit(
459 459 commit_id=safe_str(target_ref_id))
460 460
461 461 source_commit = commits_source_repo.get_commit(
462 462 commit_id=safe_str(source_ref_id))
463 463
464 464 except CommitDoesNotExistError:
465 465 log.warning(
466 466 'Failed to get commit from `{}` repo'.format(
467 467 commits_source_repo), exc_info=True)
468 468 except RepositoryRequirementError:
469 469 log.warning(
470 470 'Failed to get all required data from repo', exc_info=True)
471 471 c.missing_requirements = True
472 472
473 473 c.ancestor = None # set it to None, to hide it from PR view
474 474
475 475 try:
476 476 ancestor_id = source_scm.get_common_ancestor(
477 477 source_commit.raw_id, target_commit.raw_id, target_scm)
478 478 c.ancestor_commit = source_scm.get_commit(ancestor_id)
479 479 except Exception:
480 480 c.ancestor_commit = None
481 481
482 482 c.statuses = source_repo.statuses(
483 483 [x.raw_id for x in c.commit_ranges])
484 484
485 485 # auto collapse if we have more than limit
486 486 collapse_limit = diffs.DiffProcessor._collapse_commits_over
487 487 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
488 488 c.compare_mode = compare
489 489
490 490 # diff_limit is the old behavior, will cut off the whole diff
491 491 # if the limit is applied otherwise will just hide the
492 492 # big files from the front-end
493 493 diff_limit = c.visual.cut_off_limit_diff
494 494 file_limit = c.visual.cut_off_limit_file
495 495
496 496 c.missing_commits = False
497 497 if (c.missing_requirements
498 498 or isinstance(source_commit, EmptyCommit)
499 499 or source_commit == target_commit):
500 500
501 501 c.missing_commits = True
502 502 else:
503 503
504 504 c.diffset = self._get_diffset(
505 505 c.source_repo.repo_name, commits_source_repo,
506 506 source_ref_id, target_ref_id,
507 507 target_commit, source_commit,
508 508 diff_limit, c.fulldiff, file_limit, display_inline_comments)
509 509
510 510 c.limited_diff = c.diffset.limited_diff
511 511
512 512 # calculate removed files that are bound to comments
513 513 comment_deleted_files = [
514 514 fname for fname in display_inline_comments
515 515 if fname not in c.diffset.file_stats]
516 516
517 517 c.deleted_files_comments = collections.defaultdict(dict)
518 518 for fname, per_line_comments in display_inline_comments.items():
519 519 if fname in comment_deleted_files:
520 520 c.deleted_files_comments[fname]['stats'] = 0
521 521 c.deleted_files_comments[fname]['comments'] = list()
522 522 for lno, comments in per_line_comments.items():
523 523 c.deleted_files_comments[fname]['comments'].extend(
524 524 comments)
525 525
526 526 # this is a hack to properly display links, when creating PR, the
527 527 # compare view and others uses different notation, and
528 528 # compare_commits.mako renders links based on the target_repo.
529 529 # We need to swap that here to generate it properly on the html side
530 530 c.target_repo = c.source_repo
531 531
532 532 c.commit_statuses = ChangesetStatus.STATUSES
533 533
534 534 c.show_version_changes = not pr_closed
535 535 if c.show_version_changes:
536 536 cur_obj = pull_request_at_ver
537 537 prev_obj = prev_pull_request_at_ver
538 538
539 539 old_commit_ids = prev_obj.revisions
540 540 new_commit_ids = cur_obj.revisions
541 541 commit_changes = PullRequestModel()._calculate_commit_id_changes(
542 542 old_commit_ids, new_commit_ids)
543 543 c.commit_changes_summary = commit_changes
544 544
545 545 # calculate the diff for commits between versions
546 546 c.commit_changes = []
547 547 mark = lambda cs, fw: list(
548 548 h.itertools.izip_longest([], cs, fillvalue=fw))
549 549 for c_type, raw_id in mark(commit_changes.added, 'a') \
550 550 + mark(commit_changes.removed, 'r') \
551 551 + mark(commit_changes.common, 'c'):
552 552
553 553 if raw_id in commit_cache:
554 554 commit = commit_cache[raw_id]
555 555 else:
556 556 try:
557 557 commit = commits_source_repo.get_commit(raw_id)
558 558 except CommitDoesNotExistError:
559 559 # in case we fail extracting still use "dummy" commit
560 560 # for display in commit diff
561 561 commit = h.AttributeDict(
562 562 {'raw_id': raw_id,
563 563 'message': 'EMPTY or MISSING COMMIT'})
564 564 c.commit_changes.append([c_type, commit])
565 565
566 566 # current user review statuses for each version
567 567 c.review_versions = {}
568 568 if self._rhodecode_user.user_id in allowed_reviewers:
569 569 for co in general_comments:
570 570 if co.author.user_id == self._rhodecode_user.user_id:
571 571 # each comment has a status change
572 572 status = co.status_change
573 573 if status:
574 574 _ver_pr = status[0].comment.pull_request_version_id
575 575 c.review_versions[_ver_pr] = status[0]
576 576
577 577 return self._get_template_context(c)
578 578
579 579 def assure_not_empty_repo(self):
580 580 _ = self.request.translate
581 581
582 582 try:
583 583 self.db_repo.scm_instance().get_commit()
584 584 except EmptyRepositoryError:
585 585 h.flash(h.literal(_('There are no commits yet')),
586 586 category='warning')
587 587 raise HTTPFound(
588 588 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
589 589
590 590 @LoginRequired()
591 591 @NotAnonymous()
592 592 @HasRepoPermissionAnyDecorator(
593 593 'repository.read', 'repository.write', 'repository.admin')
594 594 @view_config(
595 595 route_name='pullrequest_new', request_method='GET',
596 596 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
597 597 def pull_request_new(self):
598 598 _ = self.request.translate
599 599 c = self.load_default_context()
600 600
601 601 self.assure_not_empty_repo()
602 602 source_repo = self.db_repo
603 603
604 604 commit_id = self.request.GET.get('commit')
605 605 branch_ref = self.request.GET.get('branch')
606 606 bookmark_ref = self.request.GET.get('bookmark')
607 607
608 608 try:
609 609 source_repo_data = PullRequestModel().generate_repo_data(
610 610 source_repo, commit_id=commit_id,
611 611 branch=branch_ref, bookmark=bookmark_ref,
612 612 translator=self.request.translate)
613 613 except CommitDoesNotExistError as e:
614 614 log.exception(e)
615 615 h.flash(_('Commit does not exist'), 'error')
616 616 raise HTTPFound(
617 617 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
618 618
619 619 default_target_repo = source_repo
620 620
621 621 if source_repo.parent:
622 622 parent_vcs_obj = source_repo.parent.scm_instance()
623 623 if parent_vcs_obj and not parent_vcs_obj.is_empty():
624 624 # change default if we have a parent repo
625 625 default_target_repo = source_repo.parent
626 626
627 627 target_repo_data = PullRequestModel().generate_repo_data(
628 628 default_target_repo, translator=self.request.translate)
629 629
630 630 selected_source_ref = source_repo_data['refs']['selected_ref']
631 631 title_source_ref = ''
632 632 if selected_source_ref:
633 633 title_source_ref = selected_source_ref.split(':', 2)[1]
634 634 c.default_title = PullRequestModel().generate_pullrequest_title(
635 635 source=source_repo.repo_name,
636 636 source_ref=title_source_ref,
637 637 target=default_target_repo.repo_name
638 638 )
639 639
640 640 c.default_repo_data = {
641 641 'source_repo_name': source_repo.repo_name,
642 642 'source_refs_json': json.dumps(source_repo_data),
643 643 'target_repo_name': default_target_repo.repo_name,
644 644 'target_refs_json': json.dumps(target_repo_data),
645 645 }
646 646 c.default_source_ref = selected_source_ref
647 647
648 648 return self._get_template_context(c)
649 649
650 650 @LoginRequired()
651 651 @NotAnonymous()
652 652 @HasRepoPermissionAnyDecorator(
653 653 'repository.read', 'repository.write', 'repository.admin')
654 654 @view_config(
655 655 route_name='pullrequest_repo_refs', request_method='GET',
656 656 renderer='json_ext', xhr=True)
657 657 def pull_request_repo_refs(self):
658 658 self.load_default_context()
659 659 target_repo_name = self.request.matchdict['target_repo_name']
660 660 repo = Repository.get_by_repo_name(target_repo_name)
661 661 if not repo:
662 662 raise HTTPNotFound()
663 663
664 664 target_perm = HasRepoPermissionAny(
665 665 'repository.read', 'repository.write', 'repository.admin')(
666 666 target_repo_name)
667 667 if not target_perm:
668 668 raise HTTPNotFound()
669 669
670 670 return PullRequestModel().generate_repo_data(
671 671 repo, translator=self.request.translate)
672 672
673 673 @LoginRequired()
674 674 @NotAnonymous()
675 675 @HasRepoPermissionAnyDecorator(
676 676 'repository.read', 'repository.write', 'repository.admin')
677 677 @view_config(
678 678 route_name='pullrequest_repo_destinations', request_method='GET',
679 679 renderer='json_ext', xhr=True)
680 680 def pull_request_repo_destinations(self):
681 681 _ = self.request.translate
682 682 filter_query = self.request.GET.get('query')
683 683
684 684 query = Repository.query() \
685 685 .order_by(func.length(Repository.repo_name)) \
686 686 .filter(
687 687 or_(Repository.repo_name == self.db_repo.repo_name,
688 688 Repository.fork_id == self.db_repo.repo_id))
689 689
690 690 if filter_query:
691 691 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
692 692 query = query.filter(
693 693 Repository.repo_name.ilike(ilike_expression))
694 694
695 695 add_parent = False
696 696 if self.db_repo.parent:
697 697 if filter_query in self.db_repo.parent.repo_name:
698 698 parent_vcs_obj = self.db_repo.parent.scm_instance()
699 699 if parent_vcs_obj and not parent_vcs_obj.is_empty():
700 700 add_parent = True
701 701
702 702 limit = 20 - 1 if add_parent else 20
703 703 all_repos = query.limit(limit).all()
704 704 if add_parent:
705 705 all_repos += [self.db_repo.parent]
706 706
707 707 repos = []
708 708 for obj in ScmModel().get_repos(all_repos):
709 709 repos.append({
710 710 'id': obj['name'],
711 711 'text': obj['name'],
712 712 'type': 'repo',
713 713 'obj': obj['dbrepo']
714 714 })
715 715
716 716 data = {
717 717 'more': False,
718 718 'results': [{
719 719 'text': _('Repositories'),
720 720 'children': repos
721 721 }] if repos else []
722 722 }
723 723 return data
724 724
725 725 @LoginRequired()
726 726 @NotAnonymous()
727 727 @HasRepoPermissionAnyDecorator(
728 728 'repository.read', 'repository.write', 'repository.admin')
729 729 @CSRFRequired()
730 730 @view_config(
731 731 route_name='pullrequest_create', request_method='POST',
732 732 renderer=None)
733 733 def pull_request_create(self):
734 734 _ = self.request.translate
735 735 self.assure_not_empty_repo()
736 736 self.load_default_context()
737 737
738 738 controls = peppercorn.parse(self.request.POST.items())
739 739
740 740 try:
741 741 form = PullRequestForm(
742 742 self.request.translate, self.db_repo.repo_id)()
743 743 _form = form.to_python(controls)
744 744 except formencode.Invalid as errors:
745 745 if errors.error_dict.get('revisions'):
746 746 msg = 'Revisions: %s' % errors.error_dict['revisions']
747 747 elif errors.error_dict.get('pullrequest_title'):
748 748 msg = errors.error_dict.get('pullrequest_title')
749 749 else:
750 750 msg = _('Error creating pull request: {}').format(errors)
751 751 log.exception(msg)
752 752 h.flash(msg, 'error')
753 753
754 754 # would rather just go back to form ...
755 755 raise HTTPFound(
756 756 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
757 757
758 758 source_repo = _form['source_repo']
759 759 source_ref = _form['source_ref']
760 760 target_repo = _form['target_repo']
761 761 target_ref = _form['target_ref']
762 762 commit_ids = _form['revisions'][::-1]
763 763
764 764 # find the ancestor for this pr
765 765 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
766 766 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
767 767
768 768 # re-check permissions again here
769 769 # source_repo we must have read permissions
770 770
771 771 source_perm = HasRepoPermissionAny(
772 772 'repository.read',
773 773 'repository.write', 'repository.admin')(source_db_repo.repo_name)
774 774 if not source_perm:
775 775 msg = _('Not Enough permissions to source repo `{}`.'.format(
776 776 source_db_repo.repo_name))
777 777 h.flash(msg, category='error')
778 778 # copy the args back to redirect
779 779 org_query = self.request.GET.mixed()
780 780 raise HTTPFound(
781 781 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
782 782 _query=org_query))
783 783
784 784 # target repo we must have read permissions, and also later on
785 785 # we want to check branch permissions here
786 786 target_perm = HasRepoPermissionAny(
787 787 'repository.read',
788 788 'repository.write', 'repository.admin')(target_db_repo.repo_name)
789 789 if not target_perm:
790 790 msg = _('Not Enough permissions to target repo `{}`.'.format(
791 791 target_db_repo.repo_name))
792 792 h.flash(msg, category='error')
793 793 # copy the args back to redirect
794 794 org_query = self.request.GET.mixed()
795 795 raise HTTPFound(
796 796 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
797 797 _query=org_query))
798 798
799 799 source_scm = source_db_repo.scm_instance()
800 800 target_scm = target_db_repo.scm_instance()
801 801
802 802 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
803 803 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
804 804
805 805 ancestor = source_scm.get_common_ancestor(
806 806 source_commit.raw_id, target_commit.raw_id, target_scm)
807 807
808 808 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
809 809 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
810 810
811 811 pullrequest_title = _form['pullrequest_title']
812 812 title_source_ref = source_ref.split(':', 2)[1]
813 813 if not pullrequest_title:
814 814 pullrequest_title = PullRequestModel().generate_pullrequest_title(
815 815 source=source_repo,
816 816 source_ref=title_source_ref,
817 817 target=target_repo
818 818 )
819 819
820 820 description = _form['pullrequest_desc']
821 821
822 822 get_default_reviewers_data, validate_default_reviewers = \
823 823 PullRequestModel().get_reviewer_functions()
824 824
825 825 # recalculate reviewers logic, to make sure we can validate this
826 826 reviewer_rules = get_default_reviewers_data(
827 827 self._rhodecode_db_user, source_db_repo,
828 828 source_commit, target_db_repo, target_commit)
829 829
830 830 given_reviewers = _form['review_members']
831 831 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
832 832
833 833 try:
834 834 pull_request = PullRequestModel().create(
835 835 self._rhodecode_user.user_id, source_repo, source_ref,
836 836 target_repo, target_ref, commit_ids, reviewers,
837 837 pullrequest_title, description, reviewer_rules
838 838 )
839 839 Session().commit()
840 840
841 841 h.flash(_('Successfully opened new pull request'),
842 842 category='success')
843 843 except Exception:
844 844 msg = _('Error occurred during creation of this pull request.')
845 845 log.exception(msg)
846 846 h.flash(msg, category='error')
847 847
848 848 # copy the args back to redirect
849 849 org_query = self.request.GET.mixed()
850 850 raise HTTPFound(
851 851 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
852 852 _query=org_query))
853 853
854 854 raise HTTPFound(
855 855 h.route_path('pullrequest_show', repo_name=target_repo,
856 856 pull_request_id=pull_request.pull_request_id))
857 857
858 858 @LoginRequired()
859 859 @NotAnonymous()
860 860 @HasRepoPermissionAnyDecorator(
861 861 'repository.read', 'repository.write', 'repository.admin')
862 862 @CSRFRequired()
863 863 @view_config(
864 864 route_name='pullrequest_update', request_method='POST',
865 865 renderer='json_ext')
866 866 def pull_request_update(self):
867 867 pull_request = PullRequest.get_or_404(
868 868 self.request.matchdict['pull_request_id'])
869 869 _ = self.request.translate
870 870
871 871 self.load_default_context()
872 872
873 873 if pull_request.is_closed():
874 874 log.debug('update: forbidden because pull request is closed')
875 875 msg = _(u'Cannot update closed pull requests.')
876 876 h.flash(msg, category='error')
877 877 return True
878 878
879 879 # only owner or admin can update it
880 880 allowed_to_update = PullRequestModel().check_user_update(
881 881 pull_request, self._rhodecode_user)
882 882 if allowed_to_update:
883 883 controls = peppercorn.parse(self.request.POST.items())
884 884
885 885 if 'review_members' in controls:
886 886 self._update_reviewers(
887 887 pull_request, controls['review_members'],
888 888 pull_request.reviewer_data)
889 889 elif str2bool(self.request.POST.get('update_commits', 'false')):
890 890 self._update_commits(pull_request)
891 891 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
892 892 self._edit_pull_request(pull_request)
893 893 else:
894 894 raise HTTPBadRequest()
895 895 return True
896 896 raise HTTPForbidden()
897 897
898 898 def _edit_pull_request(self, pull_request):
899 899 _ = self.request.translate
900 900 try:
901 901 PullRequestModel().edit(
902 902 pull_request, self.request.POST.get('title'),
903 903 self.request.POST.get('description'), self._rhodecode_user)
904 904 except ValueError:
905 905 msg = _(u'Cannot update closed pull requests.')
906 906 h.flash(msg, category='error')
907 907 return
908 908 else:
909 909 Session().commit()
910 910
911 911 msg = _(u'Pull request title & description updated.')
912 912 h.flash(msg, category='success')
913 913 return
914 914
915 915 def _update_commits(self, pull_request):
916 916 _ = self.request.translate
917 917 resp = PullRequestModel().update_commits(pull_request)
918 918
919 919 if resp.executed:
920 920
921 921 if resp.target_changed and resp.source_changed:
922 922 changed = 'target and source repositories'
923 923 elif resp.target_changed and not resp.source_changed:
924 924 changed = 'target repository'
925 925 elif not resp.target_changed and resp.source_changed:
926 926 changed = 'source repository'
927 927 else:
928 928 changed = 'nothing'
929 929
930 930 msg = _(
931 931 u'Pull request updated to "{source_commit_id}" with '
932 932 u'{count_added} added, {count_removed} removed commits. '
933 933 u'Source of changes: {change_source}')
934 934 msg = msg.format(
935 935 source_commit_id=pull_request.source_ref_parts.commit_id,
936 936 count_added=len(resp.changes.added),
937 937 count_removed=len(resp.changes.removed),
938 938 change_source=changed)
939 939 h.flash(msg, category='success')
940 940
941 941 channel = '/repo${}$/pr/{}'.format(
942 942 pull_request.target_repo.repo_name,
943 943 pull_request.pull_request_id)
944 944 message = msg + (
945 945 ' - <a onclick="window.location.reload()">'
946 946 '<strong>{}</strong></a>'.format(_('Reload page')))
947 947 channelstream.post_message(
948 948 channel, message, self._rhodecode_user.username,
949 949 registry=self.request.registry)
950 950 else:
951 951 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
952 952 warning_reasons = [
953 953 UpdateFailureReason.NO_CHANGE,
954 954 UpdateFailureReason.WRONG_REF_TYPE,
955 955 ]
956 956 category = 'warning' if resp.reason in warning_reasons else 'error'
957 957 h.flash(msg, category=category)
958 958
959 959 @LoginRequired()
960 960 @NotAnonymous()
961 961 @HasRepoPermissionAnyDecorator(
962 962 'repository.read', 'repository.write', 'repository.admin')
963 963 @CSRFRequired()
964 964 @view_config(
965 965 route_name='pullrequest_merge', request_method='POST',
966 966 renderer='json_ext')
967 967 def pull_request_merge(self):
968 968 """
969 969 Merge will perform a server-side merge of the specified
970 970 pull request, if the pull request is approved and mergeable.
971 971 After successful merging, the pull request is automatically
972 972 closed, with a relevant comment.
973 973 """
974 974 pull_request = PullRequest.get_or_404(
975 975 self.request.matchdict['pull_request_id'])
976 976
977 977 self.load_default_context()
978 978 check = MergeCheck.validate(pull_request, self._rhodecode_db_user,
979 979 translator=self.request.translate)
980 980 merge_possible = not check.failed
981 981
982 982 for err_type, error_msg in check.errors:
983 983 h.flash(error_msg, category=err_type)
984 984
985 985 if merge_possible:
986 986 log.debug("Pre-conditions checked, trying to merge.")
987 987 extras = vcs_operation_context(
988 988 self.request.environ, repo_name=pull_request.target_repo.repo_name,
989 989 username=self._rhodecode_db_user.username, action='push',
990 990 scm=pull_request.target_repo.repo_type)
991 991 self._merge_pull_request(
992 992 pull_request, self._rhodecode_db_user, extras)
993 993 else:
994 994 log.debug("Pre-conditions failed, NOT merging.")
995 995
996 996 raise HTTPFound(
997 997 h.route_path('pullrequest_show',
998 998 repo_name=pull_request.target_repo.repo_name,
999 999 pull_request_id=pull_request.pull_request_id))
1000 1000
1001 1001 def _merge_pull_request(self, pull_request, user, extras):
1002 1002 _ = self.request.translate
1003 1003 merge_resp = PullRequestModel().merge(pull_request, user, extras=extras)
1004 1004
1005 1005 if merge_resp.executed:
1006 1006 log.debug("The merge was successful, closing the pull request.")
1007 1007 PullRequestModel().close_pull_request(
1008 1008 pull_request.pull_request_id, user)
1009 1009 Session().commit()
1010 1010 msg = _('Pull request was successfully merged and closed.')
1011 1011 h.flash(msg, category='success')
1012 1012 else:
1013 1013 log.debug(
1014 1014 "The merge was not successful. Merge response: %s",
1015 1015 merge_resp)
1016 1016 msg = PullRequestModel().merge_status_message(
1017 1017 merge_resp.failure_reason)
1018 1018 h.flash(msg, category='error')
1019 1019
1020 1020 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1021 1021 _ = self.request.translate
1022 1022 get_default_reviewers_data, validate_default_reviewers = \
1023 1023 PullRequestModel().get_reviewer_functions()
1024 1024
1025 1025 try:
1026 1026 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1027 1027 except ValueError as e:
1028 1028 log.error('Reviewers Validation: {}'.format(e))
1029 1029 h.flash(e, category='error')
1030 1030 return
1031 1031
1032 1032 PullRequestModel().update_reviewers(
1033 1033 pull_request, reviewers, self._rhodecode_user)
1034 1034 h.flash(_('Pull request reviewers updated.'), category='success')
1035 1035 Session().commit()
1036 1036
1037 1037 @LoginRequired()
1038 1038 @NotAnonymous()
1039 1039 @HasRepoPermissionAnyDecorator(
1040 1040 'repository.read', 'repository.write', 'repository.admin')
1041 1041 @CSRFRequired()
1042 1042 @view_config(
1043 1043 route_name='pullrequest_delete', request_method='POST',
1044 1044 renderer='json_ext')
1045 1045 def pull_request_delete(self):
1046 1046 _ = self.request.translate
1047 1047
1048 1048 pull_request = PullRequest.get_or_404(
1049 1049 self.request.matchdict['pull_request_id'])
1050 1050 self.load_default_context()
1051 1051
1052 1052 pr_closed = pull_request.is_closed()
1053 1053 allowed_to_delete = PullRequestModel().check_user_delete(
1054 1054 pull_request, self._rhodecode_user) and not pr_closed
1055 1055
1056 1056 # only owner can delete it !
1057 1057 if allowed_to_delete:
1058 1058 PullRequestModel().delete(pull_request, self._rhodecode_user)
1059 1059 Session().commit()
1060 1060 h.flash(_('Successfully deleted pull request'),
1061 1061 category='success')
1062 1062 raise HTTPFound(h.route_path('pullrequest_show_all',
1063 1063 repo_name=self.db_repo_name))
1064 1064
1065 1065 log.warning('user %s tried to delete pull request without access',
1066 1066 self._rhodecode_user)
1067 1067 raise HTTPNotFound()
1068 1068
1069 1069 @LoginRequired()
1070 1070 @NotAnonymous()
1071 1071 @HasRepoPermissionAnyDecorator(
1072 1072 'repository.read', 'repository.write', 'repository.admin')
1073 1073 @CSRFRequired()
1074 1074 @view_config(
1075 1075 route_name='pullrequest_comment_create', request_method='POST',
1076 1076 renderer='json_ext')
1077 1077 def pull_request_comment_create(self):
1078 1078 _ = self.request.translate
1079 1079
1080 1080 pull_request = PullRequest.get_or_404(
1081 1081 self.request.matchdict['pull_request_id'])
1082 1082 pull_request_id = pull_request.pull_request_id
1083 1083
1084 1084 if pull_request.is_closed():
1085 1085 log.debug('comment: forbidden because pull request is closed')
1086 1086 raise HTTPForbidden()
1087 1087
1088 1088 allowed_to_comment = PullRequestModel().check_user_comment(
1089 1089 pull_request, self._rhodecode_user)
1090 1090 if not allowed_to_comment:
1091 1091 log.debug(
1092 1092 'comment: forbidden because pull request is from forbidden repo')
1093 1093 raise HTTPForbidden()
1094 1094
1095 1095 c = self.load_default_context()
1096 1096
1097 1097 status = self.request.POST.get('changeset_status', None)
1098 1098 text = self.request.POST.get('text')
1099 1099 comment_type = self.request.POST.get('comment_type')
1100 1100 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1101 1101 close_pull_request = self.request.POST.get('close_pull_request')
1102 1102
1103 1103 # the logic here should work like following, if we submit close
1104 1104 # pr comment, use `close_pull_request_with_comment` function
1105 1105 # else handle regular comment logic
1106 1106
1107 1107 if close_pull_request:
1108 1108 # only owner or admin or person with write permissions
1109 1109 allowed_to_close = PullRequestModel().check_user_update(
1110 1110 pull_request, self._rhodecode_user)
1111 1111 if not allowed_to_close:
1112 1112 log.debug('comment: forbidden because not allowed to close '
1113 1113 'pull request %s', pull_request_id)
1114 1114 raise HTTPForbidden()
1115 1115 comment, status = PullRequestModel().close_pull_request_with_comment(
1116 1116 pull_request, self._rhodecode_user, self.db_repo, message=text)
1117 1117 Session().flush()
1118 1118 events.trigger(
1119 1119 events.PullRequestCommentEvent(pull_request, comment))
1120 1120
1121 1121 else:
1122 1122 # regular comment case, could be inline, or one with status.
1123 1123 # for that one we check also permissions
1124 1124
1125 1125 allowed_to_change_status = PullRequestModel().check_user_change_status(
1126 1126 pull_request, self._rhodecode_user)
1127 1127
1128 1128 if status and allowed_to_change_status:
1129 1129 message = (_('Status change %(transition_icon)s %(status)s')
1130 1130 % {'transition_icon': '>',
1131 1131 'status': ChangesetStatus.get_status_lbl(status)})
1132 1132 text = text or message
1133 1133
1134 1134 comment = CommentsModel().create(
1135 1135 text=text,
1136 1136 repo=self.db_repo.repo_id,
1137 1137 user=self._rhodecode_user.user_id,
1138 1138 pull_request=pull_request,
1139 1139 f_path=self.request.POST.get('f_path'),
1140 1140 line_no=self.request.POST.get('line'),
1141 1141 status_change=(ChangesetStatus.get_status_lbl(status)
1142 1142 if status and allowed_to_change_status else None),
1143 1143 status_change_type=(status
1144 1144 if status and allowed_to_change_status else None),
1145 1145 comment_type=comment_type,
1146 1146 resolves_comment_id=resolves_comment_id
1147 1147 )
1148 1148
1149 1149 if allowed_to_change_status:
1150 1150 # calculate old status before we change it
1151 1151 old_calculated_status = pull_request.calculated_review_status()
1152 1152
1153 1153 # get status if set !
1154 1154 if status:
1155 1155 ChangesetStatusModel().set_status(
1156 1156 self.db_repo.repo_id,
1157 1157 status,
1158 1158 self._rhodecode_user.user_id,
1159 1159 comment,
1160 1160 pull_request=pull_request
1161 1161 )
1162 1162
1163 1163 Session().flush()
1164 1164 # this is somehow required to get access to some relationship
1165 1165 # loaded on comment
1166 1166 Session().refresh(comment)
1167 1167
1168 1168 events.trigger(
1169 1169 events.PullRequestCommentEvent(pull_request, comment))
1170 1170
1171 1171 # we now calculate the status of pull request, and based on that
1172 1172 # calculation we set the commits status
1173 1173 calculated_status = pull_request.calculated_review_status()
1174 1174 if old_calculated_status != calculated_status:
1175 1175 PullRequestModel()._trigger_pull_request_hook(
1176 1176 pull_request, self._rhodecode_user, 'review_status_change')
1177 1177
1178 1178 Session().commit()
1179 1179
1180 1180 data = {
1181 1181 'target_id': h.safeid(h.safe_unicode(
1182 1182 self.request.POST.get('f_path'))),
1183 1183 }
1184 1184 if comment:
1185 1185 c.co = comment
1186 1186 rendered_comment = render(
1187 1187 'rhodecode:templates/changeset/changeset_comment_block.mako',
1188 1188 self._get_template_context(c), self.request)
1189 1189
1190 1190 data.update(comment.get_dict())
1191 1191 data.update({'rendered_text': rendered_comment})
1192 1192
1193 1193 return data
1194 1194
1195 1195 @LoginRequired()
1196 1196 @NotAnonymous()
1197 1197 @HasRepoPermissionAnyDecorator(
1198 1198 'repository.read', 'repository.write', 'repository.admin')
1199 1199 @CSRFRequired()
1200 1200 @view_config(
1201 1201 route_name='pullrequest_comment_delete', request_method='POST',
1202 1202 renderer='json_ext')
1203 1203 def pull_request_comment_delete(self):
1204 1204 pull_request = PullRequest.get_or_404(
1205 1205 self.request.matchdict['pull_request_id'])
1206 1206
1207 1207 comment = ChangesetComment.get_or_404(
1208 1208 self.request.matchdict['comment_id'])
1209 1209 comment_id = comment.comment_id
1210 1210
1211 1211 if pull_request.is_closed():
1212 1212 log.debug('comment: forbidden because pull request is closed')
1213 1213 raise HTTPForbidden()
1214 1214
1215 1215 if not comment:
1216 1216 log.debug('Comment with id:%s not found, skipping', comment_id)
1217 1217 # comment already deleted in another call probably
1218 1218 return True
1219 1219
1220 1220 if comment.pull_request.is_closed():
1221 1221 # don't allow deleting comments on closed pull request
1222 1222 raise HTTPForbidden()
1223 1223
1224 1224 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1225 1225 super_admin = h.HasPermissionAny('hg.admin')()
1226 1226 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1227 1227 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1228 1228 comment_repo_admin = is_repo_admin and is_repo_comment
1229 1229
1230 1230 if super_admin or comment_owner or comment_repo_admin:
1231 1231 old_calculated_status = comment.pull_request.calculated_review_status()
1232 1232 CommentsModel().delete(comment=comment, user=self._rhodecode_user)
1233 1233 Session().commit()
1234 1234 calculated_status = comment.pull_request.calculated_review_status()
1235 1235 if old_calculated_status != calculated_status:
1236 1236 PullRequestModel()._trigger_pull_request_hook(
1237 1237 comment.pull_request, self._rhodecode_user, 'review_status_change')
1238 1238 return True
1239 1239 else:
1240 1240 log.warning('No permissions for user %s to delete comment_id: %s',
1241 1241 self._rhodecode_db_user, comment_id)
1242 1242 raise HTTPNotFound()
@@ -1,1620 +1,1641 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-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 Base module for all VCS systems
23 23 """
24 24
25 25 import collections
26 26 import datetime
27 27 import itertools
28 28 import logging
29 29 import os
30 30 import time
31 31 import warnings
32 32
33 33 from zope.cachedescriptors.property import Lazy as LazyProperty
34 34
35 35 from rhodecode.lib.utils2 import safe_str, safe_unicode
36 36 from rhodecode.lib.vcs import connection
37 37 from rhodecode.lib.vcs.utils import author_name, author_email
38 38 from rhodecode.lib.vcs.conf import settings
39 39 from rhodecode.lib.vcs.exceptions import (
40 40 CommitError, EmptyRepositoryError, NodeAlreadyAddedError,
41 41 NodeAlreadyChangedError, NodeAlreadyExistsError, NodeAlreadyRemovedError,
42 42 NodeDoesNotExistError, NodeNotChangedError, VCSError,
43 43 ImproperArchiveTypeError, BranchDoesNotExistError, CommitDoesNotExistError,
44 44 RepositoryError)
45 45
46 46
47 47 log = logging.getLogger(__name__)
48 48
49 49
50 50 FILEMODE_DEFAULT = 0100644
51 51 FILEMODE_EXECUTABLE = 0100755
52 52
53 53 Reference = collections.namedtuple('Reference', ('type', 'name', 'commit_id'))
54 54 MergeResponse = collections.namedtuple(
55 55 'MergeResponse',
56 56 ('possible', 'executed', 'merge_ref', 'failure_reason'))
57 57
58 58
59 59 class MergeFailureReason(object):
60 60 """
61 61 Enumeration with all the reasons why the server side merge could fail.
62 62
63 63 DO NOT change the number of the reasons, as they may be stored in the
64 64 database.
65 65
66 66 Changing the name of a reason is acceptable and encouraged to deprecate old
67 67 reasons.
68 68 """
69 69
70 70 # Everything went well.
71 71 NONE = 0
72 72
73 73 # An unexpected exception was raised. Check the logs for more details.
74 74 UNKNOWN = 1
75 75
76 76 # The merge was not successful, there are conflicts.
77 77 MERGE_FAILED = 2
78 78
79 79 # The merge succeeded but we could not push it to the target repository.
80 80 PUSH_FAILED = 3
81 81
82 82 # The specified target is not a head in the target repository.
83 83 TARGET_IS_NOT_HEAD = 4
84 84
85 85 # The source repository contains more branches than the target. Pushing
86 86 # the merge will create additional branches in the target.
87 87 HG_SOURCE_HAS_MORE_BRANCHES = 5
88 88
89 89 # The target reference has multiple heads. That does not allow to correctly
90 90 # identify the target location. This could only happen for mercurial
91 91 # branches.
92 92 HG_TARGET_HAS_MULTIPLE_HEADS = 6
93 93
94 94 # The target repository is locked
95 95 TARGET_IS_LOCKED = 7
96 96
97 97 # Deprecated, use MISSING_TARGET_REF or MISSING_SOURCE_REF instead.
98 98 # A involved commit could not be found.
99 99 _DEPRECATED_MISSING_COMMIT = 8
100 100
101 101 # The target repo reference is missing.
102 102 MISSING_TARGET_REF = 9
103 103
104 104 # The source repo reference is missing.
105 105 MISSING_SOURCE_REF = 10
106 106
107 107 # The merge was not successful, there are conflicts related to sub
108 108 # repositories.
109 109 SUBREPO_MERGE_FAILED = 11
110 110
111 111
112 112 class UpdateFailureReason(object):
113 113 """
114 114 Enumeration with all the reasons why the pull request update could fail.
115 115
116 116 DO NOT change the number of the reasons, as they may be stored in the
117 117 database.
118 118
119 119 Changing the name of a reason is acceptable and encouraged to deprecate old
120 120 reasons.
121 121 """
122 122
123 123 # Everything went well.
124 124 NONE = 0
125 125
126 126 # An unexpected exception was raised. Check the logs for more details.
127 127 UNKNOWN = 1
128 128
129 129 # The pull request is up to date.
130 130 NO_CHANGE = 2
131 131
132 132 # The pull request has a reference type that is not supported for update.
133 133 WRONG_REF_TYPE = 3
134 134
135 135 # Update failed because the target reference is missing.
136 136 MISSING_TARGET_REF = 4
137 137
138 138 # Update failed because the source reference is missing.
139 139 MISSING_SOURCE_REF = 5
140 140
141 141
142 142 class BaseRepository(object):
143 143 """
144 144 Base Repository for final backends
145 145
146 146 .. attribute:: DEFAULT_BRANCH_NAME
147 147
148 148 name of default branch (i.e. "trunk" for svn, "master" for git etc.
149 149
150 150 .. attribute:: commit_ids
151 151
152 152 list of all available commit ids, in ascending order
153 153
154 154 .. attribute:: path
155 155
156 156 absolute path to the repository
157 157
158 158 .. attribute:: bookmarks
159 159
160 160 Mapping from name to :term:`Commit ID` of the bookmark. Empty in case
161 161 there are no bookmarks or the backend implementation does not support
162 162 bookmarks.
163 163
164 164 .. attribute:: tags
165 165
166 166 Mapping from name to :term:`Commit ID` of the tag.
167 167
168 168 """
169 169
170 170 DEFAULT_BRANCH_NAME = None
171 171 DEFAULT_CONTACT = u"Unknown"
172 172 DEFAULT_DESCRIPTION = u"unknown"
173 173 EMPTY_COMMIT_ID = '0' * 40
174 174
175 175 path = None
176 176
177 177 def __init__(self, repo_path, config=None, create=False, **kwargs):
178 178 """
179 179 Initializes repository. Raises RepositoryError if repository could
180 180 not be find at the given ``repo_path`` or directory at ``repo_path``
181 181 exists and ``create`` is set to True.
182 182
183 183 :param repo_path: local path of the repository
184 184 :param config: repository configuration
185 185 :param create=False: if set to True, would try to create repository.
186 186 :param src_url=None: if set, should be proper url from which repository
187 187 would be cloned; requires ``create`` parameter to be set to True -
188 188 raises RepositoryError if src_url is set and create evaluates to
189 189 False
190 190 """
191 191 raise NotImplementedError
192 192
193 193 def __repr__(self):
194 194 return '<%s at %s>' % (self.__class__.__name__, self.path)
195 195
196 196 def __len__(self):
197 197 return self.count()
198 198
199 199 def __eq__(self, other):
200 200 same_instance = isinstance(other, self.__class__)
201 201 return same_instance and other.path == self.path
202 202
203 203 def __ne__(self, other):
204 204 return not self.__eq__(other)
205 205
206 206 @classmethod
207 207 def get_default_config(cls, default=None):
208 208 config = Config()
209 209 if default and isinstance(default, list):
210 210 for section, key, val in default:
211 211 config.set(section, key, val)
212 212 return config
213 213
214 214 @LazyProperty
215 215 def EMPTY_COMMIT(self):
216 216 return EmptyCommit(self.EMPTY_COMMIT_ID)
217 217
218 218 @LazyProperty
219 219 def alias(self):
220 220 for k, v in settings.BACKENDS.items():
221 221 if v.split('.')[-1] == str(self.__class__.__name__):
222 222 return k
223 223
224 224 @LazyProperty
225 225 def name(self):
226 226 return safe_unicode(os.path.basename(self.path))
227 227
228 228 @LazyProperty
229 229 def description(self):
230 230 raise NotImplementedError
231 231
232 232 def refs(self):
233 233 """
234 234 returns a `dict` with branches, bookmarks, tags, and closed_branches
235 235 for this repository
236 236 """
237 237 return dict(
238 238 branches=self.branches,
239 239 branches_closed=self.branches_closed,
240 240 tags=self.tags,
241 241 bookmarks=self.bookmarks
242 242 )
243 243
244 244 @LazyProperty
245 245 def branches(self):
246 246 """
247 247 A `dict` which maps branch names to commit ids.
248 248 """
249 249 raise NotImplementedError
250 250
251 251 @LazyProperty
252 252 def branches_closed(self):
253 253 """
254 254 A `dict` which maps tags names to commit ids.
255 255 """
256 256 raise NotImplementedError
257 257
258 258 @LazyProperty
259 259 def bookmarks(self):
260 260 """
261 261 A `dict` which maps tags names to commit ids.
262 262 """
263 263 raise NotImplementedError
264 264
265 265 @LazyProperty
266 266 def tags(self):
267 267 """
268 268 A `dict` which maps tags names to commit ids.
269 269 """
270 270 raise NotImplementedError
271 271
272 272 @LazyProperty
273 273 def size(self):
274 274 """
275 275 Returns combined size in bytes for all repository files
276 276 """
277 277 tip = self.get_commit()
278 278 return tip.size
279 279
280 280 def size_at_commit(self, commit_id):
281 281 commit = self.get_commit(commit_id)
282 282 return commit.size
283 283
284 284 def is_empty(self):
285 285 return not bool(self.commit_ids)
286 286
287 287 @staticmethod
288 288 def check_url(url, config):
289 289 """
290 290 Function will check given url and try to verify if it's a valid
291 291 link.
292 292 """
293 293 raise NotImplementedError
294 294
295 295 @staticmethod
296 296 def is_valid_repository(path):
297 297 """
298 298 Check if given `path` contains a valid repository of this backend
299 299 """
300 300 raise NotImplementedError
301 301
302 302 # ==========================================================================
303 303 # COMMITS
304 304 # ==========================================================================
305 305
306 306 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
307 307 """
308 308 Returns instance of `BaseCommit` class. If `commit_id` and `commit_idx`
309 309 are both None, most recent commit is returned.
310 310
311 311 :param pre_load: Optional. List of commit attributes to load.
312 312
313 313 :raises ``EmptyRepositoryError``: if there are no commits
314 314 """
315 315 raise NotImplementedError
316 316
317 317 def __iter__(self):
318 318 for commit_id in self.commit_ids:
319 319 yield self.get_commit(commit_id=commit_id)
320 320
321 321 def get_commits(
322 322 self, start_id=None, end_id=None, start_date=None, end_date=None,
323 323 branch_name=None, show_hidden=False, pre_load=None):
324 324 """
325 325 Returns iterator of `BaseCommit` objects from start to end
326 326 not inclusive. This should behave just like a list, ie. end is not
327 327 inclusive.
328 328
329 329 :param start_id: None or str, must be a valid commit id
330 330 :param end_id: None or str, must be a valid commit id
331 331 :param start_date:
332 332 :param end_date:
333 333 :param branch_name:
334 334 :param show_hidden:
335 335 :param pre_load:
336 336 """
337 337 raise NotImplementedError
338 338
339 339 def __getitem__(self, key):
340 340 """
341 341 Allows index based access to the commit objects of this repository.
342 342 """
343 343 pre_load = ["author", "branch", "date", "message", "parents"]
344 344 if isinstance(key, slice):
345 345 return self._get_range(key, pre_load)
346 346 return self.get_commit(commit_idx=key, pre_load=pre_load)
347 347
348 348 def _get_range(self, slice_obj, pre_load):
349 349 for commit_id in self.commit_ids.__getitem__(slice_obj):
350 350 yield self.get_commit(commit_id=commit_id, pre_load=pre_load)
351 351
352 352 def count(self):
353 353 return len(self.commit_ids)
354 354
355 355 def tag(self, name, user, commit_id=None, message=None, date=None, **opts):
356 356 """
357 357 Creates and returns a tag for the given ``commit_id``.
358 358
359 359 :param name: name for new tag
360 360 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
361 361 :param commit_id: commit id for which new tag would be created
362 362 :param message: message of the tag's commit
363 363 :param date: date of tag's commit
364 364
365 365 :raises TagAlreadyExistError: if tag with same name already exists
366 366 """
367 367 raise NotImplementedError
368 368
369 369 def remove_tag(self, name, user, message=None, date=None):
370 370 """
371 371 Removes tag with the given ``name``.
372 372
373 373 :param name: name of the tag to be removed
374 374 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
375 375 :param message: message of the tag's removal commit
376 376 :param date: date of tag's removal commit
377 377
378 378 :raises TagDoesNotExistError: if tag with given name does not exists
379 379 """
380 380 raise NotImplementedError
381 381
382 382 def get_diff(
383 383 self, commit1, commit2, path=None, ignore_whitespace=False,
384 384 context=3, path1=None):
385 385 """
386 386 Returns (git like) *diff*, as plain text. Shows changes introduced by
387 387 `commit2` since `commit1`.
388 388
389 389 :param commit1: Entry point from which diff is shown. Can be
390 390 ``self.EMPTY_COMMIT`` - in this case, patch showing all
391 391 the changes since empty state of the repository until `commit2`
392 392 :param commit2: Until which commit changes should be shown.
393 393 :param path: Can be set to a path of a file to create a diff of that
394 394 file. If `path1` is also set, this value is only associated to
395 395 `commit2`.
396 396 :param ignore_whitespace: If set to ``True``, would not show whitespace
397 397 changes. Defaults to ``False``.
398 398 :param context: How many lines before/after changed lines should be
399 399 shown. Defaults to ``3``.
400 400 :param path1: Can be set to a path to associate with `commit1`. This
401 401 parameter works only for backends which support diff generation for
402 402 different paths. Other backends will raise a `ValueError` if `path1`
403 403 is set and has a different value than `path`.
404 404 :param file_path: filter this diff by given path pattern
405 405 """
406 406 raise NotImplementedError
407 407
408 408 def strip(self, commit_id, branch=None):
409 409 """
410 410 Strip given commit_id from the repository
411 411 """
412 412 raise NotImplementedError
413 413
414 414 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
415 415 """
416 416 Return a latest common ancestor commit if one exists for this repo
417 417 `commit_id1` vs `commit_id2` from `repo2`.
418 418
419 419 :param commit_id1: Commit it from this repository to use as a
420 420 target for the comparison.
421 421 :param commit_id2: Source commit id to use for comparison.
422 422 :param repo2: Source repository to use for comparison.
423 423 """
424 424 raise NotImplementedError
425 425
426 426 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
427 427 """
428 428 Compare this repository's revision `commit_id1` with `commit_id2`.
429 429
430 430 Returns a tuple(commits, ancestor) that would be merged from
431 431 `commit_id2`. Doing a normal compare (``merge=False``), ``None``
432 432 will be returned as ancestor.
433 433
434 434 :param commit_id1: Commit it from this repository to use as a
435 435 target for the comparison.
436 436 :param commit_id2: Source commit id to use for comparison.
437 437 :param repo2: Source repository to use for comparison.
438 438 :param merge: If set to ``True`` will do a merge compare which also
439 439 returns the common ancestor.
440 440 :param pre_load: Optional. List of commit attributes to load.
441 441 """
442 442 raise NotImplementedError
443 443
444 444 def merge(self, target_ref, source_repo, source_ref, workspace_id,
445 445 user_name='', user_email='', message='', dry_run=False,
446 446 use_rebase=False, close_branch=False):
447 447 """
448 448 Merge the revisions specified in `source_ref` from `source_repo`
449 449 onto the `target_ref` of this repository.
450 450
451 451 `source_ref` and `target_ref` are named tupls with the following
452 452 fields `type`, `name` and `commit_id`.
453 453
454 454 Returns a MergeResponse named tuple with the following fields
455 455 'possible', 'executed', 'source_commit', 'target_commit',
456 456 'merge_commit'.
457 457
458 458 :param target_ref: `target_ref` points to the commit on top of which
459 459 the `source_ref` should be merged.
460 460 :param source_repo: The repository that contains the commits to be
461 461 merged.
462 462 :param source_ref: `source_ref` points to the topmost commit from
463 463 the `source_repo` which should be merged.
464 464 :param workspace_id: `workspace_id` unique identifier.
465 465 :param user_name: Merge commit `user_name`.
466 466 :param user_email: Merge commit `user_email`.
467 467 :param message: Merge commit `message`.
468 468 :param dry_run: If `True` the merge will not take place.
469 469 :param use_rebase: If `True` commits from the source will be rebased
470 470 on top of the target instead of being merged.
471 471 :param close_branch: If `True` branch will be close before merging it
472 472 """
473 473 if dry_run:
474 474 message = message or 'dry_run_merge_message'
475 475 user_email = user_email or 'dry-run-merge@rhodecode.com'
476 476 user_name = user_name or 'Dry-Run User'
477 477 else:
478 478 if not user_name:
479 479 raise ValueError('user_name cannot be empty')
480 480 if not user_email:
481 481 raise ValueError('user_email cannot be empty')
482 482 if not message:
483 483 raise ValueError('message cannot be empty')
484 484
485 485 shadow_repository_path = self._maybe_prepare_merge_workspace(
486 486 workspace_id, target_ref, source_ref)
487 487
488 488 try:
489 489 return self._merge_repo(
490 490 shadow_repository_path, target_ref, source_repo,
491 491 source_ref, message, user_name, user_email, dry_run=dry_run,
492 492 use_rebase=use_rebase, close_branch=close_branch)
493 493 except RepositoryError:
494 494 log.exception(
495 495 'Unexpected failure when running merge, dry-run=%s',
496 496 dry_run)
497 497 return MergeResponse(
498 498 False, False, None, MergeFailureReason.UNKNOWN)
499 499
500 500 def _merge_repo(self, shadow_repository_path, target_ref,
501 501 source_repo, source_ref, merge_message,
502 502 merger_name, merger_email, dry_run=False,
503 503 use_rebase=False, close_branch=False):
504 504 """Internal implementation of merge."""
505 505 raise NotImplementedError
506 506
507 507 def _maybe_prepare_merge_workspace(self, workspace_id, target_ref, source_ref):
508 508 """
509 509 Create the merge workspace.
510 510
511 511 :param workspace_id: `workspace_id` unique identifier.
512 512 """
513 513 raise NotImplementedError
514 514
515 515 def cleanup_merge_workspace(self, workspace_id):
516 516 """
517 517 Remove merge workspace.
518 518
519 519 This function MUST not fail in case there is no workspace associated to
520 520 the given `workspace_id`.
521 521
522 522 :param workspace_id: `workspace_id` unique identifier.
523 523 """
524 524 raise NotImplementedError
525 525
526 526 # ========== #
527 527 # COMMIT API #
528 528 # ========== #
529 529
530 530 @LazyProperty
531 531 def in_memory_commit(self):
532 532 """
533 533 Returns :class:`InMemoryCommit` object for this repository.
534 534 """
535 535 raise NotImplementedError
536 536
537 537 # ======================== #
538 538 # UTILITIES FOR SUBCLASSES #
539 539 # ======================== #
540 540
541 541 def _validate_diff_commits(self, commit1, commit2):
542 542 """
543 543 Validates that the given commits are related to this repository.
544 544
545 545 Intended as a utility for sub classes to have a consistent validation
546 546 of input parameters in methods like :meth:`get_diff`.
547 547 """
548 548 self._validate_commit(commit1)
549 549 self._validate_commit(commit2)
550 550 if (isinstance(commit1, EmptyCommit) and
551 551 isinstance(commit2, EmptyCommit)):
552 552 raise ValueError("Cannot compare two empty commits")
553 553
554 554 def _validate_commit(self, commit):
555 555 if not isinstance(commit, BaseCommit):
556 556 raise TypeError(
557 557 "%s is not of type BaseCommit" % repr(commit))
558 558 if commit.repository != self and not isinstance(commit, EmptyCommit):
559 559 raise ValueError(
560 560 "Commit %s must be a valid commit from this repository %s, "
561 561 "related to this repository instead %s." %
562 562 (commit, self, commit.repository))
563 563
564 564 def _validate_commit_id(self, commit_id):
565 565 if not isinstance(commit_id, basestring):
566 566 raise TypeError("commit_id must be a string value")
567 567
568 568 def _validate_commit_idx(self, commit_idx):
569 569 if not isinstance(commit_idx, (int, long)):
570 570 raise TypeError("commit_idx must be a numeric value")
571 571
572 572 def _validate_branch_name(self, branch_name):
573 573 if branch_name and branch_name not in self.branches_all:
574 574 msg = ("Branch %s not found in %s" % (branch_name, self))
575 575 raise BranchDoesNotExistError(msg)
576 576
577 577 #
578 578 # Supporting deprecated API parts
579 579 # TODO: johbo: consider to move this into a mixin
580 580 #
581 581
582 582 @property
583 583 def EMPTY_CHANGESET(self):
584 584 warnings.warn(
585 585 "Use EMPTY_COMMIT or EMPTY_COMMIT_ID instead", DeprecationWarning)
586 586 return self.EMPTY_COMMIT_ID
587 587
588 588 @property
589 589 def revisions(self):
590 590 warnings.warn("Use commits attribute instead", DeprecationWarning)
591 591 return self.commit_ids
592 592
593 593 @revisions.setter
594 594 def revisions(self, value):
595 595 warnings.warn("Use commits attribute instead", DeprecationWarning)
596 596 self.commit_ids = value
597 597
598 598 def get_changeset(self, revision=None, pre_load=None):
599 599 warnings.warn("Use get_commit instead", DeprecationWarning)
600 600 commit_id = None
601 601 commit_idx = None
602 602 if isinstance(revision, basestring):
603 603 commit_id = revision
604 604 else:
605 605 commit_idx = revision
606 606 return self.get_commit(
607 607 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
608 608
609 609 def get_changesets(
610 610 self, start=None, end=None, start_date=None, end_date=None,
611 611 branch_name=None, pre_load=None):
612 612 warnings.warn("Use get_commits instead", DeprecationWarning)
613 613 start_id = self._revision_to_commit(start)
614 614 end_id = self._revision_to_commit(end)
615 615 return self.get_commits(
616 616 start_id=start_id, end_id=end_id, start_date=start_date,
617 617 end_date=end_date, branch_name=branch_name, pre_load=pre_load)
618 618
619 619 def _revision_to_commit(self, revision):
620 620 """
621 621 Translates a revision to a commit_id
622 622
623 623 Helps to support the old changeset based API which allows to use
624 624 commit ids and commit indices interchangeable.
625 625 """
626 626 if revision is None:
627 627 return revision
628 628
629 629 if isinstance(revision, basestring):
630 630 commit_id = revision
631 631 else:
632 632 commit_id = self.commit_ids[revision]
633 633 return commit_id
634 634
635 635 @property
636 636 def in_memory_changeset(self):
637 637 warnings.warn("Use in_memory_commit instead", DeprecationWarning)
638 638 return self.in_memory_commit
639 639
640 #
641 def get_path_permissions(self, username):
642 """
643
644 Returns a path permission checker or None if not supported
645
646 :param username: session user name
647 :return: an instance of BasePathPermissionChecker or None
648 """
649 return None
650
640 651
641 652 class BaseCommit(object):
642 653 """
643 654 Each backend should implement it's commit representation.
644 655
645 656 **Attributes**
646 657
647 658 ``repository``
648 659 repository object within which commit exists
649 660
650 661 ``id``
651 662 The commit id, may be ``raw_id`` or i.e. for mercurial's tip
652 663 just ``tip``.
653 664
654 665 ``raw_id``
655 666 raw commit representation (i.e. full 40 length sha for git
656 667 backend)
657 668
658 669 ``short_id``
659 670 shortened (if apply) version of ``raw_id``; it would be simple
660 671 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
661 672 as ``raw_id`` for subversion
662 673
663 674 ``idx``
664 675 commit index
665 676
666 677 ``files``
667 678 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
668 679
669 680 ``dirs``
670 681 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
671 682
672 683 ``nodes``
673 684 combined list of ``Node`` objects
674 685
675 686 ``author``
676 687 author of the commit, as unicode
677 688
678 689 ``message``
679 690 message of the commit, as unicode
680 691
681 692 ``parents``
682 693 list of parent commits
683 694
684 695 """
685 696
686 697 branch = None
687 698 """
688 699 Depending on the backend this should be set to the branch name of the
689 700 commit. Backends not supporting branches on commits should leave this
690 701 value as ``None``.
691 702 """
692 703
693 704 _ARCHIVE_PREFIX_TEMPLATE = b'{repo_name}-{short_id}'
694 705 """
695 706 This template is used to generate a default prefix for repository archives
696 707 if no prefix has been specified.
697 708 """
698 709
699 710 def __str__(self):
700 711 return '<%s at %s:%s>' % (
701 712 self.__class__.__name__, self.idx, self.short_id)
702 713
703 714 def __repr__(self):
704 715 return self.__str__()
705 716
706 717 def __unicode__(self):
707 718 return u'%s:%s' % (self.idx, self.short_id)
708 719
709 720 def __eq__(self, other):
710 721 same_instance = isinstance(other, self.__class__)
711 722 return same_instance and self.raw_id == other.raw_id
712 723
713 724 def __json__(self):
714 725 parents = []
715 726 try:
716 727 for parent in self.parents:
717 728 parents.append({'raw_id': parent.raw_id})
718 729 except NotImplementedError:
719 730 # empty commit doesn't have parents implemented
720 731 pass
721 732
722 733 return {
723 734 'short_id': self.short_id,
724 735 'raw_id': self.raw_id,
725 736 'revision': self.idx,
726 737 'message': self.message,
727 738 'date': self.date,
728 739 'author': self.author,
729 740 'parents': parents,
730 741 'branch': self.branch
731 742 }
732 743
733 744 def _get_refs(self):
734 745 return {
735 746 'branches': [self.branch],
736 747 'bookmarks': getattr(self, 'bookmarks', []),
737 748 'tags': self.tags
738 749 }
739 750
740 751 @LazyProperty
741 752 def last(self):
742 753 """
743 754 ``True`` if this is last commit in repository, ``False``
744 755 otherwise; trying to access this attribute while there is no
745 756 commits would raise `EmptyRepositoryError`
746 757 """
747 758 if self.repository is None:
748 759 raise CommitError("Cannot check if it's most recent commit")
749 760 return self.raw_id == self.repository.commit_ids[-1]
750 761
751 762 @LazyProperty
752 763 def parents(self):
753 764 """
754 765 Returns list of parent commits.
755 766 """
756 767 raise NotImplementedError
757 768
758 769 @property
759 770 def merge(self):
760 771 """
761 772 Returns boolean if commit is a merge.
762 773 """
763 774 return len(self.parents) > 1
764 775
765 776 @LazyProperty
766 777 def children(self):
767 778 """
768 779 Returns list of child commits.
769 780 """
770 781 raise NotImplementedError
771 782
772 783 @LazyProperty
773 784 def id(self):
774 785 """
775 786 Returns string identifying this commit.
776 787 """
777 788 raise NotImplementedError
778 789
779 790 @LazyProperty
780 791 def raw_id(self):
781 792 """
782 793 Returns raw string identifying this commit.
783 794 """
784 795 raise NotImplementedError
785 796
786 797 @LazyProperty
787 798 def short_id(self):
788 799 """
789 800 Returns shortened version of ``raw_id`` attribute, as string,
790 801 identifying this commit, useful for presentation to users.
791 802 """
792 803 raise NotImplementedError
793 804
794 805 @LazyProperty
795 806 def idx(self):
796 807 """
797 808 Returns integer identifying this commit.
798 809 """
799 810 raise NotImplementedError
800 811
801 812 @LazyProperty
802 813 def committer(self):
803 814 """
804 815 Returns committer for this commit
805 816 """
806 817 raise NotImplementedError
807 818
808 819 @LazyProperty
809 820 def committer_name(self):
810 821 """
811 822 Returns committer name for this commit
812 823 """
813 824
814 825 return author_name(self.committer)
815 826
816 827 @LazyProperty
817 828 def committer_email(self):
818 829 """
819 830 Returns committer email address for this commit
820 831 """
821 832
822 833 return author_email(self.committer)
823 834
824 835 @LazyProperty
825 836 def author(self):
826 837 """
827 838 Returns author for this commit
828 839 """
829 840
830 841 raise NotImplementedError
831 842
832 843 @LazyProperty
833 844 def author_name(self):
834 845 """
835 846 Returns author name for this commit
836 847 """
837 848
838 849 return author_name(self.author)
839 850
840 851 @LazyProperty
841 852 def author_email(self):
842 853 """
843 854 Returns author email address for this commit
844 855 """
845 856
846 857 return author_email(self.author)
847 858
848 859 def get_file_mode(self, path):
849 860 """
850 861 Returns stat mode of the file at `path`.
851 862 """
852 863 raise NotImplementedError
853 864
854 865 def is_link(self, path):
855 866 """
856 867 Returns ``True`` if given `path` is a symlink
857 868 """
858 869 raise NotImplementedError
859 870
860 871 def get_file_content(self, path):
861 872 """
862 873 Returns content of the file at the given `path`.
863 874 """
864 875 raise NotImplementedError
865 876
866 877 def get_file_size(self, path):
867 878 """
868 879 Returns size of the file at the given `path`.
869 880 """
870 881 raise NotImplementedError
871 882
872 883 def get_file_commit(self, path, pre_load=None):
873 884 """
874 885 Returns last commit of the file at the given `path`.
875 886
876 887 :param pre_load: Optional. List of commit attributes to load.
877 888 """
878 889 commits = self.get_file_history(path, limit=1, pre_load=pre_load)
879 890 if not commits:
880 891 raise RepositoryError(
881 892 'Failed to fetch history for path {}. '
882 893 'Please check if such path exists in your repository'.format(
883 894 path))
884 895 return commits[0]
885 896
886 897 def get_file_history(self, path, limit=None, pre_load=None):
887 898 """
888 899 Returns history of file as reversed list of :class:`BaseCommit`
889 900 objects for which file at given `path` has been modified.
890 901
891 902 :param limit: Optional. Allows to limit the size of the returned
892 903 history. This is intended as a hint to the underlying backend, so
893 904 that it can apply optimizations depending on the limit.
894 905 :param pre_load: Optional. List of commit attributes to load.
895 906 """
896 907 raise NotImplementedError
897 908
898 909 def get_file_annotate(self, path, pre_load=None):
899 910 """
900 911 Returns a generator of four element tuples with
901 912 lineno, sha, commit lazy loader and line
902 913
903 914 :param pre_load: Optional. List of commit attributes to load.
904 915 """
905 916 raise NotImplementedError
906 917
907 918 def get_nodes(self, path):
908 919 """
909 920 Returns combined ``DirNode`` and ``FileNode`` objects list representing
910 921 state of commit at the given ``path``.
911 922
912 923 :raises ``CommitError``: if node at the given ``path`` is not
913 924 instance of ``DirNode``
914 925 """
915 926 raise NotImplementedError
916 927
917 928 def get_node(self, path):
918 929 """
919 930 Returns ``Node`` object from the given ``path``.
920 931
921 932 :raises ``NodeDoesNotExistError``: if there is no node at the given
922 933 ``path``
923 934 """
924 935 raise NotImplementedError
925 936
926 937 def get_largefile_node(self, path):
927 938 """
928 939 Returns the path to largefile from Mercurial/Git-lfs storage.
929 940 or None if it's not a largefile node
930 941 """
931 942 return None
932 943
933 944 def archive_repo(self, file_path, kind='tgz', subrepos=None,
934 945 prefix=None, write_metadata=False, mtime=None):
935 946 """
936 947 Creates an archive containing the contents of the repository.
937 948
938 949 :param file_path: path to the file which to create the archive.
939 950 :param kind: one of following: ``"tbz2"``, ``"tgz"``, ``"zip"``.
940 951 :param prefix: name of root directory in archive.
941 952 Default is repository name and commit's short_id joined with dash:
942 953 ``"{repo_name}-{short_id}"``.
943 954 :param write_metadata: write a metadata file into archive.
944 955 :param mtime: custom modification time for archive creation, defaults
945 956 to time.time() if not given.
946 957
947 958 :raise VCSError: If prefix has a problem.
948 959 """
949 960 allowed_kinds = settings.ARCHIVE_SPECS.keys()
950 961 if kind not in allowed_kinds:
951 962 raise ImproperArchiveTypeError(
952 963 'Archive kind (%s) not supported use one of %s' %
953 964 (kind, allowed_kinds))
954 965
955 966 prefix = self._validate_archive_prefix(prefix)
956 967
957 968 mtime = mtime or time.mktime(self.date.timetuple())
958 969
959 970 file_info = []
960 971 cur_rev = self.repository.get_commit(commit_id=self.raw_id)
961 972 for _r, _d, files in cur_rev.walk('/'):
962 973 for f in files:
963 974 f_path = os.path.join(prefix, f.path)
964 975 file_info.append(
965 976 (f_path, f.mode, f.is_link(), f.raw_bytes))
966 977
967 978 if write_metadata:
968 979 metadata = [
969 980 ('repo_name', self.repository.name),
970 981 ('rev', self.raw_id),
971 982 ('create_time', mtime),
972 983 ('branch', self.branch),
973 984 ('tags', ','.join(self.tags)),
974 985 ]
975 986 meta = ["%s:%s" % (f_name, value) for f_name, value in metadata]
976 987 file_info.append(('.archival.txt', 0644, False, '\n'.join(meta)))
977 988
978 989 connection.Hg.archive_repo(file_path, mtime, file_info, kind)
979 990
980 991 def _validate_archive_prefix(self, prefix):
981 992 if prefix is None:
982 993 prefix = self._ARCHIVE_PREFIX_TEMPLATE.format(
983 994 repo_name=safe_str(self.repository.name),
984 995 short_id=self.short_id)
985 996 elif not isinstance(prefix, str):
986 997 raise ValueError("prefix not a bytes object: %s" % repr(prefix))
987 998 elif prefix.startswith('/'):
988 999 raise VCSError("Prefix cannot start with leading slash")
989 1000 elif prefix.strip() == '':
990 1001 raise VCSError("Prefix cannot be empty")
991 1002 return prefix
992 1003
993 1004 @LazyProperty
994 1005 def root(self):
995 1006 """
996 1007 Returns ``RootNode`` object for this commit.
997 1008 """
998 1009 return self.get_node('')
999 1010
1000 1011 def next(self, branch=None):
1001 1012 """
1002 1013 Returns next commit from current, if branch is gives it will return
1003 1014 next commit belonging to this branch
1004 1015
1005 1016 :param branch: show commits within the given named branch
1006 1017 """
1007 1018 indexes = xrange(self.idx + 1, self.repository.count())
1008 1019 return self._find_next(indexes, branch)
1009 1020
1010 1021 def prev(self, branch=None):
1011 1022 """
1012 1023 Returns previous commit from current, if branch is gives it will
1013 1024 return previous commit belonging to this branch
1014 1025
1015 1026 :param branch: show commit within the given named branch
1016 1027 """
1017 1028 indexes = xrange(self.idx - 1, -1, -1)
1018 1029 return self._find_next(indexes, branch)
1019 1030
1020 1031 def _find_next(self, indexes, branch=None):
1021 1032 if branch and self.branch != branch:
1022 1033 raise VCSError('Branch option used on commit not belonging '
1023 1034 'to that branch')
1024 1035
1025 1036 for next_idx in indexes:
1026 1037 commit = self.repository.get_commit(commit_idx=next_idx)
1027 1038 if branch and branch != commit.branch:
1028 1039 continue
1029 1040 return commit
1030 1041 raise CommitDoesNotExistError
1031 1042
1032 1043 def diff(self, ignore_whitespace=True, context=3):
1033 1044 """
1034 1045 Returns a `Diff` object representing the change made by this commit.
1035 1046 """
1036 1047 parent = (
1037 1048 self.parents[0] if self.parents else self.repository.EMPTY_COMMIT)
1038 1049 diff = self.repository.get_diff(
1039 1050 parent, self,
1040 1051 ignore_whitespace=ignore_whitespace,
1041 1052 context=context)
1042 1053 return diff
1043 1054
1044 1055 @LazyProperty
1045 1056 def added(self):
1046 1057 """
1047 1058 Returns list of added ``FileNode`` objects.
1048 1059 """
1049 1060 raise NotImplementedError
1050 1061
1051 1062 @LazyProperty
1052 1063 def changed(self):
1053 1064 """
1054 1065 Returns list of modified ``FileNode`` objects.
1055 1066 """
1056 1067 raise NotImplementedError
1057 1068
1058 1069 @LazyProperty
1059 1070 def removed(self):
1060 1071 """
1061 1072 Returns list of removed ``FileNode`` objects.
1062 1073 """
1063 1074 raise NotImplementedError
1064 1075
1065 1076 @LazyProperty
1066 1077 def size(self):
1067 1078 """
1068 1079 Returns total number of bytes from contents of all filenodes.
1069 1080 """
1070 1081 return sum((node.size for node in self.get_filenodes_generator()))
1071 1082
1072 1083 def walk(self, topurl=''):
1073 1084 """
1074 1085 Similar to os.walk method. Insted of filesystem it walks through
1075 1086 commit starting at given ``topurl``. Returns generator of tuples
1076 1087 (topnode, dirnodes, filenodes).
1077 1088 """
1078 1089 topnode = self.get_node(topurl)
1079 1090 if not topnode.is_dir():
1080 1091 return
1081 1092 yield (topnode, topnode.dirs, topnode.files)
1082 1093 for dirnode in topnode.dirs:
1083 1094 for tup in self.walk(dirnode.path):
1084 1095 yield tup
1085 1096
1086 1097 def get_filenodes_generator(self):
1087 1098 """
1088 1099 Returns generator that yields *all* file nodes.
1089 1100 """
1090 1101 for topnode, dirs, files in self.walk():
1091 1102 for node in files:
1092 1103 yield node
1093 1104
1094 1105 #
1095 1106 # Utilities for sub classes to support consistent behavior
1096 1107 #
1097 1108
1098 1109 def no_node_at_path(self, path):
1099 1110 return NodeDoesNotExistError(
1100 1111 u"There is no file nor directory at the given path: "
1101 1112 u"`%s` at commit %s" % (safe_unicode(path), self.short_id))
1102 1113
1103 1114 def _fix_path(self, path):
1104 1115 """
1105 1116 Paths are stored without trailing slash so we need to get rid off it if
1106 1117 needed.
1107 1118 """
1108 1119 return path.rstrip('/')
1109 1120
1110 1121 #
1111 1122 # Deprecated API based on changesets
1112 1123 #
1113 1124
1114 1125 @property
1115 1126 def revision(self):
1116 1127 warnings.warn("Use idx instead", DeprecationWarning)
1117 1128 return self.idx
1118 1129
1119 1130 @revision.setter
1120 1131 def revision(self, value):
1121 1132 warnings.warn("Use idx instead", DeprecationWarning)
1122 1133 self.idx = value
1123 1134
1124 1135 def get_file_changeset(self, path):
1125 1136 warnings.warn("Use get_file_commit instead", DeprecationWarning)
1126 1137 return self.get_file_commit(path)
1127 1138
1128 1139
1129 1140 class BaseChangesetClass(type):
1130 1141
1131 1142 def __instancecheck__(self, instance):
1132 1143 return isinstance(instance, BaseCommit)
1133 1144
1134 1145
1135 1146 class BaseChangeset(BaseCommit):
1136 1147
1137 1148 __metaclass__ = BaseChangesetClass
1138 1149
1139 1150 def __new__(cls, *args, **kwargs):
1140 1151 warnings.warn(
1141 1152 "Use BaseCommit instead of BaseChangeset", DeprecationWarning)
1142 1153 return super(BaseChangeset, cls).__new__(cls, *args, **kwargs)
1143 1154
1144 1155
1145 1156 class BaseInMemoryCommit(object):
1146 1157 """
1147 1158 Represents differences between repository's state (most recent head) and
1148 1159 changes made *in place*.
1149 1160
1150 1161 **Attributes**
1151 1162
1152 1163 ``repository``
1153 1164 repository object for this in-memory-commit
1154 1165
1155 1166 ``added``
1156 1167 list of ``FileNode`` objects marked as *added*
1157 1168
1158 1169 ``changed``
1159 1170 list of ``FileNode`` objects marked as *changed*
1160 1171
1161 1172 ``removed``
1162 1173 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
1163 1174 *removed*
1164 1175
1165 1176 ``parents``
1166 1177 list of :class:`BaseCommit` instances representing parents of
1167 1178 in-memory commit. Should always be 2-element sequence.
1168 1179
1169 1180 """
1170 1181
1171 1182 def __init__(self, repository):
1172 1183 self.repository = repository
1173 1184 self.added = []
1174 1185 self.changed = []
1175 1186 self.removed = []
1176 1187 self.parents = []
1177 1188
1178 1189 def add(self, *filenodes):
1179 1190 """
1180 1191 Marks given ``FileNode`` objects as *to be committed*.
1181 1192
1182 1193 :raises ``NodeAlreadyExistsError``: if node with same path exists at
1183 1194 latest commit
1184 1195 :raises ``NodeAlreadyAddedError``: if node with same path is already
1185 1196 marked as *added*
1186 1197 """
1187 1198 # Check if not already marked as *added* first
1188 1199 for node in filenodes:
1189 1200 if node.path in (n.path for n in self.added):
1190 1201 raise NodeAlreadyAddedError(
1191 1202 "Such FileNode %s is already marked for addition"
1192 1203 % node.path)
1193 1204 for node in filenodes:
1194 1205 self.added.append(node)
1195 1206
1196 1207 def change(self, *filenodes):
1197 1208 """
1198 1209 Marks given ``FileNode`` objects to be *changed* in next commit.
1199 1210
1200 1211 :raises ``EmptyRepositoryError``: if there are no commits yet
1201 1212 :raises ``NodeAlreadyExistsError``: if node with same path is already
1202 1213 marked to be *changed*
1203 1214 :raises ``NodeAlreadyRemovedError``: if node with same path is already
1204 1215 marked to be *removed*
1205 1216 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
1206 1217 commit
1207 1218 :raises ``NodeNotChangedError``: if node hasn't really be changed
1208 1219 """
1209 1220 for node in filenodes:
1210 1221 if node.path in (n.path for n in self.removed):
1211 1222 raise NodeAlreadyRemovedError(
1212 1223 "Node at %s is already marked as removed" % node.path)
1213 1224 try:
1214 1225 self.repository.get_commit()
1215 1226 except EmptyRepositoryError:
1216 1227 raise EmptyRepositoryError(
1217 1228 "Nothing to change - try to *add* new nodes rather than "
1218 1229 "changing them")
1219 1230 for node in filenodes:
1220 1231 if node.path in (n.path for n in self.changed):
1221 1232 raise NodeAlreadyChangedError(
1222 1233 "Node at '%s' is already marked as changed" % node.path)
1223 1234 self.changed.append(node)
1224 1235
1225 1236 def remove(self, *filenodes):
1226 1237 """
1227 1238 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
1228 1239 *removed* in next commit.
1229 1240
1230 1241 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
1231 1242 be *removed*
1232 1243 :raises ``NodeAlreadyChangedError``: if node has been already marked to
1233 1244 be *changed*
1234 1245 """
1235 1246 for node in filenodes:
1236 1247 if node.path in (n.path for n in self.removed):
1237 1248 raise NodeAlreadyRemovedError(
1238 1249 "Node is already marked to for removal at %s" % node.path)
1239 1250 if node.path in (n.path for n in self.changed):
1240 1251 raise NodeAlreadyChangedError(
1241 1252 "Node is already marked to be changed at %s" % node.path)
1242 1253 # We only mark node as *removed* - real removal is done by
1243 1254 # commit method
1244 1255 self.removed.append(node)
1245 1256
1246 1257 def reset(self):
1247 1258 """
1248 1259 Resets this instance to initial state (cleans ``added``, ``changed``
1249 1260 and ``removed`` lists).
1250 1261 """
1251 1262 self.added = []
1252 1263 self.changed = []
1253 1264 self.removed = []
1254 1265 self.parents = []
1255 1266
1256 1267 def get_ipaths(self):
1257 1268 """
1258 1269 Returns generator of paths from nodes marked as added, changed or
1259 1270 removed.
1260 1271 """
1261 1272 for node in itertools.chain(self.added, self.changed, self.removed):
1262 1273 yield node.path
1263 1274
1264 1275 def get_paths(self):
1265 1276 """
1266 1277 Returns list of paths from nodes marked as added, changed or removed.
1267 1278 """
1268 1279 return list(self.get_ipaths())
1269 1280
1270 1281 def check_integrity(self, parents=None):
1271 1282 """
1272 1283 Checks in-memory commit's integrity. Also, sets parents if not
1273 1284 already set.
1274 1285
1275 1286 :raises CommitError: if any error occurs (i.e.
1276 1287 ``NodeDoesNotExistError``).
1277 1288 """
1278 1289 if not self.parents:
1279 1290 parents = parents or []
1280 1291 if len(parents) == 0:
1281 1292 try:
1282 1293 parents = [self.repository.get_commit(), None]
1283 1294 except EmptyRepositoryError:
1284 1295 parents = [None, None]
1285 1296 elif len(parents) == 1:
1286 1297 parents += [None]
1287 1298 self.parents = parents
1288 1299
1289 1300 # Local parents, only if not None
1290 1301 parents = [p for p in self.parents if p]
1291 1302
1292 1303 # Check nodes marked as added
1293 1304 for p in parents:
1294 1305 for node in self.added:
1295 1306 try:
1296 1307 p.get_node(node.path)
1297 1308 except NodeDoesNotExistError:
1298 1309 pass
1299 1310 else:
1300 1311 raise NodeAlreadyExistsError(
1301 1312 "Node `%s` already exists at %s" % (node.path, p))
1302 1313
1303 1314 # Check nodes marked as changed
1304 1315 missing = set(self.changed)
1305 1316 not_changed = set(self.changed)
1306 1317 if self.changed and not parents:
1307 1318 raise NodeDoesNotExistError(str(self.changed[0].path))
1308 1319 for p in parents:
1309 1320 for node in self.changed:
1310 1321 try:
1311 1322 old = p.get_node(node.path)
1312 1323 missing.remove(node)
1313 1324 # if content actually changed, remove node from not_changed
1314 1325 if old.content != node.content:
1315 1326 not_changed.remove(node)
1316 1327 except NodeDoesNotExistError:
1317 1328 pass
1318 1329 if self.changed and missing:
1319 1330 raise NodeDoesNotExistError(
1320 1331 "Node `%s` marked as modified but missing in parents: %s"
1321 1332 % (node.path, parents))
1322 1333
1323 1334 if self.changed and not_changed:
1324 1335 raise NodeNotChangedError(
1325 1336 "Node `%s` wasn't actually changed (parents: %s)"
1326 1337 % (not_changed.pop().path, parents))
1327 1338
1328 1339 # Check nodes marked as removed
1329 1340 if self.removed and not parents:
1330 1341 raise NodeDoesNotExistError(
1331 1342 "Cannot remove node at %s as there "
1332 1343 "were no parents specified" % self.removed[0].path)
1333 1344 really_removed = set()
1334 1345 for p in parents:
1335 1346 for node in self.removed:
1336 1347 try:
1337 1348 p.get_node(node.path)
1338 1349 really_removed.add(node)
1339 1350 except CommitError:
1340 1351 pass
1341 1352 not_removed = set(self.removed) - really_removed
1342 1353 if not_removed:
1343 1354 # TODO: johbo: This code branch does not seem to be covered
1344 1355 raise NodeDoesNotExistError(
1345 1356 "Cannot remove node at %s from "
1346 1357 "following parents: %s" % (not_removed, parents))
1347 1358
1348 1359 def commit(
1349 1360 self, message, author, parents=None, branch=None, date=None,
1350 1361 **kwargs):
1351 1362 """
1352 1363 Performs in-memory commit (doesn't check workdir in any way) and
1353 1364 returns newly created :class:`BaseCommit`. Updates repository's
1354 1365 attribute `commits`.
1355 1366
1356 1367 .. note::
1357 1368
1358 1369 While overriding this method each backend's should call
1359 1370 ``self.check_integrity(parents)`` in the first place.
1360 1371
1361 1372 :param message: message of the commit
1362 1373 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
1363 1374 :param parents: single parent or sequence of parents from which commit
1364 1375 would be derived
1365 1376 :param date: ``datetime.datetime`` instance. Defaults to
1366 1377 ``datetime.datetime.now()``.
1367 1378 :param branch: branch name, as string. If none given, default backend's
1368 1379 branch would be used.
1369 1380
1370 1381 :raises ``CommitError``: if any error occurs while committing
1371 1382 """
1372 1383 raise NotImplementedError
1373 1384
1374 1385
1375 1386 class BaseInMemoryChangesetClass(type):
1376 1387
1377 1388 def __instancecheck__(self, instance):
1378 1389 return isinstance(instance, BaseInMemoryCommit)
1379 1390
1380 1391
1381 1392 class BaseInMemoryChangeset(BaseInMemoryCommit):
1382 1393
1383 1394 __metaclass__ = BaseInMemoryChangesetClass
1384 1395
1385 1396 def __new__(cls, *args, **kwargs):
1386 1397 warnings.warn(
1387 1398 "Use BaseCommit instead of BaseInMemoryCommit", DeprecationWarning)
1388 1399 return super(BaseInMemoryChangeset, cls).__new__(cls, *args, **kwargs)
1389 1400
1390 1401
1391 1402 class EmptyCommit(BaseCommit):
1392 1403 """
1393 1404 An dummy empty commit. It's possible to pass hash when creating
1394 1405 an EmptyCommit
1395 1406 """
1396 1407
1397 1408 def __init__(
1398 1409 self, commit_id='0' * 40, repo=None, alias=None, idx=-1,
1399 1410 message='', author='', date=None):
1400 1411 self._empty_commit_id = commit_id
1401 1412 # TODO: johbo: Solve idx parameter, default value does not make
1402 1413 # too much sense
1403 1414 self.idx = idx
1404 1415 self.message = message
1405 1416 self.author = author
1406 1417 self.date = date or datetime.datetime.fromtimestamp(0)
1407 1418 self.repository = repo
1408 1419 self.alias = alias
1409 1420
1410 1421 @LazyProperty
1411 1422 def raw_id(self):
1412 1423 """
1413 1424 Returns raw string identifying this commit, useful for web
1414 1425 representation.
1415 1426 """
1416 1427
1417 1428 return self._empty_commit_id
1418 1429
1419 1430 @LazyProperty
1420 1431 def branch(self):
1421 1432 if self.alias:
1422 1433 from rhodecode.lib.vcs.backends import get_backend
1423 1434 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1424 1435
1425 1436 @LazyProperty
1426 1437 def short_id(self):
1427 1438 return self.raw_id[:12]
1428 1439
1429 1440 @LazyProperty
1430 1441 def id(self):
1431 1442 return self.raw_id
1432 1443
1433 1444 def get_file_commit(self, path):
1434 1445 return self
1435 1446
1436 1447 def get_file_content(self, path):
1437 1448 return u''
1438 1449
1439 1450 def get_file_size(self, path):
1440 1451 return 0
1441 1452
1442 1453
1443 1454 class EmptyChangesetClass(type):
1444 1455
1445 1456 def __instancecheck__(self, instance):
1446 1457 return isinstance(instance, EmptyCommit)
1447 1458
1448 1459
1449 1460 class EmptyChangeset(EmptyCommit):
1450 1461
1451 1462 __metaclass__ = EmptyChangesetClass
1452 1463
1453 1464 def __new__(cls, *args, **kwargs):
1454 1465 warnings.warn(
1455 1466 "Use EmptyCommit instead of EmptyChangeset", DeprecationWarning)
1456 1467 return super(EmptyCommit, cls).__new__(cls, *args, **kwargs)
1457 1468
1458 1469 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
1459 1470 alias=None, revision=-1, message='', author='', date=None):
1460 1471 if requested_revision is not None:
1461 1472 warnings.warn(
1462 1473 "Parameter requested_revision not supported anymore",
1463 1474 DeprecationWarning)
1464 1475 super(EmptyChangeset, self).__init__(
1465 1476 commit_id=cs, repo=repo, alias=alias, idx=revision,
1466 1477 message=message, author=author, date=date)
1467 1478
1468 1479 @property
1469 1480 def revision(self):
1470 1481 warnings.warn("Use idx instead", DeprecationWarning)
1471 1482 return self.idx
1472 1483
1473 1484 @revision.setter
1474 1485 def revision(self, value):
1475 1486 warnings.warn("Use idx instead", DeprecationWarning)
1476 1487 self.idx = value
1477 1488
1478 1489
1479 1490 class EmptyRepository(BaseRepository):
1480 1491 def __init__(self, repo_path=None, config=None, create=False, **kwargs):
1481 1492 pass
1482 1493
1483 1494 def get_diff(self, *args, **kwargs):
1484 1495 from rhodecode.lib.vcs.backends.git.diff import GitDiff
1485 1496 return GitDiff('')
1486 1497
1487 1498
1488 1499 class CollectionGenerator(object):
1489 1500
1490 1501 def __init__(self, repo, commit_ids, collection_size=None, pre_load=None):
1491 1502 self.repo = repo
1492 1503 self.commit_ids = commit_ids
1493 1504 # TODO: (oliver) this isn't currently hooked up
1494 1505 self.collection_size = None
1495 1506 self.pre_load = pre_load
1496 1507
1497 1508 def __len__(self):
1498 1509 if self.collection_size is not None:
1499 1510 return self.collection_size
1500 1511 return self.commit_ids.__len__()
1501 1512
1502 1513 def __iter__(self):
1503 1514 for commit_id in self.commit_ids:
1504 1515 # TODO: johbo: Mercurial passes in commit indices or commit ids
1505 1516 yield self._commit_factory(commit_id)
1506 1517
1507 1518 def _commit_factory(self, commit_id):
1508 1519 """
1509 1520 Allows backends to override the way commits are generated.
1510 1521 """
1511 1522 return self.repo.get_commit(commit_id=commit_id,
1512 1523 pre_load=self.pre_load)
1513 1524
1514 1525 def __getslice__(self, i, j):
1515 1526 """
1516 1527 Returns an iterator of sliced repository
1517 1528 """
1518 1529 commit_ids = self.commit_ids[i:j]
1519 1530 return self.__class__(
1520 1531 self.repo, commit_ids, pre_load=self.pre_load)
1521 1532
1522 1533 def __repr__(self):
1523 1534 return '<CollectionGenerator[len:%s]>' % (self.__len__())
1524 1535
1525 1536
1526 1537 class Config(object):
1527 1538 """
1528 1539 Represents the configuration for a repository.
1529 1540
1530 1541 The API is inspired by :class:`ConfigParser.ConfigParser` from the
1531 1542 standard library. It implements only the needed subset.
1532 1543 """
1533 1544
1534 1545 def __init__(self):
1535 1546 self._values = {}
1536 1547
1537 1548 def copy(self):
1538 1549 clone = Config()
1539 1550 for section, values in self._values.items():
1540 1551 clone._values[section] = values.copy()
1541 1552 return clone
1542 1553
1543 1554 def __repr__(self):
1544 1555 return '<Config(%s sections) at %s>' % (
1545 1556 len(self._values), hex(id(self)))
1546 1557
1547 1558 def items(self, section):
1548 1559 return self._values.get(section, {}).iteritems()
1549 1560
1550 1561 def get(self, section, option):
1551 1562 return self._values.get(section, {}).get(option)
1552 1563
1553 1564 def set(self, section, option, value):
1554 1565 section_values = self._values.setdefault(section, {})
1555 1566 section_values[option] = value
1556 1567
1557 1568 def clear_section(self, section):
1558 1569 self._values[section] = {}
1559 1570
1560 1571 def serialize(self):
1561 1572 """
1562 1573 Creates a list of three tuples (section, key, value) representing
1563 1574 this config object.
1564 1575 """
1565 1576 items = []
1566 1577 for section in self._values:
1567 1578 for option, value in self._values[section].items():
1568 1579 items.append(
1569 1580 (safe_str(section), safe_str(option), safe_str(value)))
1570 1581 return items
1571 1582
1572 1583
1573 1584 class Diff(object):
1574 1585 """
1575 1586 Represents a diff result from a repository backend.
1576 1587
1577 1588 Subclasses have to provide a backend specific value for
1578 1589 :attr:`_header_re` and :attr:`_meta_re`.
1579 1590 """
1580 1591 _meta_re = None
1581 1592 _header_re = None
1582 1593
1583 1594 def __init__(self, raw_diff):
1584 1595 self.raw = raw_diff
1585 1596
1586 1597 def chunks(self):
1587 1598 """
1588 1599 split the diff in chunks of separate --git a/file b/file chunks
1589 1600 to make diffs consistent we must prepend with \n, and make sure
1590 1601 we can detect last chunk as this was also has special rule
1591 1602 """
1592 1603
1593 1604 diff_parts = ('\n' + self.raw).split('\ndiff --git')
1594 1605 header = diff_parts[0]
1595 1606
1596 1607 if self._meta_re:
1597 1608 match = self._meta_re.match(header)
1598 1609
1599 1610 chunks = diff_parts[1:]
1600 1611 total_chunks = len(chunks)
1601 1612
1602 1613 return (
1603 1614 DiffChunk(chunk, self, cur_chunk == total_chunks)
1604 1615 for cur_chunk, chunk in enumerate(chunks, start=1))
1605 1616
1606 1617
1607 1618 class DiffChunk(object):
1608 1619
1609 1620 def __init__(self, chunk, diff, last_chunk):
1610 1621 self._diff = diff
1611 1622
1612 1623 # since we split by \ndiff --git that part is lost from original diff
1613 1624 # we need to re-apply it at the end, EXCEPT ! if it's last chunk
1614 1625 if not last_chunk:
1615 1626 chunk += '\n'
1616 1627
1617 1628 match = self._diff._header_re.match(chunk)
1618 1629 self.header = match.groupdict()
1619 1630 self.diff = chunk[match.end():]
1620 1631 self.raw = chunk
1632
1633
1634 class BasePathPermissionChecker(object):
1635
1636 def __init__(self, username, has_full_access = False):
1637 self.username = username
1638 self.has_full_access = has_full_access
1639
1640 def has_access(self, path):
1641 raise NotImplemented()
@@ -1,693 +1,695 b''
1 1 <%namespace name="commentblock" file="/changeset/changeset_file_comment.mako"/>
2 2
3 3 <%def name="diff_line_anchor(filename, line, type)"><%
4 4 return '%s_%s_%i' % (h.safeid(filename), type, line)
5 5 %></%def>
6 6
7 7 <%def name="action_class(action)">
8 8 <%
9 9 return {
10 10 '-': 'cb-deletion',
11 11 '+': 'cb-addition',
12 12 ' ': 'cb-context',
13 13 }.get(action, 'cb-empty')
14 14 %>
15 15 </%def>
16 16
17 17 <%def name="op_class(op_id)">
18 18 <%
19 19 return {
20 20 DEL_FILENODE: 'deletion', # file deleted
21 21 BIN_FILENODE: 'warning' # binary diff hidden
22 22 }.get(op_id, 'addition')
23 23 %>
24 24 </%def>
25 25
26 26
27 27
28 28 <%def name="render_diffset(diffset, commit=None,
29 29
30 30 # collapse all file diff entries when there are more than this amount of files in the diff
31 31 collapse_when_files_over=20,
32 32
33 33 # collapse lines in the diff when more than this amount of lines changed in the file diff
34 34 lines_changed_limit=500,
35 35
36 36 # add a ruler at to the output
37 37 ruler_at_chars=0,
38 38
39 39 # show inline comments
40 40 use_comments=False,
41 41
42 42 # disable new comments
43 43 disable_new_comments=False,
44 44
45 45 # special file-comments that were deleted in previous versions
46 46 # it's used for showing outdated comments for deleted files in a PR
47 47 deleted_files_comments=None
48 48
49 49 )">
50 50
51 51 %if use_comments:
52 52 <div id="cb-comments-inline-container-template" class="js-template">
53 53 ${inline_comments_container([])}
54 54 </div>
55 55 <div class="js-template" id="cb-comment-inline-form-template">
56 56 <div class="comment-inline-form ac">
57 57
58 58 %if c.rhodecode_user.username != h.DEFAULT_USER:
59 59 ## render template for inline comments
60 60 ${commentblock.comment_form(form_type='inline')}
61 61 %else:
62 62 ${h.form('', class_='inline-form comment-form-login', method='get')}
63 63 <div class="pull-left">
64 64 <div class="comment-help pull-right">
65 65 ${_('You need to be logged in to leave comments.')} <a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
66 66 </div>
67 67 </div>
68 68 <div class="comment-button pull-right">
69 69 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
70 70 ${_('Cancel')}
71 71 </button>
72 72 </div>
73 73 <div class="clearfix"></div>
74 74 ${h.end_form()}
75 75 %endif
76 76 </div>
77 77 </div>
78 78
79 79 %endif
80 80 <%
81 81 collapse_all = len(diffset.files) > collapse_when_files_over
82 82 %>
83 83
84 84 %if c.diffmode == 'sideside':
85 85 <style>
86 86 .wrapper {
87 87 max-width: 1600px !important;
88 88 }
89 89 </style>
90 90 %endif
91 91
92 92 %if ruler_at_chars:
93 93 <style>
94 94 .diff table.cb .cb-content:after {
95 95 content: "";
96 96 border-left: 1px solid blue;
97 97 position: absolute;
98 98 top: 0;
99 99 height: 18px;
100 100 opacity: .2;
101 101 z-index: 10;
102 102 //## +5 to account for diff action (+/-)
103 103 left: ${ruler_at_chars + 5}ch;
104 104 </style>
105 105 %endif
106 106
107 107 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
108 108 <div class="diffset-heading ${diffset.limited_diff and 'diffset-heading-warning' or ''}">
109 109 %if commit:
110 110 <div class="pull-right">
111 111 <a class="btn tooltip" title="${h.tooltip(_('Browse Files at revision {}').format(commit.raw_id))}" href="${h.route_path('repo_files',repo_name=diffset.repo_name, commit_id=commit.raw_id, f_path='')}">
112 112 ${_('Browse Files')}
113 113 </a>
114 114 </div>
115 115 %endif
116 116 <h2 class="clearinner">
117 117 %if commit:
118 118 <a class="tooltip revision" title="${h.tooltip(commit.message)}" href="${h.route_path('repo_commit',repo_name=c.repo_name,commit_id=commit.raw_id)}">${'r%s:%s' % (commit.revision,h.short_id(commit.raw_id))}</a> -
119 119 ${h.age_component(commit.date)} -
120 120 %endif
121 121
122 122 %if diffset.limited_diff:
123 123 ${_('The requested commit is too big and content was truncated.')}
124 124
125 125 ${_ungettext('%(num)s file changed.', '%(num)s files changed.', diffset.changed_files) % {'num': diffset.changed_files}}
126 126 <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
127 127 %else:
128 128 ${_ungettext('%(num)s file changed: %(linesadd)s inserted, ''%(linesdel)s deleted',
129 129 '%(num)s files changed: %(linesadd)s inserted, %(linesdel)s deleted', diffset.changed_files) % {'num': diffset.changed_files, 'linesadd': diffset.lines_added, 'linesdel': diffset.lines_deleted}}
130 130 %endif
131 131
132 132 </h2>
133 133 </div>
134 134
135 %if not diffset.files:
135 %if diffset.has_hidden_changes:
136 <p class="empty_data">${_('Some changes may be hidden')}</p>
137 %elif not diffset.files:
136 138 <p class="empty_data">${_('No files')}</p>
137 139 %endif
138 140
139 141 <div class="filediffs">
140 142 ## initial value could be marked as False later on
141 143 <% over_lines_changed_limit = False %>
142 144 %for i, filediff in enumerate(diffset.files):
143 145
144 146 <%
145 147 lines_changed = filediff.patch['stats']['added'] + filediff.patch['stats']['deleted']
146 148 over_lines_changed_limit = lines_changed > lines_changed_limit
147 149 %>
148 150 <input ${collapse_all and 'checked' or ''} class="filediff-collapse-state" id="filediff-collapse-${id(filediff)}" type="checkbox">
149 151 <div
150 152 class="filediff"
151 153 data-f-path="${filediff.patch['filename']}"
152 154 id="a_${h.FID('', filediff.patch['filename'])}">
153 155 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
154 156 <div class="filediff-collapse-indicator"></div>
155 157 ${diff_ops(filediff)}
156 158 </label>
157 159 ${diff_menu(filediff, use_comments=use_comments)}
158 160 <table class="cb cb-diff-${c.diffmode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
159 161 %if not filediff.hunks:
160 162 %for op_id, op_text in filediff.patch['stats']['ops'].items():
161 163 <tr>
162 164 <td class="cb-text cb-${op_class(op_id)}" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
163 165 %if op_id == DEL_FILENODE:
164 166 ${_('File was deleted')}
165 167 %elif op_id == BIN_FILENODE:
166 168 ${_('Binary file hidden')}
167 169 %else:
168 170 ${op_text}
169 171 %endif
170 172 </td>
171 173 </tr>
172 174 %endfor
173 175 %endif
174 176 %if filediff.limited_diff:
175 177 <tr class="cb-warning cb-collapser">
176 178 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
177 179 ${_('The requested commit is too big and content was truncated.')} <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
178 180 </td>
179 181 </tr>
180 182 %else:
181 183 %if over_lines_changed_limit:
182 184 <tr class="cb-warning cb-collapser">
183 185 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
184 186 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
185 187 <a href="#" class="cb-expand"
186 188 onclick="$(this).closest('table').removeClass('cb-collapsed'); return false;">${_('Show them')}
187 189 </a>
188 190 <a href="#" class="cb-collapse"
189 191 onclick="$(this).closest('table').addClass('cb-collapsed'); return false;">${_('Hide them')}
190 192 </a>
191 193 </td>
192 194 </tr>
193 195 %endif
194 196 %endif
195 197
196 198 %for hunk in filediff.hunks:
197 199 <tr class="cb-hunk">
198 200 <td ${c.diffmode == 'unified' and 'colspan=3' or ''}>
199 201 ## TODO: dan: add ajax loading of more context here
200 202 ## <a href="#">
201 203 <i class="icon-more"></i>
202 204 ## </a>
203 205 </td>
204 206 <td ${c.diffmode == 'sideside' and 'colspan=5' or ''}>
205 207 @@
206 208 -${hunk.source_start},${hunk.source_length}
207 209 +${hunk.target_start},${hunk.target_length}
208 210 ${hunk.section_header}
209 211 </td>
210 212 </tr>
211 213 %if c.diffmode == 'unified':
212 214 ${render_hunk_lines_unified(hunk, use_comments=use_comments)}
213 215 %elif c.diffmode == 'sideside':
214 216 ${render_hunk_lines_sideside(hunk, use_comments=use_comments)}
215 217 %else:
216 218 <tr class="cb-line">
217 219 <td>unknown diff mode</td>
218 220 </tr>
219 221 %endif
220 222 %endfor
221 223
222 224 ## outdated comments that do not fit into currently displayed lines
223 225 % for lineno, comments in filediff.left_comments.items():
224 226
225 227 %if c.diffmode == 'unified':
226 228 <tr class="cb-line">
227 229 <td class="cb-data cb-context"></td>
228 230 <td class="cb-lineno cb-context"></td>
229 231 <td class="cb-lineno cb-context"></td>
230 232 <td class="cb-content cb-context">
231 233 ${inline_comments_container(comments)}
232 234 </td>
233 235 </tr>
234 236 %elif c.diffmode == 'sideside':
235 237 <tr class="cb-line">
236 238 <td class="cb-data cb-context"></td>
237 239 <td class="cb-lineno cb-context"></td>
238 240 <td class="cb-content cb-context">
239 241 % if lineno.startswith('o'):
240 242 ${inline_comments_container(comments)}
241 243 % endif
242 244 </td>
243 245
244 246 <td class="cb-data cb-context"></td>
245 247 <td class="cb-lineno cb-context"></td>
246 248 <td class="cb-content cb-context">
247 249 % if lineno.startswith('n'):
248 250 ${inline_comments_container(comments)}
249 251 % endif
250 252 </td>
251 253 </tr>
252 254 %endif
253 255
254 256 % endfor
255 257
256 258 </table>
257 259 </div>
258 260 %endfor
259 261
260 262 ## outdated comments that are made for a file that has been deleted
261 263 % for filename, comments_dict in (deleted_files_comments or {}).items():
262 264
263 265 <div class="filediffs filediff-outdated" style="display: none">
264 266 <input ${collapse_all and 'checked' or ''} class="filediff-collapse-state" id="filediff-collapse-${id(filename)}" type="checkbox">
265 267 <div class="filediff" data-f-path="${filename}" id="a_${h.FID('', filename)}">
266 268 <label for="filediff-collapse-${id(filename)}" class="filediff-heading">
267 269 <div class="filediff-collapse-indicator"></div>
268 270 <span class="pill">
269 271 ## file was deleted
270 272 <strong>${filename}</strong>
271 273 </span>
272 274 <span class="pill-group" style="float: left">
273 275 ## file op, doesn't need translation
274 276 <span class="pill" op="removed">removed in this version</span>
275 277 </span>
276 278 <a class="pill filediff-anchor" href="#a_${h.FID('', filename)}">ΒΆ</a>
277 279 <span class="pill-group" style="float: right">
278 280 <span class="pill" op="deleted">-${comments_dict['stats']}</span>
279 281 </span>
280 282 </label>
281 283
282 284 <table class="cb cb-diff-${c.diffmode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
283 285 <tr>
284 286 % if c.diffmode == 'unified':
285 287 <td></td>
286 288 %endif
287 289
288 290 <td></td>
289 291 <td class="cb-text cb-${op_class(BIN_FILENODE)}" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=5'}>
290 292 ${_('File was deleted in this version, and outdated comments were made on it')}
291 293 </td>
292 294 </tr>
293 295 %if c.diffmode == 'unified':
294 296 <tr class="cb-line">
295 297 <td class="cb-data cb-context"></td>
296 298 <td class="cb-lineno cb-context"></td>
297 299 <td class="cb-lineno cb-context"></td>
298 300 <td class="cb-content cb-context">
299 301 ${inline_comments_container(comments_dict['comments'])}
300 302 </td>
301 303 </tr>
302 304 %elif c.diffmode == 'sideside':
303 305 <tr class="cb-line">
304 306 <td class="cb-data cb-context"></td>
305 307 <td class="cb-lineno cb-context"></td>
306 308 <td class="cb-content cb-context"></td>
307 309
308 310 <td class="cb-data cb-context"></td>
309 311 <td class="cb-lineno cb-context"></td>
310 312 <td class="cb-content cb-context">
311 313 ${inline_comments_container(comments_dict['comments'])}
312 314 </td>
313 315 </tr>
314 316 %endif
315 317 </table>
316 318 </div>
317 319 </div>
318 320 % endfor
319 321
320 322 </div>
321 323 </div>
322 324 </%def>
323 325
324 326 <%def name="diff_ops(filediff)">
325 327 <%
326 328 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
327 329 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
328 330 %>
329 331 <span class="pill">
330 332 %if filediff.source_file_path and filediff.target_file_path:
331 333 %if filediff.source_file_path != filediff.target_file_path:
332 334 ## file was renamed, or copied
333 335 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
334 336 <strong>${filediff.target_file_path}</strong> β¬… <del>${filediff.source_file_path}</del>
335 337 <% final_path = filediff.target_file_path %>
336 338 %elif COPIED_FILENODE in filediff.patch['stats']['ops']:
337 339 <strong>${filediff.target_file_path}</strong> β¬… ${filediff.source_file_path}
338 340 <% final_path = filediff.target_file_path %>
339 341 %endif
340 342 %else:
341 343 ## file was modified
342 344 <strong>${filediff.source_file_path}</strong>
343 345 <% final_path = filediff.source_file_path %>
344 346 %endif
345 347 %else:
346 348 %if filediff.source_file_path:
347 349 ## file was deleted
348 350 <strong>${filediff.source_file_path}</strong>
349 351 <% final_path = filediff.source_file_path %>
350 352 %else:
351 353 ## file was added
352 354 <strong>${filediff.target_file_path}</strong>
353 355 <% final_path = filediff.target_file_path %>
354 356 %endif
355 357 %endif
356 358 <i style="color: #aaa" class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${final_path}" title="${_('Copy the full path')}" onclick="return false;"></i>
357 359 </span>
358 360 <span class="pill-group" style="float: left">
359 361 %if filediff.limited_diff:
360 362 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
361 363 %endif
362 364
363 365 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
364 366 <span class="pill" op="renamed">renamed</span>
365 367 %endif
366 368
367 369 %if COPIED_FILENODE in filediff.patch['stats']['ops']:
368 370 <span class="pill" op="copied">copied</span>
369 371 %endif
370 372
371 373 %if NEW_FILENODE in filediff.patch['stats']['ops']:
372 374 <span class="pill" op="created">created</span>
373 375 %if filediff['target_mode'].startswith('120'):
374 376 <span class="pill" op="symlink">symlink</span>
375 377 %else:
376 378 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
377 379 %endif
378 380 %endif
379 381
380 382 %if DEL_FILENODE in filediff.patch['stats']['ops']:
381 383 <span class="pill" op="removed">removed</span>
382 384 %endif
383 385
384 386 %if CHMOD_FILENODE in filediff.patch['stats']['ops']:
385 387 <span class="pill" op="mode">
386 388 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
387 389 </span>
388 390 %endif
389 391 </span>
390 392
391 393 <a class="pill filediff-anchor" href="#a_${h.FID('', filediff.patch['filename'])}">ΒΆ</a>
392 394
393 395 <span class="pill-group" style="float: right">
394 396 %if BIN_FILENODE in filediff.patch['stats']['ops']:
395 397 <span class="pill" op="binary">binary</span>
396 398 %if MOD_FILENODE in filediff.patch['stats']['ops']:
397 399 <span class="pill" op="modified">modified</span>
398 400 %endif
399 401 %endif
400 402 %if filediff.patch['stats']['added']:
401 403 <span class="pill" op="added">+${filediff.patch['stats']['added']}</span>
402 404 %endif
403 405 %if filediff.patch['stats']['deleted']:
404 406 <span class="pill" op="deleted">-${filediff.patch['stats']['deleted']}</span>
405 407 %endif
406 408 </span>
407 409
408 410 </%def>
409 411
410 412 <%def name="nice_mode(filemode)">
411 413 ${filemode.startswith('100') and filemode[3:] or filemode}
412 414 </%def>
413 415
414 416 <%def name="diff_menu(filediff, use_comments=False)">
415 417 <div class="filediff-menu">
416 418 %if filediff.diffset.source_ref:
417 419 %if filediff.operation in ['D', 'M']:
418 420 <a
419 421 class="tooltip"
420 422 href="${h.route_path('repo_files',repo_name=filediff.diffset.repo_name,commit_id=filediff.diffset.source_ref,f_path=filediff.source_file_path)}"
421 423 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
422 424 >
423 425 ${_('Show file before')}
424 426 </a> |
425 427 %else:
426 428 <span
427 429 class="tooltip"
428 430 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
429 431 >
430 432 ${_('Show file before')}
431 433 </span> |
432 434 %endif
433 435 %if filediff.operation in ['A', 'M']:
434 436 <a
435 437 class="tooltip"
436 438 href="${h.route_path('repo_files',repo_name=filediff.diffset.source_repo_name,commit_id=filediff.diffset.target_ref,f_path=filediff.target_file_path)}"
437 439 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
438 440 >
439 441 ${_('Show file after')}
440 442 </a> |
441 443 %else:
442 444 <span
443 445 class="tooltip"
444 446 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
445 447 >
446 448 ${_('Show file after')}
447 449 </span> |
448 450 %endif
449 451 <a
450 452 class="tooltip"
451 453 title="${h.tooltip(_('Raw diff'))}"
452 454 href="${h.route_path('repo_files_diff',repo_name=filediff.diffset.repo_name,f_path=filediff.target_file_path, _query=dict(diff2=filediff.diffset.target_ref,diff1=filediff.diffset.source_ref,diff='raw'))}"
453 455 >
454 456 ${_('Raw diff')}
455 457 </a> |
456 458 <a
457 459 class="tooltip"
458 460 title="${h.tooltip(_('Download diff'))}"
459 461 href="${h.route_path('repo_files_diff',repo_name=filediff.diffset.repo_name,f_path=filediff.target_file_path, _query=dict(diff2=filediff.diffset.target_ref,diff1=filediff.diffset.source_ref,diff='download'))}"
460 462 >
461 463 ${_('Download diff')}
462 464 </a>
463 465 % if use_comments:
464 466 |
465 467 % endif
466 468
467 469 ## TODO: dan: refactor ignorews_url and context_url into the diff renderer same as diffmode=unified/sideside. Also use ajax to load more context (by clicking hunks)
468 470 %if hasattr(c, 'ignorews_url'):
469 471 ${c.ignorews_url(request, h.FID('', filediff.patch['filename']))}
470 472 %endif
471 473 %if hasattr(c, 'context_url'):
472 474 ${c.context_url(request, h.FID('', filediff.patch['filename']))}
473 475 %endif
474 476
475 477 %if use_comments:
476 478 <a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
477 479 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
478 480 </a>
479 481 %endif
480 482 %endif
481 483 </div>
482 484 </%def>
483 485
484 486
485 487 <%def name="inline_comments_container(comments)">
486 488 <div class="inline-comments">
487 489 %for comment in comments:
488 490 ${commentblock.comment_block(comment, inline=True)}
489 491 %endfor
490 492
491 493 % if comments and comments[-1].outdated:
492 494 <span class="btn btn-secondary cb-comment-add-button comment-outdated}"
493 495 style="display: none;}">
494 496 ${_('Add another comment')}
495 497 </span>
496 498 % else:
497 499 <span onclick="return Rhodecode.comments.createComment(this)"
498 500 class="btn btn-secondary cb-comment-add-button">
499 501 ${_('Add another comment')}
500 502 </span>
501 503 % endif
502 504
503 505 </div>
504 506 </%def>
505 507
506 508
507 509 <%def name="render_hunk_lines_sideside(hunk, use_comments=False)">
508 510 %for i, line in enumerate(hunk.sideside):
509 511 <%
510 512 old_line_anchor, new_line_anchor = None, None
511 513 if line.original.lineno:
512 514 old_line_anchor = diff_line_anchor(hunk.source_file_path, line.original.lineno, 'o')
513 515 if line.modified.lineno:
514 516 new_line_anchor = diff_line_anchor(hunk.target_file_path, line.modified.lineno, 'n')
515 517 %>
516 518
517 519 <tr class="cb-line">
518 520 <td class="cb-data ${action_class(line.original.action)}"
519 521 data-line-number="${line.original.lineno}"
520 522 >
521 523 <div>
522 524 %if line.original.comments:
523 525 <% has_outdated = any([x.outdated for x in line.original.comments]) %>
524 526 % if has_outdated:
525 527 <i title="${_('comments including outdated')}:${len(line.original.comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
526 528 % else:
527 529 <i title="${_('comments')}: ${len(line.original.comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
528 530 % endif
529 531 %endif
530 532 </div>
531 533 </td>
532 534 <td class="cb-lineno ${action_class(line.original.action)}"
533 535 data-line-number="${line.original.lineno}"
534 536 %if old_line_anchor:
535 537 id="${old_line_anchor}"
536 538 %endif
537 539 >
538 540 %if line.original.lineno:
539 541 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
540 542 %endif
541 543 </td>
542 544 <td class="cb-content ${action_class(line.original.action)}"
543 545 data-line-number="o${line.original.lineno}"
544 546 >
545 547 %if use_comments and line.original.lineno:
546 548 ${render_add_comment_button()}
547 549 %endif
548 550 <span class="cb-code">${line.original.action} ${line.original.content or '' | n}</span>
549 551 %if use_comments and line.original.lineno and line.original.comments:
550 552 ${inline_comments_container(line.original.comments)}
551 553 %endif
552 554 </td>
553 555 <td class="cb-data ${action_class(line.modified.action)}"
554 556 data-line-number="${line.modified.lineno}"
555 557 >
556 558 <div>
557 559 %if line.modified.comments:
558 560 <% has_outdated = any([x.outdated for x in line.modified.comments]) %>
559 561 % if has_outdated:
560 562 <i title="${_('comments including outdated')}:${len(line.modified.comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
561 563 % else:
562 564 <i title="${_('comments')}: ${len(line.modified.comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
563 565 % endif
564 566 %endif
565 567 </div>
566 568 </td>
567 569 <td class="cb-lineno ${action_class(line.modified.action)}"
568 570 data-line-number="${line.modified.lineno}"
569 571 %if new_line_anchor:
570 572 id="${new_line_anchor}"
571 573 %endif
572 574 >
573 575 %if line.modified.lineno:
574 576 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
575 577 %endif
576 578 </td>
577 579 <td class="cb-content ${action_class(line.modified.action)}"
578 580 data-line-number="n${line.modified.lineno}"
579 581 >
580 582 %if use_comments and line.modified.lineno:
581 583 ${render_add_comment_button()}
582 584 %endif
583 585 <span class="cb-code">${line.modified.action} ${line.modified.content or '' | n}</span>
584 586 %if use_comments and line.modified.lineno and line.modified.comments:
585 587 ${inline_comments_container(line.modified.comments)}
586 588 %endif
587 589 </td>
588 590 </tr>
589 591 %endfor
590 592 </%def>
591 593
592 594
593 595 <%def name="render_hunk_lines_unified(hunk, use_comments=False)">
594 596 %for old_line_no, new_line_no, action, content, comments in hunk.unified:
595 597 <%
596 598 old_line_anchor, new_line_anchor = None, None
597 599 if old_line_no:
598 600 old_line_anchor = diff_line_anchor(hunk.source_file_path, old_line_no, 'o')
599 601 if new_line_no:
600 602 new_line_anchor = diff_line_anchor(hunk.target_file_path, new_line_no, 'n')
601 603 %>
602 604 <tr class="cb-line">
603 605 <td class="cb-data ${action_class(action)}">
604 606 <div>
605 607 % if comments:
606 608 <% has_outdated = any([x.outdated for x in comments]) %>
607 609 % if has_outdated:
608 610 <i title="${_('comments including outdated')}:${len(comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
609 611 % else:
610 612 <i title="${_('comments')}: ${len(comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
611 613 % endif
612 614 % endif
613 615 </div>
614 616 </td>
615 617 <td class="cb-lineno ${action_class(action)}"
616 618 data-line-number="${old_line_no}"
617 619 %if old_line_anchor:
618 620 id="${old_line_anchor}"
619 621 %endif
620 622 >
621 623 %if old_line_anchor:
622 624 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
623 625 %endif
624 626 </td>
625 627 <td class="cb-lineno ${action_class(action)}"
626 628 data-line-number="${new_line_no}"
627 629 %if new_line_anchor:
628 630 id="${new_line_anchor}"
629 631 %endif
630 632 >
631 633 %if new_line_anchor:
632 634 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
633 635 %endif
634 636 </td>
635 637 <td class="cb-content ${action_class(action)}"
636 638 data-line-number="${new_line_no and 'n' or 'o'}${new_line_no or old_line_no}"
637 639 >
638 640 %if use_comments:
639 641 ${render_add_comment_button()}
640 642 %endif
641 643 <span class="cb-code">${action} ${content or '' | n}</span>
642 644 %if use_comments and comments:
643 645 ${inline_comments_container(comments)}
644 646 %endif
645 647 </td>
646 648 </tr>
647 649 %endfor
648 650 </%def>
649 651
650 652 <%def name="render_add_comment_button()">
651 653 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this)">
652 654 <span><i class="icon-comment"></i></span>
653 655 </button>
654 656 </%def>
655 657
656 658 <%def name="render_diffset_menu()">
657 659
658 660 <div class="diffset-menu clearinner">
659 661 <div class="pull-right">
660 662 <div class="btn-group">
661 663
662 664 <a
663 665 class="btn ${c.diffmode == 'sideside' and 'btn-primary'} tooltip"
664 666 title="${h.tooltip(_('View side by side'))}"
665 667 href="${h.current_route_path(request, diffmode='sideside')}">
666 668 <span>${_('Side by Side')}</span>
667 669 </a>
668 670 <a
669 671 class="btn ${c.diffmode == 'unified' and 'btn-primary'} tooltip"
670 672 title="${h.tooltip(_('View unified'))}" href="${h.current_route_path(request, diffmode='unified')}">
671 673 <span>${_('Unified')}</span>
672 674 </a>
673 675 </div>
674 676 </div>
675 677
676 678 <div class="pull-left">
677 679 <div class="btn-group">
678 680 <a
679 681 class="btn"
680 682 href="#"
681 683 onclick="$('input[class=filediff-collapse-state]').prop('checked', false); return false">${_('Expand All Files')}</a>
682 684 <a
683 685 class="btn"
684 686 href="#"
685 687 onclick="$('input[class=filediff-collapse-state]').prop('checked', true); return false">${_('Collapse All Files')}</a>
686 688 <a
687 689 class="btn"
688 690 href="#"
689 691 onclick="return Rhodecode.comments.toggleWideMode(this)">${_('Wide Mode Diff')}</a>
690 692 </div>
691 693 </div>
692 694 </div>
693 695 </%def>
@@ -1,34 +1,38 b''
1 1 ## -*- coding: utf-8 -*-
2 2
3 3 ${_('%(user)s commited on %(date)s UTC') % {
4 4 'user': h.person(commit.author),
5 5 'date': h.format_date(commit.date)
6 6 }}
7 7 <br/>
8 8 % if commit.branch:
9 9 branch: ${commit.branch} <br/>
10 10 % endif
11 11
12 12 % for bookmark in getattr(commit, 'bookmarks', []):
13 13 bookmark: ${bookmark} <br/>
14 14 % endfor
15 15
16 16 % for tag in commit.tags:
17 17 tag: ${tag} <br/>
18 18 % endfor
19 19
20 % if has_hidden_changes:
21 Has hidden changes<br/>
22 % endif
23
20 24 commit: <a href="${h.route_url('repo_commit', repo_name=c.rhodecode_db_repo.repo_name, commit_id=commit.raw_id)}">${h.show_id(commit)}</a>
21 25 <pre>
22 26 ${h.urlify_commit_message(commit.message)}
23 27
24 28 % for change in parsed_diff:
25 29 % if limited_diff:
26 30 ${_('Commit was too big and was cut off...')}
27 31 % endif
28 32 ${change['operation']} ${change['filename']} ${'(%(added)s lines added, %(removed)s lines removed)' % {'added': change['stats']['added'], 'removed': change['stats']['deleted']}}
29 33 % endfor
30 34
31 35 % if feed_include_diff:
32 ${diff_processor.as_raw()}
36 ${c.path_filter.get_raw_patch(diff_processor)}
33 37 % endif
34 38 </pre>
General Comments 0
You need to be logged in to leave comments. Login now