##// END OF EJS Templates
archives: fixed bugs with serving archives from non-ascii repos, and also deliver archives at much bigger reading blocks for faster downloads
super-admin -
r5135:aabb0aed default
parent child Browse files
Show More
@@ -1,1581 +1,1582 b''
1 1 # Copyright (C) 2011-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import itertools
20 20 import logging
21 21 import os
22 22 import collections
23 23 import urllib.request
24 24 import urllib.parse
25 25 import urllib.error
26 26 import pathlib
27 27
28 28 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
29 29
30 30 from pyramid.renderers import render
31 31 from pyramid.response import Response
32 32
33 33 import rhodecode
34 34 from rhodecode.apps._base import RepoAppView
35 35
36 36
37 37 from rhodecode.lib import diffs, helpers as h, rc_cache
38 38 from rhodecode.lib import audit_logger
39 39 from rhodecode.lib.hash_utils import sha1_safe
40 40 from rhodecode.lib.rc_cache.archive_cache import get_archival_cache_store, get_archival_config, ReentrantLock
41 from rhodecode.lib.str_utils import safe_bytes
41 from rhodecode.lib.str_utils import safe_bytes, convert_special_chars
42 42 from rhodecode.lib.view_utils import parse_path_ref
43 43 from rhodecode.lib.exceptions import NonRelativePathError
44 44 from rhodecode.lib.codeblocks import (
45 45 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
46 46 from rhodecode.lib.utils2 import convert_line_endings, detect_mode
47 47 from rhodecode.lib.type_utils import str2bool
48 48 from rhodecode.lib.str_utils import safe_str, safe_int
49 49 from rhodecode.lib.auth import (
50 50 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
51 51 from rhodecode.lib.vcs import path as vcspath
52 52 from rhodecode.lib.vcs.backends.base import EmptyCommit
53 53 from rhodecode.lib.vcs.conf import settings
54 54 from rhodecode.lib.vcs.nodes import FileNode
55 55 from rhodecode.lib.vcs.exceptions import (
56 56 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
57 57 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
58 58 NodeDoesNotExistError, CommitError, NodeError)
59 59
60 60 from rhodecode.model.scm import ScmModel
61 61 from rhodecode.model.db import Repository
62 62
63 63 log = logging.getLogger(__name__)
64 64
65 65
66 def get_archive_name(db_repo_name, commit_sha, ext, subrepos=False, path_sha='', with_hash=True):
66 def get_archive_name(db_repo_id, db_repo_name, commit_sha, ext, subrepos=False, path_sha='', with_hash=True):
67 67 # original backward compat name of archive
68 clean_name = safe_str(db_repo_name.replace('/', '_'))
68 clean_name = safe_str(convert_special_chars(db_repo_name).replace('/', '_'))
69 69
70 # e.g vcsserver-sub-1-abcfdef-archive-all.zip
71 # vcsserver-sub-0-abcfdef-COMMIT_SHA-PATH_SHA.zip
72
70 # e.g vcsserver-id-abcd-sub-1-abcfdef-archive-all.zip
71 # vcsserver-id-abcd-sub-0-abcfdef-COMMIT_SHA-PATH_SHA.zip
72 id_sha = sha1_safe(str(db_repo_id))[:4]
73 73 sub_repo = 'sub-1' if subrepos else 'sub-0'
74 74 commit = commit_sha if with_hash else 'archive'
75 75 path_marker = (path_sha if with_hash else '') or 'all'
76 archive_name = f'{clean_name}-{sub_repo}-{commit}-{path_marker}{ext}'
76 archive_name = f'{clean_name}-id-{id_sha}-{sub_repo}-{commit}-{path_marker}{ext}'
77 77
78 78 return archive_name
79 79
80 80
81 81 def get_path_sha(at_path):
82 82 return safe_str(sha1_safe(at_path)[:8])
83 83
84 84
85 85 def _get_archive_spec(fname):
86 86 log.debug('Detecting archive spec for: `%s`', fname)
87 87
88 88 fileformat = None
89 89 ext = None
90 90 content_type = None
91 91 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
92 92
93 93 if fname.endswith(extension):
94 94 fileformat = a_type
95 95 log.debug('archive is of type: %s', fileformat)
96 96 ext = extension
97 97 break
98 98
99 99 if not fileformat:
100 100 raise ValueError()
101 101
102 102 # left over part of whole fname is the commit
103 103 commit_id = fname[:-len(ext)]
104 104
105 105 return commit_id, ext, fileformat, content_type
106 106
107 107
108 108 class RepoFilesView(RepoAppView):
109 109
110 110 @staticmethod
111 111 def adjust_file_path_for_svn(f_path, repo):
112 112 """
113 113 Computes the relative path of `f_path`.
114 114
115 115 This is mainly based on prefix matching of the recognized tags and
116 116 branches in the underlying repository.
117 117 """
118 118 tags_and_branches = itertools.chain(
119 119 repo.branches.keys(),
120 120 repo.tags.keys())
121 121 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
122 122
123 123 for name in tags_and_branches:
124 124 if f_path.startswith(f'{name}/'):
125 125 f_path = vcspath.relpath(f_path, name)
126 126 break
127 127 return f_path
128 128
129 129 def load_default_context(self):
130 130 c = self._get_local_tmpl_context(include_app_defaults=True)
131 131 c.rhodecode_repo = self.rhodecode_vcs_repo
132 132 c.enable_downloads = self.db_repo.enable_downloads
133 133 return c
134 134
135 135 def _ensure_not_locked(self, commit_id='tip'):
136 136 _ = self.request.translate
137 137
138 138 repo = self.db_repo
139 139 if repo.enable_locking and repo.locked[0]:
140 140 h.flash(_('This repository has been locked by %s on %s')
141 141 % (h.person_by_id(repo.locked[0]),
142 142 h.format_date(h.time_to_datetime(repo.locked[1]))),
143 143 'warning')
144 144 files_url = h.route_path(
145 145 'repo_files:default_path',
146 146 repo_name=self.db_repo_name, commit_id=commit_id)
147 147 raise HTTPFound(files_url)
148 148
149 149 def forbid_non_head(self, is_head, f_path, commit_id='tip', json_mode=False):
150 150 _ = self.request.translate
151 151
152 152 if not is_head:
153 153 message = _('Cannot modify file. '
154 154 'Given commit `{}` is not head of a branch.').format(commit_id)
155 155 h.flash(message, category='warning')
156 156
157 157 if json_mode:
158 158 return message
159 159
160 160 files_url = h.route_path(
161 161 'repo_files', repo_name=self.db_repo_name, commit_id=commit_id,
162 162 f_path=f_path)
163 163 raise HTTPFound(files_url)
164 164
165 165 def check_branch_permission(self, branch_name, commit_id='tip', json_mode=False):
166 166 _ = self.request.translate
167 167
168 168 rule, branch_perm = self._rhodecode_user.get_rule_and_branch_permission(
169 169 self.db_repo_name, branch_name)
170 170 if branch_perm and branch_perm not in ['branch.push', 'branch.push_force']:
171 171 message = _('Branch `{}` changes forbidden by rule {}.').format(
172 172 h.escape(branch_name), h.escape(rule))
173 173 h.flash(message, 'warning')
174 174
175 175 if json_mode:
176 176 return message
177 177
178 178 files_url = h.route_path(
179 179 'repo_files:default_path', repo_name=self.db_repo_name, commit_id=commit_id)
180 180
181 181 raise HTTPFound(files_url)
182 182
183 183 def _get_commit_and_path(self):
184 184 default_commit_id = self.db_repo.landing_ref_name
185 185 default_f_path = '/'
186 186
187 187 commit_id = self.request.matchdict.get(
188 188 'commit_id', default_commit_id)
189 189 f_path = self._get_f_path(self.request.matchdict, default_f_path)
190 190 return commit_id, f_path
191 191
192 192 def _get_default_encoding(self, c):
193 193 enc_list = getattr(c, 'default_encodings', [])
194 194 return enc_list[0] if enc_list else 'UTF-8'
195 195
196 196 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
197 197 """
198 198 This is a safe way to get commit. If an error occurs it redirects to
199 199 tip with proper message
200 200
201 201 :param commit_id: id of commit to fetch
202 202 :param redirect_after: toggle redirection
203 203 """
204 204 _ = self.request.translate
205 205
206 206 try:
207 207 return self.rhodecode_vcs_repo.get_commit(commit_id)
208 208 except EmptyRepositoryError:
209 209 if not redirect_after:
210 210 return None
211 211
212 212 add_new = upload_new = ""
213 213 if h.HasRepoPermissionAny(
214 214 'repository.write', 'repository.admin')(self.db_repo_name):
215 215 _url = h.route_path(
216 216 'repo_files_add_file',
217 217 repo_name=self.db_repo_name, commit_id=0, f_path='')
218 218 add_new = h.link_to(
219 219 _('add a new file'), _url, class_="alert-link")
220 220
221 221 _url_upld = h.route_path(
222 222 'repo_files_upload_file',
223 223 repo_name=self.db_repo_name, commit_id=0, f_path='')
224 224 upload_new = h.link_to(
225 225 _('upload a new file'), _url_upld, class_="alert-link")
226 226
227 227 h.flash(h.literal(
228 228 _('There are no files yet. Click here to %s or %s.') % (add_new, upload_new)), category='warning')
229 229 raise HTTPFound(
230 230 h.route_path('repo_summary', repo_name=self.db_repo_name))
231 231
232 232 except (CommitDoesNotExistError, LookupError) as e:
233 233 msg = _('No such commit exists for this repository. Commit: {}').format(commit_id)
234 234 h.flash(msg, category='error')
235 235 raise HTTPNotFound()
236 236 except RepositoryError as e:
237 237 h.flash(h.escape(safe_str(e)), category='error')
238 238 raise HTTPNotFound()
239 239
240 240 def _get_filenode_or_redirect(self, commit_obj, path, pre_load=None):
241 241 """
242 242 Returns file_node, if error occurs or given path is directory,
243 243 it'll redirect to top level path
244 244 """
245 245 _ = self.request.translate
246 246
247 247 try:
248 248 file_node = commit_obj.get_node(path, pre_load=pre_load)
249 249 if file_node.is_dir():
250 250 raise RepositoryError('The given path is a directory')
251 251 except CommitDoesNotExistError:
252 252 log.exception('No such commit exists for this repository')
253 253 h.flash(_('No such commit exists for this repository'), category='error')
254 254 raise HTTPNotFound()
255 255 except RepositoryError as e:
256 256 log.warning('Repository error while fetching filenode `%s`. Err:%s', path, e)
257 257 h.flash(h.escape(safe_str(e)), category='error')
258 258 raise HTTPNotFound()
259 259
260 260 return file_node
261 261
262 262 def _is_valid_head(self, commit_id, repo, landing_ref):
263 263 branch_name = sha_commit_id = ''
264 264 is_head = False
265 265 log.debug('Checking if commit_id `%s` is a head for %s.', commit_id, repo)
266 266
267 267 for _branch_name, branch_commit_id in repo.branches.items():
268 268 # simple case we pass in branch name, it's a HEAD
269 269 if commit_id == _branch_name:
270 270 is_head = True
271 271 branch_name = _branch_name
272 272 sha_commit_id = branch_commit_id
273 273 break
274 274 # case when we pass in full sha commit_id, which is a head
275 275 elif commit_id == branch_commit_id:
276 276 is_head = True
277 277 branch_name = _branch_name
278 278 sha_commit_id = branch_commit_id
279 279 break
280 280
281 281 if h.is_svn(repo) and not repo.is_empty():
282 282 # Note: Subversion only has one head.
283 283 if commit_id == repo.get_commit(commit_idx=-1).raw_id:
284 284 is_head = True
285 285 return branch_name, sha_commit_id, is_head
286 286
287 287 # checked branches, means we only need to try to get the branch/commit_sha
288 288 if repo.is_empty():
289 289 is_head = True
290 290 branch_name = landing_ref
291 291 sha_commit_id = EmptyCommit().raw_id
292 292 else:
293 293 commit = repo.get_commit(commit_id=commit_id)
294 294 if commit:
295 295 branch_name = commit.branch
296 296 sha_commit_id = commit.raw_id
297 297
298 298 return branch_name, sha_commit_id, is_head
299 299
300 300 def _get_tree_at_commit(self, c, commit_id, f_path, full_load=False, at_rev=None):
301 301
302 302 repo_id = self.db_repo.repo_id
303 303 force_recache = self.get_recache_flag()
304 304
305 305 cache_seconds = safe_int(
306 306 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
307 307 cache_on = not force_recache and cache_seconds > 0
308 308 log.debug(
309 309 'Computing FILE TREE for repo_id %s commit_id `%s` and path `%s`'
310 310 'with caching: %s[TTL: %ss]' % (
311 311 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
312 312
313 313 cache_namespace_uid = f'repo.{rc_cache.FILE_TREE_CACHE_VER}.{repo_id}'
314 314 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
315 315
316 316 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, condition=cache_on)
317 317 def compute_file_tree(_name_hash, _repo_id, _commit_id, _f_path, _full_load, _at_rev):
318 318 log.debug('Generating cached file tree at for repo_id: %s, %s, %s',
319 319 _repo_id, _commit_id, _f_path)
320 320
321 321 c.full_load = _full_load
322 322 return render(
323 323 'rhodecode:templates/files/files_browser_tree.mako',
324 324 self._get_template_context(c), self.request, _at_rev)
325 325
326 326 return compute_file_tree(
327 327 self.db_repo.repo_name_hash, self.db_repo.repo_id, commit_id, f_path, full_load, at_rev)
328 328
329 329 def create_pure_path(self, *parts):
330 330 # Split paths and sanitize them, removing any ../ etc
331 331 sanitized_path = [
332 332 x for x in pathlib.PurePath(*parts).parts
333 333 if x not in ['.', '..']]
334 334
335 335 pure_path = pathlib.PurePath(*sanitized_path)
336 336 return pure_path
337 337
338 338 def _is_lf_enabled(self, target_repo):
339 339 lf_enabled = False
340 340
341 341 lf_key_for_vcs_map = {
342 342 'hg': 'extensions_largefiles',
343 343 'git': 'vcs_git_lfs_enabled'
344 344 }
345 345
346 346 lf_key_for_vcs = lf_key_for_vcs_map.get(target_repo.repo_type)
347 347
348 348 if lf_key_for_vcs:
349 349 lf_enabled = self._get_repo_setting(target_repo, lf_key_for_vcs)
350 350
351 351 return lf_enabled
352 352
353 353 @LoginRequired()
354 354 @HasRepoPermissionAnyDecorator(
355 355 'repository.read', 'repository.write', 'repository.admin')
356 356 def repo_archivefile(self):
357 357 # archive cache config
358 358 from rhodecode import CONFIG
359 359 _ = self.request.translate
360 360 self.load_default_context()
361 361 default_at_path = '/'
362 362 fname = self.request.matchdict['fname']
363 363 subrepos = self.request.GET.get('subrepos') == 'true'
364 364 with_hash = str2bool(self.request.GET.get('with_hash', '1'))
365 365 at_path = self.request.GET.get('at_path') or default_at_path
366 366
367 367 if not self.db_repo.enable_downloads:
368 368 return Response(_('Downloads disabled'))
369 369
370 370 try:
371 371 commit_id, ext, fileformat, content_type = \
372 372 _get_archive_spec(fname)
373 373 except ValueError:
374 374 return Response(_('Unknown archive type for: `{}`').format(
375 375 h.escape(fname)))
376 376
377 377 try:
378 378 commit = self.rhodecode_vcs_repo.get_commit(commit_id)
379 379 except CommitDoesNotExistError:
380 380 return Response(_('Unknown commit_id {}').format(
381 381 h.escape(commit_id)))
382 382 except EmptyRepositoryError:
383 383 return Response(_('Empty repository'))
384 384
385 385 # we used a ref, or a shorter version, lets redirect client ot use explicit hash
386 386 if commit_id != commit.raw_id:
387 387 fname=f'{commit.raw_id}{ext}'
388 388 raise HTTPFound(self.request.current_route_path(fname=fname))
389 389
390 390 try:
391 391 at_path = commit.get_node(at_path).path or default_at_path
392 392 except Exception:
393 393 return Response(_('No node at path {} for this repository').format(h.escape(at_path)))
394 394
395 395 path_sha = get_path_sha(at_path)
396 396
397 397 # used for cache etc, consistent unique archive name
398 398 archive_name_key = get_archive_name(
399 self.db_repo_name, commit_sha=commit.short_id, ext=ext, subrepos=subrepos,
399 self.db_repo.repo_id, self.db_repo_name, commit_sha=commit.short_id, ext=ext, subrepos=subrepos,
400 400 path_sha=path_sha, with_hash=True)
401 401
402 402 if not with_hash:
403 403 path_sha = ''
404 404
405 405 # what end client gets served
406 406 response_archive_name = get_archive_name(
407 self.db_repo_name, commit_sha=commit.short_id, ext=ext, subrepos=subrepos,
407 self.db_repo.repo_id, self.db_repo_name, commit_sha=commit.short_id, ext=ext, subrepos=subrepos,
408 408 path_sha=path_sha, with_hash=with_hash)
409 409
410 410 # remove extension from our archive directory name
411 411 archive_dir_name = response_archive_name[:-len(ext)]
412 412
413 413 archive_cache_disable = self.request.GET.get('no_cache')
414 414
415 415 d_cache = get_archival_cache_store(config=CONFIG)
416 416 # NOTE: we get the config to pass to a call to lazy-init the SAME type of cache on vcsserver
417 417 d_cache_conf = get_archival_config(config=CONFIG)
418 418
419 419 reentrant_lock_key = archive_name_key + '.lock'
420 420 with ReentrantLock(d_cache, reentrant_lock_key):
421 421 # This is also a cache key
422 422 use_cached_archive = False
423 423 if archive_name_key in d_cache and not archive_cache_disable:
424 424 reader, tag = d_cache.get(archive_name_key, read=True, tag=True, retry=True)
425 425 use_cached_archive = True
426 426 log.debug('Found cached archive as key=%s tag=%s, serving archive from cache reader=%s',
427 427 archive_name_key, tag, reader.name)
428 428 else:
429 429 reader = None
430 430 log.debug('Archive with key=%s is not yet cached, creating one now...', archive_name_key)
431 431
432 432 # generate new archive, as previous was not found in the cache
433 433 if not reader:
434 434
435 435 try:
436 436 commit.archive_repo(archive_name_key, archive_dir_name=archive_dir_name,
437 437 kind=fileformat, subrepos=subrepos,
438 438 archive_at_path=at_path, cache_config=d_cache_conf)
439 439 except ImproperArchiveTypeError:
440 440 return _('Unknown archive type')
441 441
442 442 reader, tag = d_cache.get(archive_name_key, read=True, tag=True, retry=True)
443 443
444 444 if not reader:
445 445 raise ValueError('archive cache reader is empty, failed to fetch file from distributed archive cache')
446 446
447 def archive_iterator(_reader):
447 def archive_iterator(_reader, block_size: int = 4096*512):
448 # 4096 * 64 = 64KB
448 449 while 1:
449 data = _reader.read(1024)
450 data = _reader.read(block_size)
450 451 if not data:
451 452 break
452 453 yield data
453 454
454 455 response = Response(app_iter=archive_iterator(reader))
455 456 response.content_disposition = f'attachment; filename={response_archive_name}'
456 457 response.content_type = str(content_type)
457 458
458 459 try:
459 460 return response
460 461 finally:
461 462 # store download action
462 463 audit_logger.store_web(
463 464 'repo.archive.download', action_data={
464 465 'user_agent': self.request.user_agent,
465 466 'archive_name': archive_name_key,
466 467 'archive_spec': fname,
467 468 'archive_cached': use_cached_archive},
468 469 user=self._rhodecode_user,
469 470 repo=self.db_repo,
470 471 commit=True
471 472 )
472 473
473 474 def _get_file_node(self, commit_id, f_path):
474 475 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
475 476 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
476 477 try:
477 478 node = commit.get_node(f_path)
478 479 if node.is_dir():
479 480 raise NodeError(f'{node} path is a {type(node)} not a file')
480 481 except NodeDoesNotExistError:
481 482 commit = EmptyCommit(
482 483 commit_id=commit_id,
483 484 idx=commit.idx,
484 485 repo=commit.repository,
485 486 alias=commit.repository.alias,
486 487 message=commit.message,
487 488 author=commit.author,
488 489 date=commit.date)
489 490 node = FileNode(safe_bytes(f_path), b'', commit=commit)
490 491 else:
491 492 commit = EmptyCommit(
492 493 repo=self.rhodecode_vcs_repo,
493 494 alias=self.rhodecode_vcs_repo.alias)
494 495 node = FileNode(safe_bytes(f_path), b'', commit=commit)
495 496 return node
496 497
497 498 @LoginRequired()
498 499 @HasRepoPermissionAnyDecorator(
499 500 'repository.read', 'repository.write', 'repository.admin')
500 501 def repo_files_diff(self):
501 502 c = self.load_default_context()
502 503 f_path = self._get_f_path(self.request.matchdict)
503 504 diff1 = self.request.GET.get('diff1', '')
504 505 diff2 = self.request.GET.get('diff2', '')
505 506
506 507 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
507 508
508 509 ignore_whitespace = str2bool(self.request.GET.get('ignorews'))
509 510 line_context = self.request.GET.get('context', 3)
510 511
511 512 if not any((diff1, diff2)):
512 513 h.flash(
513 514 'Need query parameter "diff1" or "diff2" to generate a diff.',
514 515 category='error')
515 516 raise HTTPBadRequest()
516 517
517 518 c.action = self.request.GET.get('diff')
518 519 if c.action not in ['download', 'raw']:
519 520 compare_url = h.route_path(
520 521 'repo_compare',
521 522 repo_name=self.db_repo_name,
522 523 source_ref_type='rev',
523 524 source_ref=diff1,
524 525 target_repo=self.db_repo_name,
525 526 target_ref_type='rev',
526 527 target_ref=diff2,
527 528 _query=dict(f_path=f_path))
528 529 # redirect to new view if we render diff
529 530 raise HTTPFound(compare_url)
530 531
531 532 try:
532 533 node1 = self._get_file_node(diff1, path1)
533 534 node2 = self._get_file_node(diff2, f_path)
534 535 except (RepositoryError, NodeError):
535 536 log.exception("Exception while trying to get node from repository")
536 537 raise HTTPFound(
537 538 h.route_path('repo_files', repo_name=self.db_repo_name,
538 539 commit_id='tip', f_path=f_path))
539 540
540 541 if all(isinstance(node.commit, EmptyCommit)
541 542 for node in (node1, node2)):
542 543 raise HTTPNotFound()
543 544
544 545 c.commit_1 = node1.commit
545 546 c.commit_2 = node2.commit
546 547
547 548 if c.action == 'download':
548 549 _diff = diffs.get_gitdiff(node1, node2,
549 550 ignore_whitespace=ignore_whitespace,
550 551 context=line_context)
551 552 # NOTE: this was using diff_format='gitdiff'
552 553 diff = diffs.DiffProcessor(_diff, diff_format='newdiff')
553 554
554 555 response = Response(self.path_filter.get_raw_patch(diff))
555 556 response.content_type = 'text/plain'
556 557 response.content_disposition = (
557 558 f'attachment; filename={f_path}_{diff1}_vs_{diff2}.diff'
558 559 )
559 560 charset = self._get_default_encoding(c)
560 561 if charset:
561 562 response.charset = charset
562 563 return response
563 564
564 565 elif c.action == 'raw':
565 566 _diff = diffs.get_gitdiff(node1, node2,
566 567 ignore_whitespace=ignore_whitespace,
567 568 context=line_context)
568 569 # NOTE: this was using diff_format='gitdiff'
569 570 diff = diffs.DiffProcessor(_diff, diff_format='newdiff')
570 571
571 572 response = Response(self.path_filter.get_raw_patch(diff))
572 573 response.content_type = 'text/plain'
573 574 charset = self._get_default_encoding(c)
574 575 if charset:
575 576 response.charset = charset
576 577 return response
577 578
578 579 # in case we ever end up here
579 580 raise HTTPNotFound()
580 581
581 582 @LoginRequired()
582 583 @HasRepoPermissionAnyDecorator(
583 584 'repository.read', 'repository.write', 'repository.admin')
584 585 def repo_files_diff_2way_redirect(self):
585 586 """
586 587 Kept only to make OLD links work
587 588 """
588 589 f_path = self._get_f_path_unchecked(self.request.matchdict)
589 590 diff1 = self.request.GET.get('diff1', '')
590 591 diff2 = self.request.GET.get('diff2', '')
591 592
592 593 if not any((diff1, diff2)):
593 594 h.flash(
594 595 'Need query parameter "diff1" or "diff2" to generate a diff.',
595 596 category='error')
596 597 raise HTTPBadRequest()
597 598
598 599 compare_url = h.route_path(
599 600 'repo_compare',
600 601 repo_name=self.db_repo_name,
601 602 source_ref_type='rev',
602 603 source_ref=diff1,
603 604 target_ref_type='rev',
604 605 target_ref=diff2,
605 606 _query=dict(f_path=f_path, diffmode='sideside',
606 607 target_repo=self.db_repo_name,))
607 608 raise HTTPFound(compare_url)
608 609
609 610 @LoginRequired()
610 611 def repo_files_default_commit_redirect(self):
611 612 """
612 613 Special page that redirects to the landing page of files based on the default
613 614 commit for repository
614 615 """
615 616 c = self.load_default_context()
616 617 ref_name = c.rhodecode_db_repo.landing_ref_name
617 618 landing_url = h.repo_files_by_ref_url(
618 619 c.rhodecode_db_repo.repo_name,
619 620 c.rhodecode_db_repo.repo_type,
620 621 f_path='',
621 622 ref_name=ref_name,
622 623 commit_id='tip',
623 624 query=dict(at=ref_name)
624 625 )
625 626
626 627 raise HTTPFound(landing_url)
627 628
628 629 @LoginRequired()
629 630 @HasRepoPermissionAnyDecorator(
630 631 'repository.read', 'repository.write', 'repository.admin')
631 632 def repo_files(self):
632 633 c = self.load_default_context()
633 634
634 635 view_name = getattr(self.request.matched_route, 'name', None)
635 636
636 637 c.annotate = view_name == 'repo_files:annotated'
637 638 # default is false, but .rst/.md files later are auto rendered, we can
638 639 # overwrite auto rendering by setting this GET flag
639 640 c.renderer = view_name == 'repo_files:rendered' or not self.request.GET.get('no-render', False)
640 641
641 642 commit_id, f_path = self._get_commit_and_path()
642 643
643 644 c.commit = self._get_commit_or_redirect(commit_id)
644 645 c.branch = self.request.GET.get('branch', None)
645 646 c.f_path = f_path
646 647 at_rev = self.request.GET.get('at')
647 648
648 649 # prev link
649 650 try:
650 651 prev_commit = c.commit.prev(c.branch)
651 652 c.prev_commit = prev_commit
652 653 c.url_prev = h.route_path(
653 654 'repo_files', repo_name=self.db_repo_name,
654 655 commit_id=prev_commit.raw_id, f_path=f_path)
655 656 if c.branch:
656 657 c.url_prev += '?branch=%s' % c.branch
657 658 except (CommitDoesNotExistError, VCSError):
658 659 c.url_prev = '#'
659 660 c.prev_commit = EmptyCommit()
660 661
661 662 # next link
662 663 try:
663 664 next_commit = c.commit.next(c.branch)
664 665 c.next_commit = next_commit
665 666 c.url_next = h.route_path(
666 667 'repo_files', repo_name=self.db_repo_name,
667 668 commit_id=next_commit.raw_id, f_path=f_path)
668 669 if c.branch:
669 670 c.url_next += '?branch=%s' % c.branch
670 671 except (CommitDoesNotExistError, VCSError):
671 672 c.url_next = '#'
672 673 c.next_commit = EmptyCommit()
673 674
674 675 # files or dirs
675 676 try:
676 677 c.file = c.commit.get_node(f_path, pre_load=['is_binary', 'size', 'data'])
677 678
678 679 c.file_author = True
679 680 c.file_tree = ''
680 681
681 682 # load file content
682 683 if c.file.is_file():
683 684 c.lf_node = {}
684 685
685 686 has_lf_enabled = self._is_lf_enabled(self.db_repo)
686 687 if has_lf_enabled:
687 688 c.lf_node = c.file.get_largefile_node()
688 689
689 690 c.file_source_page = 'true'
690 691 c.file_last_commit = c.file.last_commit
691 692
692 693 c.file_size_too_big = c.file.size > c.visual.cut_off_limit_file
693 694
694 695 if not (c.file_size_too_big or c.file.is_binary):
695 696 if c.annotate: # annotation has precedence over renderer
696 697 c.annotated_lines = filenode_as_annotated_lines_tokens(
697 698 c.file
698 699 )
699 700 else:
700 701 c.renderer = (
701 702 c.renderer and h.renderer_from_filename(c.file.path)
702 703 )
703 704 if not c.renderer:
704 705 c.lines = filenode_as_lines_tokens(c.file)
705 706
706 707 _branch_name, _sha_commit_id, is_head = \
707 708 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
708 709 landing_ref=self.db_repo.landing_ref_name)
709 710 c.on_branch_head = is_head
710 711
711 712 branch = c.commit.branch if (
712 713 c.commit.branch and '/' not in c.commit.branch) else None
713 714 c.branch_or_raw_id = branch or c.commit.raw_id
714 715 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
715 716
716 717 author = c.file_last_commit.author
717 718 c.authors = [[
718 719 h.email(author),
719 720 h.person(author, 'username_or_name_or_email'),
720 721 1
721 722 ]]
722 723
723 724 else: # load tree content at path
724 725 c.file_source_page = 'false'
725 726 c.authors = []
726 727 # this loads a simple tree without metadata to speed things up
727 728 # later via ajax we call repo_nodetree_full and fetch whole
728 729 c.file_tree = self._get_tree_at_commit(c, c.commit.raw_id, f_path, at_rev=at_rev)
729 730
730 731 c.readme_data, c.readme_file = \
731 732 self._get_readme_data(self.db_repo, c.visual.default_renderer,
732 733 c.commit.raw_id, f_path)
733 734
734 735 except RepositoryError as e:
735 736 h.flash(h.escape(safe_str(e)), category='error')
736 737 raise HTTPNotFound()
737 738
738 739 if self.request.environ.get('HTTP_X_PJAX'):
739 740 html = render('rhodecode:templates/files/files_pjax.mako',
740 741 self._get_template_context(c), self.request)
741 742 else:
742 743 html = render('rhodecode:templates/files/files.mako',
743 744 self._get_template_context(c), self.request)
744 745 return Response(html)
745 746
746 747 @HasRepoPermissionAnyDecorator(
747 748 'repository.read', 'repository.write', 'repository.admin')
748 749 def repo_files_annotated_previous(self):
749 750 self.load_default_context()
750 751
751 752 commit_id, f_path = self._get_commit_and_path()
752 753 commit = self._get_commit_or_redirect(commit_id)
753 754 prev_commit_id = commit.raw_id
754 755 line_anchor = self.request.GET.get('line_anchor')
755 756 is_file = False
756 757 try:
757 758 _file = commit.get_node(f_path)
758 759 is_file = _file.is_file()
759 760 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
760 761 pass
761 762
762 763 if is_file:
763 764 history = commit.get_path_history(f_path)
764 765 prev_commit_id = history[1].raw_id \
765 766 if len(history) > 1 else prev_commit_id
766 767 prev_url = h.route_path(
767 768 'repo_files:annotated', repo_name=self.db_repo_name,
768 769 commit_id=prev_commit_id, f_path=f_path,
769 770 _anchor=f'L{line_anchor}')
770 771
771 772 raise HTTPFound(prev_url)
772 773
773 774 @LoginRequired()
774 775 @HasRepoPermissionAnyDecorator(
775 776 'repository.read', 'repository.write', 'repository.admin')
776 777 def repo_nodetree_full(self):
777 778 """
778 779 Returns rendered html of file tree that contains commit date,
779 780 author, commit_id for the specified combination of
780 781 repo, commit_id and file path
781 782 """
782 783 c = self.load_default_context()
783 784
784 785 commit_id, f_path = self._get_commit_and_path()
785 786 commit = self._get_commit_or_redirect(commit_id)
786 787 try:
787 788 dir_node = commit.get_node(f_path)
788 789 except RepositoryError as e:
789 790 return Response(f'error: {h.escape(safe_str(e))}')
790 791
791 792 if dir_node.is_file():
792 793 return Response('')
793 794
794 795 c.file = dir_node
795 796 c.commit = commit
796 797 at_rev = self.request.GET.get('at')
797 798
798 799 html = self._get_tree_at_commit(
799 800 c, commit.raw_id, dir_node.path, full_load=True, at_rev=at_rev)
800 801
801 802 return Response(html)
802 803
803 804 def _get_attachement_headers(self, f_path):
804 805 f_name = safe_str(f_path.split(Repository.NAME_SEP)[-1])
805 806 safe_path = f_name.replace('"', '\\"')
806 807 encoded_path = urllib.parse.quote(f_name)
807 808
808 809 return "attachment; " \
809 810 "filename=\"{}\"; " \
810 811 "filename*=UTF-8\'\'{}".format(safe_path, encoded_path)
811 812
812 813 @LoginRequired()
813 814 @HasRepoPermissionAnyDecorator(
814 815 'repository.read', 'repository.write', 'repository.admin')
815 816 def repo_file_raw(self):
816 817 """
817 818 Action for show as raw, some mimetypes are "rendered",
818 819 those include images, icons.
819 820 """
820 821 c = self.load_default_context()
821 822
822 823 commit_id, f_path = self._get_commit_and_path()
823 824 commit = self._get_commit_or_redirect(commit_id)
824 825 file_node = self._get_filenode_or_redirect(commit, f_path)
825 826
826 827 raw_mimetype_mapping = {
827 828 # map original mimetype to a mimetype used for "show as raw"
828 829 # you can also provide a content-disposition to override the
829 830 # default "attachment" disposition.
830 831 # orig_type: (new_type, new_dispo)
831 832
832 833 # show images inline:
833 834 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
834 835 # for example render an SVG with javascript inside or even render
835 836 # HTML.
836 837 'image/x-icon': ('image/x-icon', 'inline'),
837 838 'image/png': ('image/png', 'inline'),
838 839 'image/gif': ('image/gif', 'inline'),
839 840 'image/jpeg': ('image/jpeg', 'inline'),
840 841 'application/pdf': ('application/pdf', 'inline'),
841 842 }
842 843
843 844 mimetype = file_node.mimetype
844 845 try:
845 846 mimetype, disposition = raw_mimetype_mapping[mimetype]
846 847 except KeyError:
847 848 # we don't know anything special about this, handle it safely
848 849 if file_node.is_binary:
849 850 # do same as download raw for binary files
850 851 mimetype, disposition = 'application/octet-stream', 'attachment'
851 852 else:
852 853 # do not just use the original mimetype, but force text/plain,
853 854 # otherwise it would serve text/html and that might be unsafe.
854 855 # Note: underlying vcs library fakes text/plain mimetype if the
855 856 # mimetype can not be determined and it thinks it is not
856 857 # binary.This might lead to erroneous text display in some
857 858 # cases, but helps in other cases, like with text files
858 859 # without extension.
859 860 mimetype, disposition = 'text/plain', 'inline'
860 861
861 862 if disposition == 'attachment':
862 863 disposition = self._get_attachement_headers(f_path)
863 864
864 865 stream_content = file_node.stream_bytes()
865 866
866 867 response = Response(app_iter=stream_content)
867 868 response.content_disposition = disposition
868 869 response.content_type = mimetype
869 870
870 871 charset = self._get_default_encoding(c)
871 872 if charset:
872 873 response.charset = charset
873 874
874 875 return response
875 876
876 877 @LoginRequired()
877 878 @HasRepoPermissionAnyDecorator(
878 879 'repository.read', 'repository.write', 'repository.admin')
879 880 def repo_file_download(self):
880 881 c = self.load_default_context()
881 882
882 883 commit_id, f_path = self._get_commit_and_path()
883 884 commit = self._get_commit_or_redirect(commit_id)
884 885 file_node = self._get_filenode_or_redirect(commit, f_path)
885 886
886 887 if self.request.GET.get('lf'):
887 888 # only if lf get flag is passed, we download this file
888 889 # as LFS/Largefile
889 890 lf_node = file_node.get_largefile_node()
890 891 if lf_node:
891 892 # overwrite our pointer with the REAL large-file
892 893 file_node = lf_node
893 894
894 895 disposition = self._get_attachement_headers(f_path)
895 896
896 897 stream_content = file_node.stream_bytes()
897 898
898 899 response = Response(app_iter=stream_content)
899 900 response.content_disposition = disposition
900 901 response.content_type = file_node.mimetype
901 902
902 903 charset = self._get_default_encoding(c)
903 904 if charset:
904 905 response.charset = charset
905 906
906 907 return response
907 908
908 909 def _get_nodelist_at_commit(self, repo_name, repo_id, commit_id, f_path):
909 910
910 911 cache_seconds = safe_int(
911 912 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
912 913 cache_on = cache_seconds > 0
913 914 log.debug(
914 915 'Computing FILE SEARCH for repo_id %s commit_id `%s` and path `%s`'
915 916 'with caching: %s[TTL: %ss]' % (
916 917 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
917 918
918 919 cache_namespace_uid = f'repo.{repo_id}'
919 920 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
920 921
921 922 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, condition=cache_on)
922 923 def compute_file_search(_name_hash, _repo_id, _commit_id, _f_path):
923 924 log.debug('Generating cached nodelist for repo_id:%s, %s, %s',
924 925 _repo_id, commit_id, f_path)
925 926 try:
926 927 _d, _f = ScmModel().get_quick_filter_nodes(repo_name, _commit_id, _f_path)
927 928 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
928 929 log.exception(safe_str(e))
929 930 h.flash(h.escape(safe_str(e)), category='error')
930 931 raise HTTPFound(h.route_path(
931 932 'repo_files', repo_name=self.db_repo_name,
932 933 commit_id='tip', f_path='/'))
933 934
934 935 return _d + _f
935 936
936 937 result = compute_file_search(self.db_repo.repo_name_hash, self.db_repo.repo_id,
937 938 commit_id, f_path)
938 939 return filter(lambda n: self.path_filter.path_access_allowed(n['name']), result)
939 940
940 941 @LoginRequired()
941 942 @HasRepoPermissionAnyDecorator(
942 943 'repository.read', 'repository.write', 'repository.admin')
943 944 def repo_nodelist(self):
944 945 self.load_default_context()
945 946
946 947 commit_id, f_path = self._get_commit_and_path()
947 948 commit = self._get_commit_or_redirect(commit_id)
948 949
949 950 metadata = self._get_nodelist_at_commit(
950 951 self.db_repo_name, self.db_repo.repo_id, commit.raw_id, f_path)
951 952 return {'nodes': [x for x in metadata]}
952 953
953 954 def _create_references(self, branches_or_tags, symbolic_reference, f_path, ref_type):
954 955 items = []
955 956 for name, commit_id in branches_or_tags.items():
956 957 sym_ref = symbolic_reference(commit_id, name, f_path, ref_type)
957 958 items.append((sym_ref, name, ref_type))
958 959 return items
959 960
960 961 def _symbolic_reference(self, commit_id, name, f_path, ref_type):
961 962 return commit_id
962 963
963 964 def _symbolic_reference_svn(self, commit_id, name, f_path, ref_type):
964 965 return commit_id
965 966
966 967 # NOTE(dan): old code we used in "diff" mode compare
967 968 new_f_path = vcspath.join(name, f_path)
968 969 return f'{new_f_path}@{commit_id}'
969 970
970 971 def _get_node_history(self, commit_obj, f_path, commits=None):
971 972 """
972 973 get commit history for given node
973 974
974 975 :param commit_obj: commit to calculate history
975 976 :param f_path: path for node to calculate history for
976 977 :param commits: if passed don't calculate history and take
977 978 commits defined in this list
978 979 """
979 980 _ = self.request.translate
980 981
981 982 # calculate history based on tip
982 983 tip = self.rhodecode_vcs_repo.get_commit()
983 984 if commits is None:
984 985 pre_load = ["author", "branch"]
985 986 try:
986 987 commits = tip.get_path_history(f_path, pre_load=pre_load)
987 988 except (NodeDoesNotExistError, CommitError):
988 989 # this node is not present at tip!
989 990 commits = commit_obj.get_path_history(f_path, pre_load=pre_load)
990 991
991 992 history = []
992 993 commits_group = ([], _("Changesets"))
993 994 for commit in commits:
994 995 branch = ' (%s)' % commit.branch if commit.branch else ''
995 996 n_desc = f'r{commit.idx}:{commit.short_id}{branch}'
996 997 commits_group[0].append((commit.raw_id, n_desc, 'sha'))
997 998 history.append(commits_group)
998 999
999 1000 symbolic_reference = self._symbolic_reference
1000 1001
1001 1002 if self.rhodecode_vcs_repo.alias == 'svn':
1002 1003 adjusted_f_path = RepoFilesView.adjust_file_path_for_svn(
1003 1004 f_path, self.rhodecode_vcs_repo)
1004 1005 if adjusted_f_path != f_path:
1005 1006 log.debug(
1006 1007 'Recognized svn tag or branch in file "%s", using svn '
1007 1008 'specific symbolic references', f_path)
1008 1009 f_path = adjusted_f_path
1009 1010 symbolic_reference = self._symbolic_reference_svn
1010 1011
1011 1012 branches = self._create_references(
1012 1013 self.rhodecode_vcs_repo.branches, symbolic_reference, f_path, 'branch')
1013 1014 branches_group = (branches, _("Branches"))
1014 1015
1015 1016 tags = self._create_references(
1016 1017 self.rhodecode_vcs_repo.tags, symbolic_reference, f_path, 'tag')
1017 1018 tags_group = (tags, _("Tags"))
1018 1019
1019 1020 history.append(branches_group)
1020 1021 history.append(tags_group)
1021 1022
1022 1023 return history, commits
1023 1024
1024 1025 @LoginRequired()
1025 1026 @HasRepoPermissionAnyDecorator(
1026 1027 'repository.read', 'repository.write', 'repository.admin')
1027 1028 def repo_file_history(self):
1028 1029 self.load_default_context()
1029 1030
1030 1031 commit_id, f_path = self._get_commit_and_path()
1031 1032 commit = self._get_commit_or_redirect(commit_id)
1032 1033 file_node = self._get_filenode_or_redirect(commit, f_path)
1033 1034
1034 1035 if file_node.is_file():
1035 1036 file_history, _hist = self._get_node_history(commit, f_path)
1036 1037
1037 1038 res = []
1038 1039 for section_items, section in file_history:
1039 1040 items = []
1040 1041 for obj_id, obj_text, obj_type in section_items:
1041 1042 at_rev = ''
1042 1043 if obj_type in ['branch', 'bookmark', 'tag']:
1043 1044 at_rev = obj_text
1044 1045 entry = {
1045 1046 'id': obj_id,
1046 1047 'text': obj_text,
1047 1048 'type': obj_type,
1048 1049 'at_rev': at_rev
1049 1050 }
1050 1051
1051 1052 items.append(entry)
1052 1053
1053 1054 res.append({
1054 1055 'text': section,
1055 1056 'children': items
1056 1057 })
1057 1058
1058 1059 data = {
1059 1060 'more': False,
1060 1061 'results': res
1061 1062 }
1062 1063 return data
1063 1064
1064 1065 log.warning('Cannot fetch history for directory')
1065 1066 raise HTTPBadRequest()
1066 1067
1067 1068 @LoginRequired()
1068 1069 @HasRepoPermissionAnyDecorator(
1069 1070 'repository.read', 'repository.write', 'repository.admin')
1070 1071 def repo_file_authors(self):
1071 1072 c = self.load_default_context()
1072 1073
1073 1074 commit_id, f_path = self._get_commit_and_path()
1074 1075 commit = self._get_commit_or_redirect(commit_id)
1075 1076 file_node = self._get_filenode_or_redirect(commit, f_path)
1076 1077
1077 1078 if not file_node.is_file():
1078 1079 raise HTTPBadRequest()
1079 1080
1080 1081 c.file_last_commit = file_node.last_commit
1081 1082 if self.request.GET.get('annotate') == '1':
1082 1083 # use _hist from annotation if annotation mode is on
1083 1084 commit_ids = {x[1] for x in file_node.annotate}
1084 1085 _hist = (
1085 1086 self.rhodecode_vcs_repo.get_commit(commit_id)
1086 1087 for commit_id in commit_ids)
1087 1088 else:
1088 1089 _f_history, _hist = self._get_node_history(commit, f_path)
1089 1090 c.file_author = False
1090 1091
1091 1092 unique = collections.OrderedDict()
1092 1093 for commit in _hist:
1093 1094 author = commit.author
1094 1095 if author not in unique:
1095 1096 unique[commit.author] = [
1096 1097 h.email(author),
1097 1098 h.person(author, 'username_or_name_or_email'),
1098 1099 1 # counter
1099 1100 ]
1100 1101
1101 1102 else:
1102 1103 # increase counter
1103 1104 unique[commit.author][2] += 1
1104 1105
1105 1106 c.authors = [val for val in unique.values()]
1106 1107
1107 1108 return self._get_template_context(c)
1108 1109
1109 1110 @LoginRequired()
1110 1111 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1111 1112 def repo_files_check_head(self):
1112 1113 self.load_default_context()
1113 1114
1114 1115 commit_id, f_path = self._get_commit_and_path()
1115 1116 _branch_name, _sha_commit_id, is_head = \
1116 1117 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1117 1118 landing_ref=self.db_repo.landing_ref_name)
1118 1119
1119 1120 new_path = self.request.POST.get('path')
1120 1121 operation = self.request.POST.get('operation')
1121 1122 path_exist = ''
1122 1123
1123 1124 if new_path and operation in ['create', 'upload']:
1124 1125 new_f_path = os.path.join(f_path.lstrip('/'), new_path)
1125 1126 try:
1126 1127 commit_obj = self.rhodecode_vcs_repo.get_commit(commit_id)
1127 1128 # NOTE(dan): construct whole path without leading /
1128 1129 file_node = commit_obj.get_node(new_f_path)
1129 1130 if file_node is not None:
1130 1131 path_exist = new_f_path
1131 1132 except EmptyRepositoryError:
1132 1133 pass
1133 1134 except Exception:
1134 1135 pass
1135 1136
1136 1137 return {
1137 1138 'branch': _branch_name,
1138 1139 'sha': _sha_commit_id,
1139 1140 'is_head': is_head,
1140 1141 'path_exists': path_exist
1141 1142 }
1142 1143
1143 1144 @LoginRequired()
1144 1145 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1145 1146 def repo_files_remove_file(self):
1146 1147 _ = self.request.translate
1147 1148 c = self.load_default_context()
1148 1149 commit_id, f_path = self._get_commit_and_path()
1149 1150
1150 1151 self._ensure_not_locked()
1151 1152 _branch_name, _sha_commit_id, is_head = \
1152 1153 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1153 1154 landing_ref=self.db_repo.landing_ref_name)
1154 1155
1155 1156 self.forbid_non_head(is_head, f_path)
1156 1157 self.check_branch_permission(_branch_name)
1157 1158
1158 1159 c.commit = self._get_commit_or_redirect(commit_id)
1159 1160 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1160 1161
1161 1162 c.default_message = _(
1162 1163 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1163 1164 c.f_path = f_path
1164 1165
1165 1166 return self._get_template_context(c)
1166 1167
1167 1168 @LoginRequired()
1168 1169 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1169 1170 @CSRFRequired()
1170 1171 def repo_files_delete_file(self):
1171 1172 _ = self.request.translate
1172 1173
1173 1174 c = self.load_default_context()
1174 1175 commit_id, f_path = self._get_commit_and_path()
1175 1176
1176 1177 self._ensure_not_locked()
1177 1178 _branch_name, _sha_commit_id, is_head = \
1178 1179 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1179 1180 landing_ref=self.db_repo.landing_ref_name)
1180 1181
1181 1182 self.forbid_non_head(is_head, f_path)
1182 1183 self.check_branch_permission(_branch_name)
1183 1184
1184 1185 c.commit = self._get_commit_or_redirect(commit_id)
1185 1186 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1186 1187
1187 1188 c.default_message = _(
1188 1189 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1189 1190 c.f_path = f_path
1190 1191 node_path = f_path
1191 1192 author = self._rhodecode_db_user.full_contact
1192 1193 message = self.request.POST.get('message') or c.default_message
1193 1194 try:
1194 1195 nodes = {
1195 1196 safe_bytes(node_path): {
1196 1197 'content': b''
1197 1198 }
1198 1199 }
1199 1200 ScmModel().delete_nodes(
1200 1201 user=self._rhodecode_db_user.user_id, repo=self.db_repo,
1201 1202 message=message,
1202 1203 nodes=nodes,
1203 1204 parent_commit=c.commit,
1204 1205 author=author,
1205 1206 )
1206 1207
1207 1208 h.flash(
1208 1209 _('Successfully deleted file `{}`').format(
1209 1210 h.escape(f_path)), category='success')
1210 1211 except Exception:
1211 1212 log.exception('Error during commit operation')
1212 1213 h.flash(_('Error occurred during commit'), category='error')
1213 1214 raise HTTPFound(
1214 1215 h.route_path('repo_commit', repo_name=self.db_repo_name,
1215 1216 commit_id='tip'))
1216 1217
1217 1218 @LoginRequired()
1218 1219 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1219 1220 def repo_files_edit_file(self):
1220 1221 _ = self.request.translate
1221 1222 c = self.load_default_context()
1222 1223 commit_id, f_path = self._get_commit_and_path()
1223 1224
1224 1225 self._ensure_not_locked()
1225 1226 _branch_name, _sha_commit_id, is_head = \
1226 1227 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1227 1228 landing_ref=self.db_repo.landing_ref_name)
1228 1229
1229 1230 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1230 1231 self.check_branch_permission(_branch_name, commit_id=commit_id)
1231 1232
1232 1233 c.commit = self._get_commit_or_redirect(commit_id)
1233 1234 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1234 1235
1235 1236 if c.file.is_binary:
1236 1237 files_url = h.route_path(
1237 1238 'repo_files',
1238 1239 repo_name=self.db_repo_name,
1239 1240 commit_id=c.commit.raw_id, f_path=f_path)
1240 1241 raise HTTPFound(files_url)
1241 1242
1242 1243 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1243 1244 c.f_path = f_path
1244 1245
1245 1246 return self._get_template_context(c)
1246 1247
1247 1248 @LoginRequired()
1248 1249 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1249 1250 @CSRFRequired()
1250 1251 def repo_files_update_file(self):
1251 1252 _ = self.request.translate
1252 1253 c = self.load_default_context()
1253 1254 commit_id, f_path = self._get_commit_and_path()
1254 1255
1255 1256 self._ensure_not_locked()
1256 1257
1257 1258 c.commit = self._get_commit_or_redirect(commit_id)
1258 1259 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1259 1260
1260 1261 if c.file.is_binary:
1261 1262 raise HTTPFound(h.route_path('repo_files', repo_name=self.db_repo_name,
1262 1263 commit_id=c.commit.raw_id, f_path=f_path))
1263 1264
1264 1265 _branch_name, _sha_commit_id, is_head = \
1265 1266 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1266 1267 landing_ref=self.db_repo.landing_ref_name)
1267 1268
1268 1269 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1269 1270 self.check_branch_permission(_branch_name, commit_id=commit_id)
1270 1271
1271 1272 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1272 1273 c.f_path = f_path
1273 1274
1274 1275 old_content = c.file.str_content
1275 1276 sl = old_content.splitlines(1)
1276 1277 first_line = sl[0] if sl else ''
1277 1278
1278 1279 r_post = self.request.POST
1279 1280 # line endings: 0 - Unix, 1 - Mac, 2 - DOS
1280 1281 line_ending_mode = detect_mode(first_line, 0)
1281 1282 content = convert_line_endings(r_post.get('content', ''), line_ending_mode)
1282 1283
1283 1284 message = r_post.get('message') or c.default_message
1284 1285
1285 1286 org_node_path = c.file.str_path
1286 1287 filename = r_post['filename']
1287 1288
1288 1289 root_path = c.file.dir_path
1289 1290 pure_path = self.create_pure_path(root_path, filename)
1290 1291 node_path = pure_path.as_posix()
1291 1292
1292 1293 default_redirect_url = h.route_path('repo_commit', repo_name=self.db_repo_name,
1293 1294 commit_id=commit_id)
1294 1295 if content == old_content and node_path == org_node_path:
1295 1296 h.flash(_('No changes detected on {}').format(h.escape(org_node_path)),
1296 1297 category='warning')
1297 1298 raise HTTPFound(default_redirect_url)
1298 1299
1299 1300 try:
1300 1301 mapping = {
1301 1302 c.file.bytes_path: {
1302 1303 'org_filename': org_node_path,
1303 1304 'filename': safe_bytes(node_path),
1304 1305 'content': safe_bytes(content),
1305 1306 'lexer': '',
1306 1307 'op': 'mod',
1307 1308 'mode': c.file.mode
1308 1309 }
1309 1310 }
1310 1311
1311 1312 commit = ScmModel().update_nodes(
1312 1313 user=self._rhodecode_db_user.user_id,
1313 1314 repo=self.db_repo,
1314 1315 message=message,
1315 1316 nodes=mapping,
1316 1317 parent_commit=c.commit,
1317 1318 )
1318 1319
1319 1320 h.flash(_('Successfully committed changes to file `{}`').format(
1320 1321 h.escape(f_path)), category='success')
1321 1322 default_redirect_url = h.route_path(
1322 1323 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1323 1324
1324 1325 except Exception:
1325 1326 log.exception('Error occurred during commit')
1326 1327 h.flash(_('Error occurred during commit'), category='error')
1327 1328
1328 1329 raise HTTPFound(default_redirect_url)
1329 1330
1330 1331 @LoginRequired()
1331 1332 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1332 1333 def repo_files_add_file(self):
1333 1334 _ = self.request.translate
1334 1335 c = self.load_default_context()
1335 1336 commit_id, f_path = self._get_commit_and_path()
1336 1337
1337 1338 self._ensure_not_locked()
1338 1339
1339 1340 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1340 1341 if c.commit is None:
1341 1342 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1342 1343
1343 1344 if self.rhodecode_vcs_repo.is_empty():
1344 1345 # for empty repository we cannot check for current branch, we rely on
1345 1346 # c.commit.branch instead
1346 1347 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1347 1348 else:
1348 1349 _branch_name, _sha_commit_id, is_head = \
1349 1350 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1350 1351 landing_ref=self.db_repo.landing_ref_name)
1351 1352
1352 1353 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1353 1354 self.check_branch_permission(_branch_name, commit_id=commit_id)
1354 1355
1355 1356 c.default_message = (_('Added file via RhodeCode Enterprise'))
1356 1357 c.f_path = f_path.lstrip('/') # ensure not relative path
1357 1358
1358 1359 return self._get_template_context(c)
1359 1360
1360 1361 @LoginRequired()
1361 1362 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1362 1363 @CSRFRequired()
1363 1364 def repo_files_create_file(self):
1364 1365 _ = self.request.translate
1365 1366 c = self.load_default_context()
1366 1367 commit_id, f_path = self._get_commit_and_path()
1367 1368
1368 1369 self._ensure_not_locked()
1369 1370
1370 1371 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1371 1372 if c.commit is None:
1372 1373 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1373 1374
1374 1375 # calculate redirect URL
1375 1376 if self.rhodecode_vcs_repo.is_empty():
1376 1377 default_redirect_url = h.route_path(
1377 1378 'repo_summary', repo_name=self.db_repo_name)
1378 1379 else:
1379 1380 default_redirect_url = h.route_path(
1380 1381 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1381 1382
1382 1383 if self.rhodecode_vcs_repo.is_empty():
1383 1384 # for empty repository we cannot check for current branch, we rely on
1384 1385 # c.commit.branch instead
1385 1386 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1386 1387 else:
1387 1388 _branch_name, _sha_commit_id, is_head = \
1388 1389 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1389 1390 landing_ref=self.db_repo.landing_ref_name)
1390 1391
1391 1392 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1392 1393 self.check_branch_permission(_branch_name, commit_id=commit_id)
1393 1394
1394 1395 c.default_message = (_('Added file via RhodeCode Enterprise'))
1395 1396 c.f_path = f_path
1396 1397
1397 1398 r_post = self.request.POST
1398 1399 message = r_post.get('message') or c.default_message
1399 1400 filename = r_post.get('filename')
1400 1401 unix_mode = 0
1401 1402
1402 1403 if not filename:
1403 1404 # If there's no commit, redirect to repo summary
1404 1405 if type(c.commit) is EmptyCommit:
1405 1406 redirect_url = h.route_path(
1406 1407 'repo_summary', repo_name=self.db_repo_name)
1407 1408 else:
1408 1409 redirect_url = default_redirect_url
1409 1410 h.flash(_('No filename specified'), category='warning')
1410 1411 raise HTTPFound(redirect_url)
1411 1412
1412 1413 root_path = f_path
1413 1414 pure_path = self.create_pure_path(root_path, filename)
1414 1415 node_path = pure_path.as_posix().lstrip('/')
1415 1416
1416 1417 author = self._rhodecode_db_user.full_contact
1417 1418 content = convert_line_endings(r_post.get('content', ''), unix_mode)
1418 1419 nodes = {
1419 1420 safe_bytes(node_path): {
1420 1421 'content': safe_bytes(content)
1421 1422 }
1422 1423 }
1423 1424
1424 1425 try:
1425 1426
1426 1427 commit = ScmModel().create_nodes(
1427 1428 user=self._rhodecode_db_user.user_id,
1428 1429 repo=self.db_repo,
1429 1430 message=message,
1430 1431 nodes=nodes,
1431 1432 parent_commit=c.commit,
1432 1433 author=author,
1433 1434 )
1434 1435
1435 1436 h.flash(_('Successfully committed new file `{}`').format(
1436 1437 h.escape(node_path)), category='success')
1437 1438
1438 1439 default_redirect_url = h.route_path(
1439 1440 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1440 1441
1441 1442 except NonRelativePathError:
1442 1443 log.exception('Non Relative path found')
1443 1444 h.flash(_('The location specified must be a relative path and must not '
1444 1445 'contain .. in the path'), category='warning')
1445 1446 raise HTTPFound(default_redirect_url)
1446 1447 except (NodeError, NodeAlreadyExistsError) as e:
1447 1448 h.flash(h.escape(safe_str(e)), category='error')
1448 1449 except Exception:
1449 1450 log.exception('Error occurred during commit')
1450 1451 h.flash(_('Error occurred during commit'), category='error')
1451 1452
1452 1453 raise HTTPFound(default_redirect_url)
1453 1454
1454 1455 @LoginRequired()
1455 1456 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1456 1457 @CSRFRequired()
1457 1458 def repo_files_upload_file(self):
1458 1459 _ = self.request.translate
1459 1460 c = self.load_default_context()
1460 1461 commit_id, f_path = self._get_commit_and_path()
1461 1462
1462 1463 self._ensure_not_locked()
1463 1464
1464 1465 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1465 1466 if c.commit is None:
1466 1467 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1467 1468
1468 1469 # calculate redirect URL
1469 1470 if self.rhodecode_vcs_repo.is_empty():
1470 1471 default_redirect_url = h.route_path(
1471 1472 'repo_summary', repo_name=self.db_repo_name)
1472 1473 else:
1473 1474 default_redirect_url = h.route_path(
1474 1475 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1475 1476
1476 1477 if self.rhodecode_vcs_repo.is_empty():
1477 1478 # for empty repository we cannot check for current branch, we rely on
1478 1479 # c.commit.branch instead
1479 1480 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1480 1481 else:
1481 1482 _branch_name, _sha_commit_id, is_head = \
1482 1483 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1483 1484 landing_ref=self.db_repo.landing_ref_name)
1484 1485
1485 1486 error = self.forbid_non_head(is_head, f_path, json_mode=True)
1486 1487 if error:
1487 1488 return {
1488 1489 'error': error,
1489 1490 'redirect_url': default_redirect_url
1490 1491 }
1491 1492 error = self.check_branch_permission(_branch_name, json_mode=True)
1492 1493 if error:
1493 1494 return {
1494 1495 'error': error,
1495 1496 'redirect_url': default_redirect_url
1496 1497 }
1497 1498
1498 1499 c.default_message = (_('Uploaded file via RhodeCode Enterprise'))
1499 1500 c.f_path = f_path
1500 1501
1501 1502 r_post = self.request.POST
1502 1503
1503 1504 message = c.default_message
1504 1505 user_message = r_post.getall('message')
1505 1506 if isinstance(user_message, list) and user_message:
1506 1507 # we take the first from duplicated results if it's not empty
1507 1508 message = user_message[0] if user_message[0] else message
1508 1509
1509 1510 nodes = {}
1510 1511
1511 1512 for file_obj in r_post.getall('files_upload') or []:
1512 1513 content = file_obj.file
1513 1514 filename = file_obj.filename
1514 1515
1515 1516 root_path = f_path
1516 1517 pure_path = self.create_pure_path(root_path, filename)
1517 1518 node_path = pure_path.as_posix().lstrip('/')
1518 1519
1519 1520 nodes[safe_bytes(node_path)] = {
1520 1521 'content': content
1521 1522 }
1522 1523
1523 1524 if not nodes:
1524 1525 error = 'missing files'
1525 1526 return {
1526 1527 'error': error,
1527 1528 'redirect_url': default_redirect_url
1528 1529 }
1529 1530
1530 1531 author = self._rhodecode_db_user.full_contact
1531 1532
1532 1533 try:
1533 1534 commit = ScmModel().create_nodes(
1534 1535 user=self._rhodecode_db_user.user_id,
1535 1536 repo=self.db_repo,
1536 1537 message=message,
1537 1538 nodes=nodes,
1538 1539 parent_commit=c.commit,
1539 1540 author=author,
1540 1541 )
1541 1542 if len(nodes) == 1:
1542 1543 flash_message = _('Successfully committed {} new files').format(len(nodes))
1543 1544 else:
1544 1545 flash_message = _('Successfully committed 1 new file')
1545 1546
1546 1547 h.flash(flash_message, category='success')
1547 1548
1548 1549 default_redirect_url = h.route_path(
1549 1550 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1550 1551
1551 1552 except NonRelativePathError:
1552 1553 log.exception('Non Relative path found')
1553 1554 error = _('The location specified must be a relative path and must not '
1554 1555 'contain .. in the path')
1555 1556 h.flash(error, category='warning')
1556 1557
1557 1558 return {
1558 1559 'error': error,
1559 1560 'redirect_url': default_redirect_url
1560 1561 }
1561 1562 except (NodeError, NodeAlreadyExistsError) as e:
1562 1563 error = h.escape(e)
1563 1564 h.flash(error, category='error')
1564 1565
1565 1566 return {
1566 1567 'error': error,
1567 1568 'redirect_url': default_redirect_url
1568 1569 }
1569 1570 except Exception:
1570 1571 log.exception('Error occurred during commit')
1571 1572 error = _('Error occurred during commit')
1572 1573 h.flash(error, category='error')
1573 1574 return {
1574 1575 'error': error,
1575 1576 'redirect_url': default_redirect_url
1576 1577 }
1577 1578
1578 1579 return {
1579 1580 'error': None,
1580 1581 'redirect_url': default_redirect_url
1581 1582 }
@@ -1,1985 +1,1984 b''
1 1 # Copyright (C) 2014-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 """
20 20 Base module for all VCS systems
21 21 """
22 22 import os
23 23 import re
24 24 import time
25 25 import shutil
26 26 import datetime
27 27 import fnmatch
28 28 import itertools
29 29 import logging
30 30 import dataclasses
31 31 import warnings
32 32
33 33 from zope.cachedescriptors.property import Lazy as LazyProperty
34 34
35 35
36 36 import rhodecode
37 37 from rhodecode.translation import lazy_ugettext
38 38 from rhodecode.lib.utils2 import safe_str, CachedProperty
39 39 from rhodecode.lib.vcs.utils import author_name, author_email
40 40 from rhodecode.lib.vcs.conf import settings
41 41 from rhodecode.lib.vcs.exceptions import (
42 42 CommitError, EmptyRepositoryError, NodeAlreadyAddedError,
43 43 NodeAlreadyChangedError, NodeAlreadyExistsError, NodeAlreadyRemovedError,
44 44 NodeDoesNotExistError, NodeNotChangedError, VCSError,
45 45 ImproperArchiveTypeError, BranchDoesNotExistError, CommitDoesNotExistError,
46 46 RepositoryError)
47 47
48 48
49 49 log = logging.getLogger(__name__)
50 50
51 51
52 52 FILEMODE_DEFAULT = 0o100644
53 53 FILEMODE_EXECUTABLE = 0o100755
54 54 EMPTY_COMMIT_ID = '0' * 40
55 55
56 56
57 57 @dataclasses.dataclass
58 58 class Reference:
59 59 type: str
60 60 name: str
61 61 commit_id: str
62 62
63 63 def __iter__(self):
64 64 yield self.type
65 65 yield self.name
66 66 yield self.commit_id
67 67
68 68 @property
69 69 def branch(self):
70 70 if self.type == 'branch':
71 71 return self.name
72 72
73 73 @property
74 74 def bookmark(self):
75 75 if self.type == 'book':
76 76 return self.name
77 77
78 78 @property
79 79 def to_str(self):
80 80 return reference_to_unicode(self)
81 81
82 82 def asdict(self):
83 83 return dict(
84 84 type=self.type,
85 85 name=self.name,
86 86 commit_id=self.commit_id
87 87 )
88 88
89 89
90 90 def unicode_to_reference(raw: str):
91 91 """
92 92 Convert a unicode (or string) to a reference object.
93 93 If unicode evaluates to False it returns None.
94 94 """
95 95 if raw:
96 96 refs = raw.split(':')
97 97 return Reference(*refs)
98 98 else:
99 99 return None
100 100
101 101
102 102 def reference_to_unicode(ref: Reference):
103 103 """
104 104 Convert a reference object to unicode.
105 105 If reference is None it returns None.
106 106 """
107 107 if ref:
108 108 return ':'.join(ref)
109 109 else:
110 110 return None
111 111
112 112
113 113 class MergeFailureReason(object):
114 114 """
115 115 Enumeration with all the reasons why the server side merge could fail.
116 116
117 117 DO NOT change the number of the reasons, as they may be stored in the
118 118 database.
119 119
120 120 Changing the name of a reason is acceptable and encouraged to deprecate old
121 121 reasons.
122 122 """
123 123
124 124 # Everything went well.
125 125 NONE = 0
126 126
127 127 # An unexpected exception was raised. Check the logs for more details.
128 128 UNKNOWN = 1
129 129
130 130 # The merge was not successful, there are conflicts.
131 131 MERGE_FAILED = 2
132 132
133 133 # The merge succeeded but we could not push it to the target repository.
134 134 PUSH_FAILED = 3
135 135
136 136 # The specified target is not a head in the target repository.
137 137 TARGET_IS_NOT_HEAD = 4
138 138
139 139 # The source repository contains more branches than the target. Pushing
140 140 # the merge will create additional branches in the target.
141 141 HG_SOURCE_HAS_MORE_BRANCHES = 5
142 142
143 143 # The target reference has multiple heads. That does not allow to correctly
144 144 # identify the target location. This could only happen for mercurial
145 145 # branches.
146 146 HG_TARGET_HAS_MULTIPLE_HEADS = 6
147 147
148 148 # The target repository is locked
149 149 TARGET_IS_LOCKED = 7
150 150
151 151 # Deprecated, use MISSING_TARGET_REF or MISSING_SOURCE_REF instead.
152 152 # A involved commit could not be found.
153 153 _DEPRECATED_MISSING_COMMIT = 8
154 154
155 155 # The target repo reference is missing.
156 156 MISSING_TARGET_REF = 9
157 157
158 158 # The source repo reference is missing.
159 159 MISSING_SOURCE_REF = 10
160 160
161 161 # The merge was not successful, there are conflicts related to sub
162 162 # repositories.
163 163 SUBREPO_MERGE_FAILED = 11
164 164
165 165
166 166 class UpdateFailureReason(object):
167 167 """
168 168 Enumeration with all the reasons why the pull request update could fail.
169 169
170 170 DO NOT change the number of the reasons, as they may be stored in the
171 171 database.
172 172
173 173 Changing the name of a reason is acceptable and encouraged to deprecate old
174 174 reasons.
175 175 """
176 176
177 177 # Everything went well.
178 178 NONE = 0
179 179
180 180 # An unexpected exception was raised. Check the logs for more details.
181 181 UNKNOWN = 1
182 182
183 183 # The pull request is up to date.
184 184 NO_CHANGE = 2
185 185
186 186 # The pull request has a reference type that is not supported for update.
187 187 WRONG_REF_TYPE = 3
188 188
189 189 # Update failed because the target reference is missing.
190 190 MISSING_TARGET_REF = 4
191 191
192 192 # Update failed because the source reference is missing.
193 193 MISSING_SOURCE_REF = 5
194 194
195 195
196 196 class MergeResponse(object):
197 197
198 198 # uses .format(**metadata) for variables
199 199 MERGE_STATUS_MESSAGES = {
200 200 MergeFailureReason.NONE: lazy_ugettext(
201 201 'This pull request can be automatically merged.'),
202 202 MergeFailureReason.UNKNOWN: lazy_ugettext(
203 203 'This pull request cannot be merged because of an unhandled exception. '
204 204 '{exception}'),
205 205 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
206 206 'This pull request cannot be merged because of merge conflicts. {unresolved_files}'),
207 207 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
208 208 'This pull request could not be merged because push to '
209 209 'target:`{target}@{merge_commit}` failed.'),
210 210 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
211 211 'This pull request cannot be merged because the target '
212 212 '`{target_ref.name}` is not a head.'),
213 213 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
214 214 'This pull request cannot be merged because the source contains '
215 215 'more branches than the target.'),
216 216 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
217 217 'This pull request cannot be merged because the target `{target_ref.name}` '
218 218 'has multiple heads: `{heads}`.'),
219 219 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
220 220 'This pull request cannot be merged because the target repository is '
221 221 'locked by {locked_by}.'),
222 222
223 223 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
224 224 'This pull request cannot be merged because the target '
225 225 'reference `{target_ref.name}` is missing.'),
226 226 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
227 227 'This pull request cannot be merged because the source '
228 228 'reference `{source_ref.name}` is missing.'),
229 229 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
230 230 'This pull request cannot be merged because of conflicts related '
231 231 'to sub repositories.'),
232 232
233 233 # Deprecations
234 234 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
235 235 'This pull request cannot be merged because the target or the '
236 236 'source reference is missing.'),
237 237
238 238 }
239 239
240 240 def __init__(self, possible, executed, merge_ref, failure_reason, metadata=None):
241 241 self.possible = possible
242 242 self.executed = executed
243 243 self.merge_ref = merge_ref
244 244 self.failure_reason = failure_reason
245 245 self.metadata = metadata or {}
246 246
247 247 def __repr__(self):
248 248 return f'<MergeResponse:{self.label} {self.failure_reason}>'
249 249
250 250 def __eq__(self, other):
251 251 same_instance = isinstance(other, self.__class__)
252 252 return same_instance \
253 253 and self.possible == other.possible \
254 254 and self.executed == other.executed \
255 255 and self.failure_reason == other.failure_reason
256 256
257 257 @property
258 258 def label(self):
259 259 label_dict = dict((v, k) for k, v in MergeFailureReason.__dict__.items() if
260 260 not k.startswith('_'))
261 261 return label_dict.get(self.failure_reason)
262 262
263 263 @property
264 264 def merge_status_message(self):
265 265 """
266 266 Return a human friendly error message for the given merge status code.
267 267 """
268 268 msg = safe_str(self.MERGE_STATUS_MESSAGES[self.failure_reason])
269 269
270 270 try:
271 271 return msg.format(**self.metadata)
272 272 except Exception:
273 273 log.exception('Failed to format %s message', self)
274 274 return msg
275 275
276 276 def asdict(self):
277 277 data = {}
278 278 for k in ['possible', 'executed', 'merge_ref', 'failure_reason',
279 279 'merge_status_message']:
280 280 data[k] = getattr(self, k)
281 281 return data
282 282
283 283
284 284 class TargetRefMissing(ValueError):
285 285 pass
286 286
287 287
288 288 class SourceRefMissing(ValueError):
289 289 pass
290 290
291 291
292 292 class BaseRepository(object):
293 293 """
294 294 Base Repository for final backends
295 295
296 296 .. attribute:: DEFAULT_BRANCH_NAME
297 297
298 298 name of default branch (i.e. "trunk" for svn, "master" for git etc.
299 299
300 300 .. attribute:: commit_ids
301 301
302 302 list of all available commit ids, in ascending order
303 303
304 304 .. attribute:: path
305 305
306 306 absolute path to the repository
307 307
308 308 .. attribute:: bookmarks
309 309
310 310 Mapping from name to :term:`Commit ID` of the bookmark. Empty in case
311 311 there are no bookmarks or the backend implementation does not support
312 312 bookmarks.
313 313
314 314 .. attribute:: tags
315 315
316 316 Mapping from name to :term:`Commit ID` of the tag.
317 317
318 318 """
319 319
320 320 DEFAULT_BRANCH_NAME = None
321 321 DEFAULT_CONTACT = "Unknown"
322 322 DEFAULT_DESCRIPTION = "unknown"
323 323 EMPTY_COMMIT_ID = '0' * 40
324 324 COMMIT_ID_PAT = re.compile(r'[0-9a-fA-F]{40}')
325 325
326 326 path = None
327 327
328 328 _is_empty = None
329 329 _commit_ids = {}
330 330
331 331 def __init__(self, repo_path, config=None, create=False, **kwargs):
332 332 """
333 333 Initializes repository. Raises RepositoryError if repository could
334 334 not be find at the given ``repo_path`` or directory at ``repo_path``
335 335 exists and ``create`` is set to True.
336 336
337 337 :param repo_path: local path of the repository
338 338 :param config: repository configuration
339 339 :param create=False: if set to True, would try to create repository.
340 340 :param src_url=None: if set, should be proper url from which repository
341 341 would be cloned; requires ``create`` parameter to be set to True -
342 342 raises RepositoryError if src_url is set and create evaluates to
343 343 False
344 344 """
345 345 raise NotImplementedError
346 346
347 347 def __repr__(self):
348 348 return f'<{self.__class__.__name__} at {self.path}>'
349 349
350 350 def __len__(self):
351 351 return self.count()
352 352
353 353 def __eq__(self, other):
354 354 same_instance = isinstance(other, self.__class__)
355 355 return same_instance and other.path == self.path
356 356
357 357 def __ne__(self, other):
358 358 return not self.__eq__(other)
359 359
360 360 def get_create_shadow_cache_pr_path(self, db_repo):
361 361 path = db_repo.cached_diffs_dir
362 362 if not os.path.exists(path):
363 363 os.makedirs(path, 0o755)
364 364 return path
365 365
366 366 @classmethod
367 367 def get_default_config(cls, default=None):
368 368 config = Config()
369 369 if default and isinstance(default, list):
370 370 for section, key, val in default:
371 371 config.set(section, key, val)
372 372 return config
373 373
374 374 @LazyProperty
375 375 def _remote(self):
376 376 raise NotImplementedError
377 377
378 378 def _heads(self, branch=None):
379 379 return []
380 380
381 381 @LazyProperty
382 382 def EMPTY_COMMIT(self):
383 383 return EmptyCommit(self.EMPTY_COMMIT_ID)
384 384
385 385 @LazyProperty
386 386 def alias(self):
387 387 for k, v in settings.BACKENDS.items():
388 388 if v.split('.')[-1] == str(self.__class__.__name__):
389 389 return k
390 390
391 391 @LazyProperty
392 392 def name(self):
393 393 return safe_str(os.path.basename(self.path))
394 394
395 395 @LazyProperty
396 396 def description(self):
397 397 raise NotImplementedError
398 398
399 399 def refs(self):
400 400 """
401 401 returns a `dict` with branches, bookmarks, tags, and closed_branches
402 402 for this repository
403 403 """
404 404 return dict(
405 405 branches=self.branches,
406 406 branches_closed=self.branches_closed,
407 407 tags=self.tags,
408 408 bookmarks=self.bookmarks
409 409 )
410 410
411 411 @LazyProperty
412 412 def branches(self):
413 413 """
414 414 A `dict` which maps branch names to commit ids.
415 415 """
416 416 raise NotImplementedError
417 417
418 418 @LazyProperty
419 419 def branches_closed(self):
420 420 """
421 421 A `dict` which maps tags names to commit ids.
422 422 """
423 423 raise NotImplementedError
424 424
425 425 @LazyProperty
426 426 def bookmarks(self):
427 427 """
428 428 A `dict` which maps tags names to commit ids.
429 429 """
430 430 raise NotImplementedError
431 431
432 432 @LazyProperty
433 433 def tags(self):
434 434 """
435 435 A `dict` which maps tags names to commit ids.
436 436 """
437 437 raise NotImplementedError
438 438
439 439 @LazyProperty
440 440 def size(self):
441 441 """
442 442 Returns combined size in bytes for all repository files
443 443 """
444 444 tip = self.get_commit()
445 445 return tip.size
446 446
447 447 def size_at_commit(self, commit_id):
448 448 commit = self.get_commit(commit_id)
449 449 return commit.size
450 450
451 451 def _check_for_empty(self):
452 452 no_commits = len(self._commit_ids) == 0
453 453 if no_commits:
454 454 # check on remote to be sure
455 455 return self._remote.is_empty()
456 456 else:
457 457 return False
458 458
459 459 def is_empty(self):
460 460 if rhodecode.is_test:
461 461 return self._check_for_empty()
462 462
463 463 if self._is_empty is None:
464 464 # cache empty for production, but not tests
465 465 self._is_empty = self._check_for_empty()
466 466
467 467 return self._is_empty
468 468
469 469 @staticmethod
470 470 def check_url(url, config):
471 471 """
472 472 Function will check given url and try to verify if it's a valid
473 473 link.
474 474 """
475 475 raise NotImplementedError
476 476
477 477 @staticmethod
478 478 def is_valid_repository(path):
479 479 """
480 480 Check if given `path` contains a valid repository of this backend
481 481 """
482 482 raise NotImplementedError
483 483
484 484 # ==========================================================================
485 485 # COMMITS
486 486 # ==========================================================================
487 487
488 488 @CachedProperty
489 489 def commit_ids(self):
490 490 raise NotImplementedError
491 491
492 492 def append_commit_id(self, commit_id):
493 493 if commit_id not in self.commit_ids:
494 494 self._rebuild_cache(self.commit_ids + [commit_id])
495 495
496 496 # clear cache
497 497 self._invalidate_prop_cache('commit_ids')
498 498 self._is_empty = False
499 499
500 500 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None,
501 501 translate_tag=None, maybe_unreachable=False, reference_obj=None):
502 502 """
503 503 Returns instance of `BaseCommit` class. If `commit_id` and `commit_idx`
504 504 are both None, most recent commit is returned.
505 505
506 506 :param pre_load: Optional. List of commit attributes to load.
507 507
508 508 :raises ``EmptyRepositoryError``: if there are no commits
509 509 """
510 510 raise NotImplementedError
511 511
512 512 def __iter__(self):
513 513 for commit_id in self.commit_ids:
514 514 yield self.get_commit(commit_id=commit_id)
515 515
516 516 def get_commits(
517 517 self, start_id=None, end_id=None, start_date=None, end_date=None,
518 518 branch_name=None, show_hidden=False, pre_load=None, translate_tags=None):
519 519 """
520 520 Returns iterator of `BaseCommit` objects from start to end
521 521 not inclusive. This should behave just like a list, ie. end is not
522 522 inclusive.
523 523
524 524 :param start_id: None or str, must be a valid commit id
525 525 :param end_id: None or str, must be a valid commit id
526 526 :param start_date:
527 527 :param end_date:
528 528 :param branch_name:
529 529 :param show_hidden:
530 530 :param pre_load:
531 531 :param translate_tags:
532 532 """
533 533 raise NotImplementedError
534 534
535 535 def __getitem__(self, key):
536 536 """
537 537 Allows index based access to the commit objects of this repository.
538 538 """
539 539 pre_load = ["author", "branch", "date", "message", "parents"]
540 540 if isinstance(key, slice):
541 541 return self._get_range(key, pre_load)
542 542 return self.get_commit(commit_idx=key, pre_load=pre_load)
543 543
544 544 def _get_range(self, slice_obj, pre_load):
545 545 for commit_id in self.commit_ids.__getitem__(slice_obj):
546 546 yield self.get_commit(commit_id=commit_id, pre_load=pre_load)
547 547
548 548 def count(self):
549 549 return len(self.commit_ids)
550 550
551 551 def tag(self, name, user, commit_id=None, message=None, date=None, **opts):
552 552 """
553 553 Creates and returns a tag for the given ``commit_id``.
554 554
555 555 :param name: name for new tag
556 556 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
557 557 :param commit_id: commit id for which new tag would be created
558 558 :param message: message of the tag's commit
559 559 :param date: date of tag's commit
560 560
561 561 :raises TagAlreadyExistError: if tag with same name already exists
562 562 """
563 563 raise NotImplementedError
564 564
565 565 def remove_tag(self, name, user, message=None, date=None):
566 566 """
567 567 Removes tag with the given ``name``.
568 568
569 569 :param name: name of the tag to be removed
570 570 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
571 571 :param message: message of the tag's removal commit
572 572 :param date: date of tag's removal commit
573 573
574 574 :raises TagDoesNotExistError: if tag with given name does not exists
575 575 """
576 576 raise NotImplementedError
577 577
578 578 def get_diff(
579 579 self, commit1, commit2, path=None, ignore_whitespace=False,
580 580 context=3, path1=None):
581 581 """
582 582 Returns (git like) *diff*, as plain text. Shows changes introduced by
583 583 `commit2` since `commit1`.
584 584
585 585 :param commit1: Entry point from which diff is shown. Can be
586 586 ``self.EMPTY_COMMIT`` - in this case, patch showing all
587 587 the changes since empty state of the repository until `commit2`
588 588 :param commit2: Until which commit changes should be shown.
589 589 :param path: Can be set to a path of a file to create a diff of that
590 590 file. If `path1` is also set, this value is only associated to
591 591 `commit2`.
592 592 :param ignore_whitespace: If set to ``True``, would not show whitespace
593 593 changes. Defaults to ``False``.
594 594 :param context: How many lines before/after changed lines should be
595 595 shown. Defaults to ``3``.
596 596 :param path1: Can be set to a path to associate with `commit1`. This
597 597 parameter works only for backends which support diff generation for
598 598 different paths. Other backends will raise a `ValueError` if `path1`
599 599 is set and has a different value than `path`.
600 600 :param file_path: filter this diff by given path pattern
601 601 """
602 602 raise NotImplementedError
603 603
604 604 def strip(self, commit_id, branch=None):
605 605 """
606 606 Strip given commit_id from the repository
607 607 """
608 608 raise NotImplementedError
609 609
610 610 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
611 611 """
612 612 Return a latest common ancestor commit if one exists for this repo
613 613 `commit_id1` vs `commit_id2` from `repo2`.
614 614
615 615 :param commit_id1: Commit it from this repository to use as a
616 616 target for the comparison.
617 617 :param commit_id2: Source commit id to use for comparison.
618 618 :param repo2: Source repository to use for comparison.
619 619 """
620 620 raise NotImplementedError
621 621
622 622 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
623 623 """
624 624 Compare this repository's revision `commit_id1` with `commit_id2`.
625 625
626 626 Returns a tuple(commits, ancestor) that would be merged from
627 627 `commit_id2`. Doing a normal compare (``merge=False``), ``None``
628 628 will be returned as ancestor.
629 629
630 630 :param commit_id1: Commit it from this repository to use as a
631 631 target for the comparison.
632 632 :param commit_id2: Source commit id to use for comparison.
633 633 :param repo2: Source repository to use for comparison.
634 634 :param merge: If set to ``True`` will do a merge compare which also
635 635 returns the common ancestor.
636 636 :param pre_load: Optional. List of commit attributes to load.
637 637 """
638 638 raise NotImplementedError
639 639
640 640 def merge(self, repo_id, workspace_id, target_ref, source_repo, source_ref,
641 641 user_name='', user_email='', message='', dry_run=False,
642 642 use_rebase=False, close_branch=False):
643 643 """
644 644 Merge the revisions specified in `source_ref` from `source_repo`
645 645 onto the `target_ref` of this repository.
646 646
647 647 `source_ref` and `target_ref` are named tupls with the following
648 648 fields `type`, `name` and `commit_id`.
649 649
650 650 Returns a MergeResponse named tuple with the following fields
651 651 'possible', 'executed', 'source_commit', 'target_commit',
652 652 'merge_commit'.
653 653
654 654 :param repo_id: `repo_id` target repo id.
655 655 :param workspace_id: `workspace_id` unique identifier.
656 656 :param target_ref: `target_ref` points to the commit on top of which
657 657 the `source_ref` should be merged.
658 658 :param source_repo: The repository that contains the commits to be
659 659 merged.
660 660 :param source_ref: `source_ref` points to the topmost commit from
661 661 the `source_repo` which should be merged.
662 662 :param user_name: Merge commit `user_name`.
663 663 :param user_email: Merge commit `user_email`.
664 664 :param message: Merge commit `message`.
665 665 :param dry_run: If `True` the merge will not take place.
666 666 :param use_rebase: If `True` commits from the source will be rebased
667 667 on top of the target instead of being merged.
668 668 :param close_branch: If `True` branch will be close before merging it
669 669 """
670 670 if dry_run:
671 671 message = message or settings.MERGE_DRY_RUN_MESSAGE
672 672 user_email = user_email or settings.MERGE_DRY_RUN_EMAIL
673 673 user_name = user_name or settings.MERGE_DRY_RUN_USER
674 674 else:
675 675 if not user_name:
676 676 raise ValueError('user_name cannot be empty')
677 677 if not user_email:
678 678 raise ValueError('user_email cannot be empty')
679 679 if not message:
680 680 raise ValueError('message cannot be empty')
681 681
682 682 try:
683 683 return self._merge_repo(
684 684 repo_id, workspace_id, target_ref, source_repo,
685 685 source_ref, message, user_name, user_email, dry_run=dry_run,
686 686 use_rebase=use_rebase, close_branch=close_branch)
687 687 except RepositoryError as exc:
688 688 log.exception('Unexpected failure when running merge, dry-run=%s', dry_run)
689 689 return MergeResponse(
690 690 False, False, None, MergeFailureReason.UNKNOWN,
691 691 metadata={'exception': str(exc)})
692 692
693 693 def _merge_repo(self, repo_id, workspace_id, target_ref,
694 694 source_repo, source_ref, merge_message,
695 695 merger_name, merger_email, dry_run=False,
696 696 use_rebase=False, close_branch=False):
697 697 """Internal implementation of merge."""
698 698 raise NotImplementedError
699 699
700 700 def _maybe_prepare_merge_workspace(
701 701 self, repo_id, workspace_id, target_ref, source_ref):
702 702 """
703 703 Create the merge workspace.
704 704
705 705 :param workspace_id: `workspace_id` unique identifier.
706 706 """
707 707 raise NotImplementedError
708 708
709 709 @classmethod
710 710 def _get_legacy_shadow_repository_path(cls, repo_path, workspace_id):
711 711 """
712 712 Legacy version that was used before. We still need it for
713 713 backward compat
714 714 """
715 715 return os.path.join(
716 716 os.path.dirname(repo_path),
717 717 f'.__shadow_{os.path.basename(repo_path)}_{workspace_id}')
718 718
719 719 @classmethod
720 720 def _get_shadow_repository_path(cls, repo_path, repo_id, workspace_id):
721 721 # The name of the shadow repository must start with '.', so it is
722 722 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
723 723 legacy_repository_path = cls._get_legacy_shadow_repository_path(repo_path, workspace_id)
724 724 if os.path.exists(legacy_repository_path):
725 725 return legacy_repository_path
726 726 else:
727 727 return os.path.join(
728 728 os.path.dirname(repo_path),
729 729 f'.__shadow_repo_{repo_id}_{workspace_id}')
730 730
731 731 def cleanup_merge_workspace(self, repo_id, workspace_id):
732 732 """
733 733 Remove merge workspace.
734 734
735 735 This function MUST not fail in case there is no workspace associated to
736 736 the given `workspace_id`.
737 737
738 738 :param workspace_id: `workspace_id` unique identifier.
739 739 """
740 740 shadow_repository_path = self._get_shadow_repository_path(
741 741 self.path, repo_id, workspace_id)
742 742 shadow_repository_path_del = '{}.{}.delete'.format(
743 743 shadow_repository_path, time.time())
744 744
745 745 # move the shadow repo, so it never conflicts with the one used.
746 746 # we use this method because shutil.rmtree had some edge case problems
747 747 # removing symlinked repositories
748 748 if not os.path.isdir(shadow_repository_path):
749 749 return
750 750
751 751 shutil.move(shadow_repository_path, shadow_repository_path_del)
752 752 try:
753 753 shutil.rmtree(shadow_repository_path_del, ignore_errors=False)
754 754 except Exception:
755 755 log.exception('Failed to gracefully remove shadow repo under %s',
756 756 shadow_repository_path_del)
757 757 shutil.rmtree(shadow_repository_path_del, ignore_errors=True)
758 758
759 759 # ========== #
760 760 # COMMIT API #
761 761 # ========== #
762 762
763 763 @LazyProperty
764 764 def in_memory_commit(self):
765 765 """
766 766 Returns :class:`InMemoryCommit` object for this repository.
767 767 """
768 768 raise NotImplementedError
769 769
770 770 # ======================== #
771 771 # UTILITIES FOR SUBCLASSES #
772 772 # ======================== #
773 773
774 774 def _validate_diff_commits(self, commit1, commit2):
775 775 """
776 776 Validates that the given commits are related to this repository.
777 777
778 778 Intended as a utility for sub classes to have a consistent validation
779 779 of input parameters in methods like :meth:`get_diff`.
780 780 """
781 781 self._validate_commit(commit1)
782 782 self._validate_commit(commit2)
783 783 if (isinstance(commit1, EmptyCommit) and
784 784 isinstance(commit2, EmptyCommit)):
785 785 raise ValueError("Cannot compare two empty commits")
786 786
787 787 def _validate_commit(self, commit):
788 788 if not isinstance(commit, BaseCommit):
789 789 raise TypeError(
790 790 "%s is not of type BaseCommit" % repr(commit))
791 791 if commit.repository != self and not isinstance(commit, EmptyCommit):
792 792 raise ValueError(
793 793 "Commit %s must be a valid commit from this repository %s, "
794 794 "related to this repository instead %s." %
795 795 (commit, self, commit.repository))
796 796
797 797 def _validate_commit_id(self, commit_id):
798 798 if not isinstance(commit_id, str):
799 799 raise TypeError(f"commit_id must be a string value got {type(commit_id)} instead")
800 800
801 801 def _validate_commit_idx(self, commit_idx):
802 802 if not isinstance(commit_idx, int):
803 803 raise TypeError(f"commit_idx must be a numeric value, got {type(commit_idx)}")
804 804
805 805 def _validate_branch_name(self, branch_name):
806 806 if branch_name and branch_name not in self.branches_all:
807 807 msg = (f"Branch {branch_name} not found in {self}")
808 808 raise BranchDoesNotExistError(msg)
809 809
810 810 #
811 811 # Supporting deprecated API parts
812 812 # TODO: johbo: consider to move this into a mixin
813 813 #
814 814
815 815 @property
816 816 def EMPTY_CHANGESET(self):
817 817 warnings.warn(
818 818 "Use EMPTY_COMMIT or EMPTY_COMMIT_ID instead", DeprecationWarning)
819 819 return self.EMPTY_COMMIT_ID
820 820
821 821 @property
822 822 def revisions(self):
823 823 warnings.warn("Use commits attribute instead", DeprecationWarning)
824 824 return self.commit_ids
825 825
826 826 @revisions.setter
827 827 def revisions(self, value):
828 828 warnings.warn("Use commits attribute instead", DeprecationWarning)
829 829 self.commit_ids = value
830 830
831 831 def get_changeset(self, revision=None, pre_load=None):
832 832 warnings.warn("Use get_commit instead", DeprecationWarning)
833 833 commit_id = None
834 834 commit_idx = None
835 835 if isinstance(revision, str):
836 836 commit_id = revision
837 837 else:
838 838 commit_idx = revision
839 839 return self.get_commit(
840 840 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
841 841
842 842 def get_changesets(
843 843 self, start=None, end=None, start_date=None, end_date=None,
844 844 branch_name=None, pre_load=None):
845 845 warnings.warn("Use get_commits instead", DeprecationWarning)
846 846 start_id = self._revision_to_commit(start)
847 847 end_id = self._revision_to_commit(end)
848 848 return self.get_commits(
849 849 start_id=start_id, end_id=end_id, start_date=start_date,
850 850 end_date=end_date, branch_name=branch_name, pre_load=pre_load)
851 851
852 852 def _revision_to_commit(self, revision):
853 853 """
854 854 Translates a revision to a commit_id
855 855
856 856 Helps to support the old changeset based API which allows to use
857 857 commit ids and commit indices interchangeable.
858 858 """
859 859 if revision is None:
860 860 return revision
861 861
862 862 if isinstance(revision, str):
863 863 commit_id = revision
864 864 else:
865 865 commit_id = self.commit_ids[revision]
866 866 return commit_id
867 867
868 868 @property
869 869 def in_memory_changeset(self):
870 870 warnings.warn("Use in_memory_commit instead", DeprecationWarning)
871 871 return self.in_memory_commit
872 872
873 873 def get_path_permissions(self, username):
874 874 """
875 875 Returns a path permission checker or None if not supported
876 876
877 877 :param username: session user name
878 878 :return: an instance of BasePathPermissionChecker or None
879 879 """
880 880 return None
881 881
882 882 def install_hooks(self, force=False):
883 883 return self._remote.install_hooks(force)
884 884
885 885 def get_hooks_info(self):
886 886 return self._remote.get_hooks_info()
887 887
888 888 def vcsserver_invalidate_cache(self, delete=False):
889 889 return self._remote.vcsserver_invalidate_cache(delete)
890 890
891 891
892 892 class BaseCommit(object):
893 893 """
894 894 Each backend should implement it's commit representation.
895 895
896 896 **Attributes**
897 897
898 898 ``repository``
899 899 repository object within which commit exists
900 900
901 901 ``id``
902 902 The commit id, may be ``raw_id`` or i.e. for mercurial's tip
903 903 just ``tip``.
904 904
905 905 ``raw_id``
906 906 raw commit representation (i.e. full 40 length sha for git
907 907 backend)
908 908
909 909 ``short_id``
910 910 shortened (if apply) version of ``raw_id``; it would be simple
911 911 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
912 912 as ``raw_id`` for subversion
913 913
914 914 ``idx``
915 915 commit index
916 916
917 917 ``files``
918 918 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
919 919
920 920 ``dirs``
921 921 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
922 922
923 923 ``nodes``
924 924 combined list of ``Node`` objects
925 925
926 926 ``author``
927 927 author of the commit, as unicode
928 928
929 929 ``message``
930 930 message of the commit, as unicode
931 931
932 932 ``parents``
933 933 list of parent commits
934 934
935 935 """
936 936 repository = None
937 937 branch = None
938 938
939 939 """
940 940 Depending on the backend this should be set to the branch name of the
941 941 commit. Backends not supporting branches on commits should leave this
942 942 value as ``None``.
943 943 """
944 944
945 945 _ARCHIVE_PREFIX_TEMPLATE = '{repo_name}-{short_id}'
946 946 """
947 947 This template is used to generate a default prefix for repository archives
948 948 if no prefix has been specified.
949 949 """
950 950
951 951 def __repr__(self):
952 952 return self.__str__()
953 953
954 954 def __str__(self):
955 955 return f'<{self.__class__.__name__} at {self.idx}:{self.short_id}>'
956 956
957 957 def __eq__(self, other):
958 958 same_instance = isinstance(other, self.__class__)
959 959 return same_instance and self.raw_id == other.raw_id
960 960
961 961 def __json__(self):
962 962 parents = []
963 963 try:
964 964 for parent in self.parents:
965 965 parents.append({'raw_id': parent.raw_id})
966 966 except NotImplementedError:
967 967 # empty commit doesn't have parents implemented
968 968 pass
969 969
970 970 return {
971 971 'short_id': self.short_id,
972 972 'raw_id': self.raw_id,
973 973 'revision': self.idx,
974 974 'message': self.message,
975 975 'date': self.date,
976 976 'author': self.author,
977 977 'parents': parents,
978 978 'branch': self.branch
979 979 }
980 980
981 981 def __getstate__(self):
982 982 d = self.__dict__.copy()
983 983 d.pop('_remote', None)
984 984 d.pop('repository', None)
985 985 return d
986 986
987 987 def get_remote(self):
988 988 return self._remote
989 989
990 990 def serialize(self):
991 991 return self.__json__()
992 992
993 993 def _get_refs(self):
994 994 return {
995 995 'branches': [self.branch] if self.branch else [],
996 996 'bookmarks': getattr(self, 'bookmarks', []),
997 997 'tags': self.tags
998 998 }
999 999
1000 1000 @LazyProperty
1001 1001 def last(self):
1002 1002 """
1003 1003 ``True`` if this is last commit in repository, ``False``
1004 1004 otherwise; trying to access this attribute while there is no
1005 1005 commits would raise `EmptyRepositoryError`
1006 1006 """
1007 1007 if self.repository is None:
1008 1008 raise CommitError("Cannot check if it's most recent commit")
1009 1009 return self.raw_id == self.repository.commit_ids[-1]
1010 1010
1011 1011 @LazyProperty
1012 1012 def parents(self):
1013 1013 """
1014 1014 Returns list of parent commits.
1015 1015 """
1016 1016 raise NotImplementedError
1017 1017
1018 1018 @LazyProperty
1019 1019 def first_parent(self):
1020 1020 """
1021 1021 Returns list of parent commits.
1022 1022 """
1023 1023 return self.parents[0] if self.parents else EmptyCommit()
1024 1024
1025 1025 @property
1026 1026 def merge(self):
1027 1027 """
1028 1028 Returns boolean if commit is a merge.
1029 1029 """
1030 1030 return len(self.parents) > 1
1031 1031
1032 1032 @LazyProperty
1033 1033 def children(self):
1034 1034 """
1035 1035 Returns list of child commits.
1036 1036 """
1037 1037 raise NotImplementedError
1038 1038
1039 1039 @LazyProperty
1040 1040 def id(self):
1041 1041 """
1042 1042 Returns string identifying this commit.
1043 1043 """
1044 1044 raise NotImplementedError
1045 1045
1046 1046 @LazyProperty
1047 1047 def raw_id(self):
1048 1048 """
1049 1049 Returns raw string identifying this commit.
1050 1050 """
1051 1051 raise NotImplementedError
1052 1052
1053 1053 @LazyProperty
1054 1054 def short_id(self):
1055 1055 """
1056 1056 Returns shortened version of ``raw_id`` attribute, as string,
1057 1057 identifying this commit, useful for presentation to users.
1058 1058 """
1059 1059 raise NotImplementedError
1060 1060
1061 1061 @LazyProperty
1062 1062 def idx(self):
1063 1063 """
1064 1064 Returns integer identifying this commit.
1065 1065 """
1066 1066 raise NotImplementedError
1067 1067
1068 1068 @LazyProperty
1069 1069 def committer(self):
1070 1070 """
1071 1071 Returns committer for this commit
1072 1072 """
1073 1073 raise NotImplementedError
1074 1074
1075 1075 @LazyProperty
1076 1076 def committer_name(self):
1077 1077 """
1078 1078 Returns committer name for this commit
1079 1079 """
1080 1080
1081 1081 return author_name(self.committer)
1082 1082
1083 1083 @LazyProperty
1084 1084 def committer_email(self):
1085 1085 """
1086 1086 Returns committer email address for this commit
1087 1087 """
1088 1088
1089 1089 return author_email(self.committer)
1090 1090
1091 1091 @LazyProperty
1092 1092 def author(self):
1093 1093 """
1094 1094 Returns author for this commit
1095 1095 """
1096 1096
1097 1097 raise NotImplementedError
1098 1098
1099 1099 @LazyProperty
1100 1100 def author_name(self):
1101 1101 """
1102 1102 Returns author name for this commit
1103 1103 """
1104 1104
1105 1105 return author_name(self.author)
1106 1106
1107 1107 @LazyProperty
1108 1108 def author_email(self):
1109 1109 """
1110 1110 Returns author email address for this commit
1111 1111 """
1112 1112
1113 1113 return author_email(self.author)
1114 1114
1115 1115 def get_file_mode(self, path: bytes):
1116 1116 """
1117 1117 Returns stat mode of the file at `path`.
1118 1118 """
1119 1119 raise NotImplementedError
1120 1120
1121 1121 def is_link(self, path):
1122 1122 """
1123 1123 Returns ``True`` if given `path` is a symlink
1124 1124 """
1125 1125 raise NotImplementedError
1126 1126
1127 1127 def is_node_binary(self, path):
1128 1128 """
1129 1129 Returns ``True`` is given path is a binary file
1130 1130 """
1131 1131 raise NotImplementedError
1132 1132
1133 1133 def node_md5_hash(self, path):
1134 1134 """
1135 1135 Returns md5 hash of a node data
1136 1136 """
1137 1137 raise NotImplementedError
1138 1138
1139 1139 def get_file_content(self, path) -> bytes:
1140 1140 """
1141 1141 Returns content of the file at the given `path`.
1142 1142 """
1143 1143 raise NotImplementedError
1144 1144
1145 1145 def get_file_content_streamed(self, path):
1146 1146 """
1147 1147 returns a streaming response from vcsserver with file content
1148 1148 """
1149 1149 raise NotImplementedError
1150 1150
1151 1151 def get_file_size(self, path):
1152 1152 """
1153 1153 Returns size of the file at the given `path`.
1154 1154 """
1155 1155 raise NotImplementedError
1156 1156
1157 1157 def get_path_commit(self, path, pre_load=None):
1158 1158 """
1159 1159 Returns last commit of the file at the given `path`.
1160 1160
1161 1161 :param pre_load: Optional. List of commit attributes to load.
1162 1162 """
1163 1163 commits = self.get_path_history(path, limit=1, pre_load=pre_load)
1164 1164 if not commits:
1165 1165 raise RepositoryError(
1166 1166 'Failed to fetch history for path {}. '
1167 1167 'Please check if such path exists in your repository'.format(
1168 1168 path))
1169 1169 return commits[0]
1170 1170
1171 1171 def get_path_history(self, path, limit=None, pre_load=None):
1172 1172 """
1173 1173 Returns history of file as reversed list of :class:`BaseCommit`
1174 1174 objects for which file at given `path` has been modified.
1175 1175
1176 1176 :param limit: Optional. Allows to limit the size of the returned
1177 1177 history. This is intended as a hint to the underlying backend, so
1178 1178 that it can apply optimizations depending on the limit.
1179 1179 :param pre_load: Optional. List of commit attributes to load.
1180 1180 """
1181 1181 raise NotImplementedError
1182 1182
1183 1183 def get_file_annotate(self, path, pre_load=None):
1184 1184 """
1185 1185 Returns a generator of four element tuples with
1186 1186 lineno, sha, commit lazy loader and line
1187 1187
1188 1188 :param pre_load: Optional. List of commit attributes to load.
1189 1189 """
1190 1190 raise NotImplementedError
1191 1191
1192 1192 def get_nodes(self, path, pre_load=None):
1193 1193 """
1194 1194 Returns combined ``DirNode`` and ``FileNode`` objects list representing
1195 1195 state of commit at the given ``path``.
1196 1196
1197 1197 :raises ``CommitError``: if node at the given ``path`` is not
1198 1198 instance of ``DirNode``
1199 1199 """
1200 1200 raise NotImplementedError
1201 1201
1202 1202 def get_node(self, path):
1203 1203 """
1204 1204 Returns ``Node`` object from the given ``path``.
1205 1205
1206 1206 :raises ``NodeDoesNotExistError``: if there is no node at the given
1207 1207 ``path``
1208 1208 """
1209 1209 raise NotImplementedError
1210 1210
1211 1211 def get_largefile_node(self, path):
1212 1212 """
1213 1213 Returns the path to largefile from Mercurial/Git-lfs storage.
1214 1214 or None if it's not a largefile node
1215 1215 """
1216 1216 return None
1217 1217
1218 1218 def archive_repo(self, archive_name_key, kind='tgz', subrepos=None,
1219 1219 archive_dir_name=None, write_metadata=False, mtime=None,
1220 1220 archive_at_path='/', cache_config=None):
1221 1221 """
1222 1222 Creates an archive containing the contents of the repository.
1223 1223
1224 1224 :param archive_name_key: unique key under this archive should be generated
1225 :param kind: one of following: ``"tbz2"``, ``"tgz"``, ``"zip"``.
1225 :param kind: one of the following: ``"tbz2"``, ``"tgz"``, ``"zip"``.
1226 1226 :param archive_dir_name: name of root directory in archive.
1227 1227 Default is repository name and commit's short_id joined with dash:
1228 1228 ``"{repo_name}-{short_id}"``.
1229 1229 :param write_metadata: write a metadata file into archive.
1230 1230 :param mtime: custom modification time for archive creation, defaults
1231 1231 to time.time() if not given.
1232 1232 :param archive_at_path: pack files at this path (default '/')
1233 1233 :param cache_config: config spec to send to vcsserver to configure the backend to store files
1234 1234
1235 1235 :raise VCSError: If prefix has a problem.
1236 1236 """
1237 1237 cache_config = cache_config or {}
1238 1238 allowed_kinds = [x[0] for x in settings.ARCHIVE_SPECS]
1239 1239 if kind not in allowed_kinds:
1240 1240 raise ImproperArchiveTypeError(
1241 'Archive kind (%s) not supported use one of %s' %
1242 (kind, allowed_kinds))
1241 f'Archive kind ({kind}) not supported use one of {allowed_kinds}')
1243 1242
1244 1243 archive_dir_name = self._validate_archive_prefix(archive_dir_name)
1245 1244 mtime = mtime is not None or time.mktime(self.date.timetuple())
1246 1245 commit_id = self.raw_id
1247 1246
1248 1247 return self.repository._remote.archive_repo(
1249 1248 archive_name_key, kind, mtime, archive_at_path,
1250 1249 archive_dir_name, commit_id, cache_config)
1251 1250
1252 1251 def _validate_archive_prefix(self, archive_dir_name):
1253 1252 if archive_dir_name is None:
1254 1253 archive_dir_name = self._ARCHIVE_PREFIX_TEMPLATE.format(
1255 1254 repo_name=safe_str(self.repository.name),
1256 1255 short_id=self.short_id)
1257 1256 elif not isinstance(archive_dir_name, str):
1258 1257 raise ValueError(f"archive_dir_name is not str object but: {type(archive_dir_name)}")
1259 1258 elif archive_dir_name.startswith('/'):
1260 1259 raise VCSError("Prefix cannot start with leading slash")
1261 1260 elif archive_dir_name.strip() == '':
1262 1261 raise VCSError("Prefix cannot be empty")
1263 1262 elif not archive_dir_name.isascii():
1264 1263 raise VCSError("Prefix cannot contain non ascii characters")
1265 1264 return archive_dir_name
1266 1265
1267 1266 @LazyProperty
1268 1267 def root(self):
1269 1268 """
1270 1269 Returns ``RootNode`` object for this commit.
1271 1270 """
1272 1271 return self.get_node('')
1273 1272
1274 1273 def next(self, branch=None):
1275 1274 """
1276 1275 Returns next commit from current, if branch is gives it will return
1277 1276 next commit belonging to this branch
1278 1277
1279 1278 :param branch: show commits within the given named branch
1280 1279 """
1281 1280 indexes = range(self.idx + 1, self.repository.count())
1282 1281 return self._find_next(indexes, branch)
1283 1282
1284 1283 def prev(self, branch=None):
1285 1284 """
1286 1285 Returns previous commit from current, if branch is gives it will
1287 1286 return previous commit belonging to this branch
1288 1287
1289 1288 :param branch: show commit within the given named branch
1290 1289 """
1291 1290 indexes = range(self.idx - 1, -1, -1)
1292 1291 return self._find_next(indexes, branch)
1293 1292
1294 1293 def _find_next(self, indexes, branch=None):
1295 1294 if branch and self.branch != branch:
1296 1295 raise VCSError('Branch option used on commit not belonging '
1297 1296 'to that branch')
1298 1297
1299 1298 for next_idx in indexes:
1300 1299 commit = self.repository.get_commit(commit_idx=next_idx)
1301 1300 if branch and branch != commit.branch:
1302 1301 continue
1303 1302 return commit
1304 1303 raise CommitDoesNotExistError
1305 1304
1306 1305 def diff(self, ignore_whitespace=True, context=3):
1307 1306 """
1308 1307 Returns a `Diff` object representing the change made by this commit.
1309 1308 """
1310 1309 parent = self.first_parent
1311 1310 diff = self.repository.get_diff(
1312 1311 parent, self,
1313 1312 ignore_whitespace=ignore_whitespace,
1314 1313 context=context)
1315 1314 return diff
1316 1315
1317 1316 @LazyProperty
1318 1317 def added(self):
1319 1318 """
1320 1319 Returns list of added ``FileNode`` objects.
1321 1320 """
1322 1321 raise NotImplementedError
1323 1322
1324 1323 @LazyProperty
1325 1324 def changed(self):
1326 1325 """
1327 1326 Returns list of modified ``FileNode`` objects.
1328 1327 """
1329 1328 raise NotImplementedError
1330 1329
1331 1330 @LazyProperty
1332 1331 def removed(self):
1333 1332 """
1334 1333 Returns list of removed ``FileNode`` objects.
1335 1334 """
1336 1335 raise NotImplementedError
1337 1336
1338 1337 @LazyProperty
1339 1338 def size(self):
1340 1339 """
1341 1340 Returns total number of bytes from contents of all filenodes.
1342 1341 """
1343 1342 return sum(node.size for node in self.get_filenodes_generator())
1344 1343
1345 1344 def walk(self, topurl=''):
1346 1345 """
1347 1346 Similar to os.walk method. Insted of filesystem it walks through
1348 1347 commit starting at given ``topurl``. Returns generator of tuples
1349 1348 (top_node, dirnodes, filenodes).
1350 1349 """
1351 1350 from rhodecode.lib.vcs.nodes import DirNode
1352 1351
1353 1352 if isinstance(topurl, DirNode):
1354 1353 top_node = topurl
1355 1354 else:
1356 1355 top_node = self.get_node(topurl)
1357 1356
1358 1357 has_default_pre_load = False
1359 1358 if isinstance(top_node, DirNode):
1360 1359 # used to inject as we walk same defaults as given top_node
1361 1360 default_pre_load = top_node.default_pre_load
1362 1361 has_default_pre_load = True
1363 1362
1364 1363 if not top_node.is_dir():
1365 1364 return
1366 1365 yield top_node, top_node.dirs, top_node.files
1367 1366 for dir_node in top_node.dirs:
1368 1367 if has_default_pre_load:
1369 1368 dir_node.default_pre_load = default_pre_load
1370 1369 yield from self.walk(dir_node)
1371 1370
1372 1371 def get_filenodes_generator(self):
1373 1372 """
1374 1373 Returns generator that yields *all* file nodes.
1375 1374 """
1376 1375 for topnode, dirs, files in self.walk():
1377 1376 yield from files
1378 1377
1379 1378 #
1380 1379 # Utilities for sub classes to support consistent behavior
1381 1380 #
1382 1381
1383 1382 def no_node_at_path(self, path):
1384 1383 return NodeDoesNotExistError(
1385 1384 f"There is no file nor directory at the given path: "
1386 1385 f"`{safe_str(path)}` at commit {self.short_id}")
1387 1386
1388 1387 def _fix_path(self, path: str) -> str:
1389 1388 """
1390 1389 Paths are stored without trailing slash so we need to get rid off it if
1391 1390 needed.
1392 1391 """
1393 1392 return safe_str(path).rstrip('/')
1394 1393
1395 1394 #
1396 1395 # Deprecated API based on changesets
1397 1396 #
1398 1397
1399 1398 @property
1400 1399 def revision(self):
1401 1400 warnings.warn("Use idx instead", DeprecationWarning)
1402 1401 return self.idx
1403 1402
1404 1403 @revision.setter
1405 1404 def revision(self, value):
1406 1405 warnings.warn("Use idx instead", DeprecationWarning)
1407 1406 self.idx = value
1408 1407
1409 1408 def get_file_changeset(self, path):
1410 1409 warnings.warn("Use get_path_commit instead", DeprecationWarning)
1411 1410 return self.get_path_commit(path)
1412 1411
1413 1412
1414 1413 class BaseChangesetClass(type):
1415 1414
1416 1415 def __instancecheck__(self, instance):
1417 1416 return isinstance(instance, BaseCommit)
1418 1417
1419 1418
1420 1419 class BaseChangeset(BaseCommit, metaclass=BaseChangesetClass):
1421 1420
1422 1421 def __new__(cls, *args, **kwargs):
1423 1422 warnings.warn(
1424 1423 "Use BaseCommit instead of BaseChangeset", DeprecationWarning)
1425 1424 return super().__new__(cls, *args, **kwargs)
1426 1425
1427 1426
1428 1427 class BaseInMemoryCommit(object):
1429 1428 """
1430 1429 Represents differences between repository's state (most recent head) and
1431 1430 changes made *in place*.
1432 1431
1433 1432 **Attributes**
1434 1433
1435 1434 ``repository``
1436 1435 repository object for this in-memory-commit
1437 1436
1438 1437 ``added``
1439 1438 list of ``FileNode`` objects marked as *added*
1440 1439
1441 1440 ``changed``
1442 1441 list of ``FileNode`` objects marked as *changed*
1443 1442
1444 1443 ``removed``
1445 1444 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
1446 1445 *removed*
1447 1446
1448 1447 ``parents``
1449 1448 list of :class:`BaseCommit` instances representing parents of
1450 1449 in-memory commit. Should always be 2-element sequence.
1451 1450
1452 1451 """
1453 1452
1454 1453 def __init__(self, repository):
1455 1454 self.repository = repository
1456 1455 self.added = []
1457 1456 self.changed = []
1458 1457 self.removed = []
1459 1458 self.parents = []
1460 1459
1461 1460 def add(self, *filenodes):
1462 1461 """
1463 1462 Marks given ``FileNode`` objects as *to be committed*.
1464 1463
1465 1464 :raises ``NodeAlreadyExistsError``: if node with same path exists at
1466 1465 latest commit
1467 1466 :raises ``NodeAlreadyAddedError``: if node with same path is already
1468 1467 marked as *added*
1469 1468 """
1470 1469 # Check if not already marked as *added* first
1471 1470 for node in filenodes:
1472 1471 if node.path in (n.path for n in self.added):
1473 1472 raise NodeAlreadyAddedError(
1474 1473 "Such FileNode %s is already marked for addition"
1475 1474 % node.path)
1476 1475 for node in filenodes:
1477 1476 self.added.append(node)
1478 1477
1479 1478 def change(self, *filenodes):
1480 1479 """
1481 1480 Marks given ``FileNode`` objects to be *changed* in next commit.
1482 1481
1483 1482 :raises ``EmptyRepositoryError``: if there are no commits yet
1484 1483 :raises ``NodeAlreadyExistsError``: if node with same path is already
1485 1484 marked to be *changed*
1486 1485 :raises ``NodeAlreadyRemovedError``: if node with same path is already
1487 1486 marked to be *removed*
1488 1487 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
1489 1488 commit
1490 1489 :raises ``NodeNotChangedError``: if node hasn't really be changed
1491 1490 """
1492 1491 for node in filenodes:
1493 1492 if node.path in (n.path for n in self.removed):
1494 1493 raise NodeAlreadyRemovedError(
1495 1494 "Node at %s is already marked as removed" % node.path)
1496 1495 try:
1497 1496 self.repository.get_commit()
1498 1497 except EmptyRepositoryError:
1499 1498 raise EmptyRepositoryError(
1500 1499 "Nothing to change - try to *add* new nodes rather than "
1501 1500 "changing them")
1502 1501 for node in filenodes:
1503 1502 if node.path in (n.path for n in self.changed):
1504 1503 raise NodeAlreadyChangedError(
1505 1504 "Node at '%s' is already marked as changed" % node.path)
1506 1505 self.changed.append(node)
1507 1506
1508 1507 def remove(self, *filenodes):
1509 1508 """
1510 1509 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
1511 1510 *removed* in next commit.
1512 1511
1513 1512 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
1514 1513 be *removed*
1515 1514 :raises ``NodeAlreadyChangedError``: if node has been already marked to
1516 1515 be *changed*
1517 1516 """
1518 1517 for node in filenodes:
1519 1518 if node.path in (n.path for n in self.removed):
1520 1519 raise NodeAlreadyRemovedError(
1521 1520 "Node is already marked to for removal at %s" % node.path)
1522 1521 if node.path in (n.path for n in self.changed):
1523 1522 raise NodeAlreadyChangedError(
1524 1523 "Node is already marked to be changed at %s" % node.path)
1525 1524 # We only mark node as *removed* - real removal is done by
1526 1525 # commit method
1527 1526 self.removed.append(node)
1528 1527
1529 1528 def reset(self):
1530 1529 """
1531 1530 Resets this instance to initial state (cleans ``added``, ``changed``
1532 1531 and ``removed`` lists).
1533 1532 """
1534 1533 self.added = []
1535 1534 self.changed = []
1536 1535 self.removed = []
1537 1536 self.parents = []
1538 1537
1539 1538 def get_ipaths(self):
1540 1539 """
1541 1540 Returns generator of paths from nodes marked as added, changed or
1542 1541 removed.
1543 1542 """
1544 1543 for node in itertools.chain(self.added, self.changed, self.removed):
1545 1544 yield node.path
1546 1545
1547 1546 def get_paths(self):
1548 1547 """
1549 1548 Returns list of paths from nodes marked as added, changed or removed.
1550 1549 """
1551 1550 return list(self.get_ipaths())
1552 1551
1553 1552 def check_integrity(self, parents=None):
1554 1553 """
1555 1554 Checks in-memory commit's integrity. Also, sets parents if not
1556 1555 already set.
1557 1556
1558 1557 :raises CommitError: if any error occurs (i.e.
1559 1558 ``NodeDoesNotExistError``).
1560 1559 """
1561 1560 if not self.parents:
1562 1561 parents = parents or []
1563 1562 if len(parents) == 0:
1564 1563 try:
1565 1564 parents = [self.repository.get_commit(), None]
1566 1565 except EmptyRepositoryError:
1567 1566 parents = [None, None]
1568 1567 elif len(parents) == 1:
1569 1568 parents += [None]
1570 1569 self.parents = parents
1571 1570
1572 1571 # Local parents, only if not None
1573 1572 parents = [p for p in self.parents if p]
1574 1573
1575 1574 # Check nodes marked as added
1576 1575 for p in parents:
1577 1576 for node in self.added:
1578 1577 try:
1579 1578 p.get_node(node.path)
1580 1579 except NodeDoesNotExistError:
1581 1580 pass
1582 1581 else:
1583 1582 raise NodeAlreadyExistsError(
1584 1583 f"Node `{node.path}` already exists at {p}")
1585 1584
1586 1585 # Check nodes marked as changed
1587 1586 missing = set(self.changed)
1588 1587 not_changed = set(self.changed)
1589 1588 if self.changed and not parents:
1590 1589 raise NodeDoesNotExistError(str(self.changed[0].path))
1591 1590 for p in parents:
1592 1591 for node in self.changed:
1593 1592 try:
1594 1593 old = p.get_node(node.path)
1595 1594 missing.remove(node)
1596 1595 # if content actually changed, remove node from not_changed
1597 1596 if old.content != node.content:
1598 1597 not_changed.remove(node)
1599 1598 except NodeDoesNotExistError:
1600 1599 pass
1601 1600 if self.changed and missing:
1602 1601 raise NodeDoesNotExistError(
1603 1602 "Node `%s` marked as modified but missing in parents: %s"
1604 1603 % (node.path, parents))
1605 1604
1606 1605 if self.changed and not_changed:
1607 1606 raise NodeNotChangedError(
1608 1607 "Node `%s` wasn't actually changed (parents: %s)"
1609 1608 % (not_changed.pop().path, parents))
1610 1609
1611 1610 # Check nodes marked as removed
1612 1611 if self.removed and not parents:
1613 1612 raise NodeDoesNotExistError(
1614 1613 "Cannot remove node at %s as there "
1615 1614 "were no parents specified" % self.removed[0].path)
1616 1615 really_removed = set()
1617 1616 for p in parents:
1618 1617 for node in self.removed:
1619 1618 try:
1620 1619 p.get_node(node.path)
1621 1620 really_removed.add(node)
1622 1621 except CommitError:
1623 1622 pass
1624 1623 not_removed = set(self.removed) - really_removed
1625 1624 if not_removed:
1626 1625 # TODO: johbo: This code branch does not seem to be covered
1627 1626 raise NodeDoesNotExistError(
1628 1627 "Cannot remove node at %s from "
1629 1628 "following parents: %s" % (not_removed, parents))
1630 1629
1631 1630 def commit(self, message, author, parents=None, branch=None, date=None, **kwargs):
1632 1631 """
1633 1632 Performs in-memory commit (doesn't check workdir in any way) and
1634 1633 returns newly created :class:`BaseCommit`. Updates repository's
1635 1634 attribute `commits`.
1636 1635
1637 1636 .. note::
1638 1637
1639 1638 While overriding this method each backend's should call
1640 1639 ``self.check_integrity(parents)`` in the first place.
1641 1640
1642 1641 :param message: message of the commit
1643 1642 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
1644 1643 :param parents: single parent or sequence of parents from which commit
1645 1644 would be derived
1646 1645 :param date: ``datetime.datetime`` instance. Defaults to
1647 1646 ``datetime.datetime.now()``.
1648 1647 :param branch: branch name, as string. If none given, default backend's
1649 1648 branch would be used.
1650 1649
1651 1650 :raises ``CommitError``: if any error occurs while committing
1652 1651 """
1653 1652 raise NotImplementedError
1654 1653
1655 1654
1656 1655 class BaseInMemoryChangesetClass(type):
1657 1656
1658 1657 def __instancecheck__(self, instance):
1659 1658 return isinstance(instance, BaseInMemoryCommit)
1660 1659
1661 1660
1662 1661 class BaseInMemoryChangeset(BaseInMemoryCommit, metaclass=BaseInMemoryChangesetClass):
1663 1662
1664 1663 def __new__(cls, *args, **kwargs):
1665 1664 warnings.warn(
1666 1665 "Use BaseCommit instead of BaseInMemoryCommit", DeprecationWarning)
1667 1666 return super().__new__(cls, *args, **kwargs)
1668 1667
1669 1668
1670 1669 class EmptyCommit(BaseCommit):
1671 1670 """
1672 1671 An dummy empty commit. It's possible to pass hash when creating
1673 1672 an EmptyCommit
1674 1673 """
1675 1674
1676 1675 def __init__(
1677 1676 self, commit_id=EMPTY_COMMIT_ID, repo=None, alias=None, idx=-1,
1678 1677 message='', author='', date=None):
1679 1678 self._empty_commit_id = commit_id
1680 1679 # TODO: johbo: Solve idx parameter, default value does not make
1681 1680 # too much sense
1682 1681 self.idx = idx
1683 1682 self.message = message
1684 1683 self.author = author
1685 1684 self.date = date or datetime.datetime.fromtimestamp(0)
1686 1685 self.repository = repo
1687 1686 self.alias = alias
1688 1687
1689 1688 @LazyProperty
1690 1689 def raw_id(self):
1691 1690 """
1692 1691 Returns raw string identifying this commit, useful for web
1693 1692 representation.
1694 1693 """
1695 1694
1696 1695 return self._empty_commit_id
1697 1696
1698 1697 @LazyProperty
1699 1698 def branch(self):
1700 1699 if self.alias:
1701 1700 from rhodecode.lib.vcs.backends import get_backend
1702 1701 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1703 1702
1704 1703 @LazyProperty
1705 1704 def short_id(self):
1706 1705 return self.raw_id[:12]
1707 1706
1708 1707 @LazyProperty
1709 1708 def id(self):
1710 1709 return self.raw_id
1711 1710
1712 1711 def get_path_commit(self, path, pre_load=None):
1713 1712 return self
1714 1713
1715 1714 def get_file_content(self, path) -> bytes:
1716 1715 return b''
1717 1716
1718 1717 def get_file_content_streamed(self, path):
1719 1718 yield self.get_file_content(path)
1720 1719
1721 1720 def get_file_size(self, path):
1722 1721 return 0
1723 1722
1724 1723
1725 1724 class EmptyChangesetClass(type):
1726 1725
1727 1726 def __instancecheck__(self, instance):
1728 1727 return isinstance(instance, EmptyCommit)
1729 1728
1730 1729
1731 1730 class EmptyChangeset(EmptyCommit, metaclass=EmptyChangesetClass):
1732 1731
1733 1732 def __new__(cls, *args, **kwargs):
1734 1733 warnings.warn(
1735 1734 "Use EmptyCommit instead of EmptyChangeset", DeprecationWarning)
1736 1735 return super(EmptyCommit, cls).__new__(cls, *args, **kwargs)
1737 1736
1738 1737 def __init__(self, cs=EMPTY_COMMIT_ID, repo=None, requested_revision=None,
1739 1738 alias=None, revision=-1, message='', author='', date=None):
1740 1739 if requested_revision is not None:
1741 1740 warnings.warn(
1742 1741 "Parameter requested_revision not supported anymore",
1743 1742 DeprecationWarning)
1744 1743 super().__init__(
1745 1744 commit_id=cs, repo=repo, alias=alias, idx=revision,
1746 1745 message=message, author=author, date=date)
1747 1746
1748 1747 @property
1749 1748 def revision(self):
1750 1749 warnings.warn("Use idx instead", DeprecationWarning)
1751 1750 return self.idx
1752 1751
1753 1752 @revision.setter
1754 1753 def revision(self, value):
1755 1754 warnings.warn("Use idx instead", DeprecationWarning)
1756 1755 self.idx = value
1757 1756
1758 1757
1759 1758 class EmptyRepository(BaseRepository):
1760 1759 def __init__(self, repo_path=None, config=None, create=False, **kwargs):
1761 1760 pass
1762 1761
1763 1762 def get_diff(self, *args, **kwargs):
1764 1763 from rhodecode.lib.vcs.backends.git.diff import GitDiff
1765 1764 return GitDiff(b'')
1766 1765
1767 1766
1768 1767 class CollectionGenerator(object):
1769 1768
1770 1769 def __init__(self, repo, commit_ids, collection_size=None, pre_load=None, translate_tag=None):
1771 1770 self.repo = repo
1772 1771 self.commit_ids = commit_ids
1773 1772 self.collection_size = collection_size
1774 1773 self.pre_load = pre_load
1775 1774 self.translate_tag = translate_tag
1776 1775
1777 1776 def __len__(self):
1778 1777 if self.collection_size is not None:
1779 1778 return self.collection_size
1780 1779 return self.commit_ids.__len__()
1781 1780
1782 1781 def __iter__(self):
1783 1782 for commit_id in self.commit_ids:
1784 1783 # TODO: johbo: Mercurial passes in commit indices or commit ids
1785 1784 yield self._commit_factory(commit_id)
1786 1785
1787 1786 def _commit_factory(self, commit_id):
1788 1787 """
1789 1788 Allows backends to override the way commits are generated.
1790 1789 """
1791 1790 return self.repo.get_commit(
1792 1791 commit_id=commit_id, pre_load=self.pre_load,
1793 1792 translate_tag=self.translate_tag)
1794 1793
1795 1794 def __getitem__(self, key):
1796 1795 """Return either a single element by index, or a sliced collection."""
1797 1796
1798 1797 if isinstance(key, slice):
1799 1798 commit_ids = self.commit_ids[key.start:key.stop]
1800 1799
1801 1800 else:
1802 1801 # single item
1803 1802 commit_ids = self.commit_ids[key]
1804 1803
1805 1804 return self.__class__(
1806 1805 self.repo, commit_ids, pre_load=self.pre_load,
1807 1806 translate_tag=self.translate_tag)
1808 1807
1809 1808 def __repr__(self):
1810 1809 return '<CollectionGenerator[len:%s]>' % (self.__len__())
1811 1810
1812 1811
1813 1812 class Config(object):
1814 1813 """
1815 1814 Represents the configuration for a repository.
1816 1815
1817 1816 The API is inspired by :class:`ConfigParser.ConfigParser` from the
1818 1817 standard library. It implements only the needed subset.
1819 1818 """
1820 1819
1821 1820 def __init__(self):
1822 1821 self._values = {}
1823 1822
1824 1823 def copy(self):
1825 1824 clone = Config()
1826 1825 for section, values in self._values.items():
1827 1826 clone._values[section] = values.copy()
1828 1827 return clone
1829 1828
1830 1829 def __repr__(self):
1831 1830 return '<Config({} sections) at {}>'.format(
1832 1831 len(self._values), hex(id(self)))
1833 1832
1834 1833 def items(self, section):
1835 1834 return self._values.get(section, {}).items()
1836 1835
1837 1836 def get(self, section, option):
1838 1837 return self._values.get(section, {}).get(option)
1839 1838
1840 1839 def set(self, section, option, value):
1841 1840 section_values = self._values.setdefault(section, {})
1842 1841 section_values[option] = value
1843 1842
1844 1843 def clear_section(self, section):
1845 1844 self._values[section] = {}
1846 1845
1847 1846 def serialize(self):
1848 1847 """
1849 1848 Creates a list of three tuples (section, key, value) representing
1850 1849 this config object.
1851 1850 """
1852 1851 items = []
1853 1852 for section in self._values:
1854 1853 for option, value in self._values[section].items():
1855 1854 items.append(
1856 1855 (safe_str(section), safe_str(option), safe_str(value)))
1857 1856 return items
1858 1857
1859 1858
1860 1859 class Diff(object):
1861 1860 """
1862 1861 Represents a diff result from a repository backend.
1863 1862
1864 1863 Subclasses have to provide a backend specific value for
1865 1864 :attr:`_header_re` and :attr:`_meta_re`.
1866 1865 """
1867 1866 _meta_re = None
1868 1867 _header_re: bytes = re.compile(br"")
1869 1868
1870 1869 def __init__(self, raw_diff: bytes):
1871 1870 if not isinstance(raw_diff, bytes):
1872 1871 raise Exception(f'raw_diff must be bytes - got {type(raw_diff)}')
1873 1872
1874 1873 self.raw = memoryview(raw_diff)
1875 1874
1876 1875 def get_header_re(self):
1877 1876 return self._header_re
1878 1877
1879 1878 def chunks(self):
1880 1879 """
1881 1880 split the diff in chunks of separate --git a/file b/file chunks
1882 1881 to make diffs consistent we must prepend with \n, and make sure
1883 1882 we can detect last chunk as this was also has special rule
1884 1883 """
1885 1884
1886 1885 diff_parts = (b'\n' + bytes(self.raw)).split(b'\ndiff --git')
1887 1886
1888 1887 chunks = diff_parts[1:]
1889 1888 total_chunks = len(chunks)
1890 1889
1891 1890 def diff_iter(_chunks):
1892 1891 for cur_chunk, chunk in enumerate(_chunks, start=1):
1893 1892 yield DiffChunk(chunk, self, cur_chunk == total_chunks)
1894 1893 return diff_iter(chunks)
1895 1894
1896 1895
1897 1896 class DiffChunk(object):
1898 1897
1899 1898 def __init__(self, chunk: bytes, diff_obj: Diff, is_last_chunk: bool):
1900 1899 self.diff_obj = diff_obj
1901 1900
1902 1901 # since we split by \ndiff --git that part is lost from original diff
1903 1902 # we need to re-apply it at the end, EXCEPT ! if it's last chunk
1904 1903 if not is_last_chunk:
1905 1904 chunk += b'\n'
1906 1905 header_re = self.diff_obj.get_header_re()
1907 1906 match = header_re.match(chunk)
1908 1907 self.header = match.groupdict()
1909 1908 self.diff = chunk[match.end():]
1910 1909 self.raw = chunk
1911 1910
1912 1911 @property
1913 1912 def header_as_str(self):
1914 1913 if self.header:
1915 1914 def safe_str_on_bytes(val):
1916 1915 if isinstance(val, bytes):
1917 1916 return safe_str(val)
1918 1917 return val
1919 1918 return {safe_str(k): safe_str_on_bytes(v) for k, v in self.header.items()}
1920 1919
1921 1920 def __repr__(self):
1922 1921 return f'DiffChunk({self.header_as_str})'
1923 1922
1924 1923
1925 1924 class BasePathPermissionChecker(object):
1926 1925
1927 1926 @staticmethod
1928 1927 def create_from_patterns(includes, excludes):
1929 1928 if includes and '*' in includes and not excludes:
1930 1929 return AllPathPermissionChecker()
1931 1930 elif excludes and '*' in excludes:
1932 1931 return NonePathPermissionChecker()
1933 1932 else:
1934 1933 return PatternPathPermissionChecker(includes, excludes)
1935 1934
1936 1935 @property
1937 1936 def has_full_access(self):
1938 1937 raise NotImplementedError()
1939 1938
1940 1939 def has_access(self, path):
1941 1940 raise NotImplementedError()
1942 1941
1943 1942
1944 1943 class AllPathPermissionChecker(BasePathPermissionChecker):
1945 1944
1946 1945 @property
1947 1946 def has_full_access(self):
1948 1947 return True
1949 1948
1950 1949 def has_access(self, path):
1951 1950 return True
1952 1951
1953 1952
1954 1953 class NonePathPermissionChecker(BasePathPermissionChecker):
1955 1954
1956 1955 @property
1957 1956 def has_full_access(self):
1958 1957 return False
1959 1958
1960 1959 def has_access(self, path):
1961 1960 return False
1962 1961
1963 1962
1964 1963 class PatternPathPermissionChecker(BasePathPermissionChecker):
1965 1964
1966 1965 def __init__(self, includes, excludes):
1967 1966 self.includes = includes
1968 1967 self.excludes = excludes
1969 1968 self.includes_re = [] if not includes else [
1970 1969 re.compile(fnmatch.translate(pattern)) for pattern in includes]
1971 1970 self.excludes_re = [] if not excludes else [
1972 1971 re.compile(fnmatch.translate(pattern)) for pattern in excludes]
1973 1972
1974 1973 @property
1975 1974 def has_full_access(self):
1976 1975 return '*' in self.includes and not self.excludes
1977 1976
1978 1977 def has_access(self, path):
1979 1978 for regex in self.excludes_re:
1980 1979 if regex.match(path):
1981 1980 return False
1982 1981 for regex in self.includes_re:
1983 1982 if regex.match(path):
1984 1983 return True
1985 1984 return False
General Comments 0
You need to be logged in to leave comments. Login now