##// END OF EJS Templates
fix(encoding for file): fixed support of non utf-8 files in all backends
super-admin -
r5647:8333bc7b default
parent child Browse files
Show More
@@ -0,0 +1,46 b''
1 # Copyright (C) 2014-2024 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
19
20 """
21 Common VCS module for rhodecode and vcsserver
22 """
23
24
25 import enum
26
27 FILEMODE_DEFAULT = 0o100644
28 FILEMODE_EXECUTABLE = 0o100755
29 FILEMODE_LINK = 0o120000
30
31
32 class NodeKind(int, enum.Enum):
33 SUBMODULE = -1
34 DIR = 1
35 FILE = 2
36 LARGE_FILE = 3
37
38
39 def map_git_obj_type(obj_type):
40 if obj_type == "blob":
41 return NodeKind.FILE
42 elif obj_type == "tree":
43 return NodeKind.DIR
44 elif obj_type == "link":
45 return NodeKind.SUBMODULE
46 return None
@@ -0,0 +1,227 b''
1 # Copyright (C) 2010-2024 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
19 import datetime
20
21 import pytest
22
23 from rhodecode.lib.str_utils import safe_bytes
24 from rhodecode.lib.vcs.nodes import FileNode
25 from rhodecode.lib.vcs_common import NodeKind
26 from rhodecode.tests.vcs.conftest import BackendTestMixin
27
28
29 class TestFileNodesListingAndCaches:
30 def test_filenode_get_root_node(self, vcsbackend):
31 repo = vcsbackend.repo
32 commit = repo.get_commit()
33
34 # check if we start with empty nodes cache
35 assert commit.nodes == {}
36 assert commit._path_mode_cache == {}
37 assert commit._path_type_cache == {}
38
39 top_dir = commit.get_node(b"")
40
41 assert top_dir.is_dir()
42
43 assert list(commit.nodes.keys()) == [b""]
44 assert list(commit._path_type_cache.keys()) == [b""]
45 assert commit._path_mode_cache == {}
46
47 def test_filenode_get_file_node(self, vcsbackend):
48 repo = vcsbackend.repo
49 commit = repo.get_commit()
50
51 # check if we start with empty nodes cache
52 assert commit.nodes == {}
53 assert commit._path_mode_cache == {}
54 assert commit._path_type_cache == {}
55
56 file_node = commit.get_node(b"README.rst")
57
58 assert file_node.is_file()
59 assert file_node.last_commit
60
61 assert list(commit.nodes.keys()) == [b"README.rst"]
62 if repo.alias == "hg":
63 assert commit._path_type_cache == {b"README.rst": NodeKind.FILE}
64 assert commit._path_mode_cache == {b"README.rst": 33188}
65
66 if repo.alias == "git":
67 assert commit._path_type_cache == {
68 b"README.rst": ["8f111ab5152b9fd34f52b0fb288e1216b5d2f55e", NodeKind.FILE]
69 }
70 assert commit._path_mode_cache == {b"README.rst": 33188}
71
72 if repo.alias == "svn":
73 assert commit._path_type_cache == {b"README.rst": NodeKind.FILE}
74 assert commit._path_mode_cache == {b"README.rst": 33188}
75
76 def test_filenode_get_nodes_from_top_dir(self, vcsbackend):
77 repo = vcsbackend.repo
78 commit = repo.get_commit()
79
80 # check if we start with empty nodes cache
81 assert commit.nodes == {}
82 assert commit._path_mode_cache == {}
83 assert commit._path_type_cache == {}
84
85 node_list = commit.get_nodes(b"")
86
87 for node in node_list:
88 assert node
89
90 if repo.alias == "svn":
91 assert list(commit.nodes.keys()) == [
92 b"README",
93 b".hgignore",
94 b"MANIFEST.in",
95 b".travis.yml",
96 b"vcs",
97 b"tox.ini",
98 b"setup.py",
99 b"setup.cfg",
100 b"docs",
101 b"fakefile",
102 b"examples",
103 b"test_and_report.sh",
104 b"bin",
105 b".gitignore",
106 b".hgtags",
107 b"README.rst",
108 ]
109 assert commit._path_type_cache == {
110 b"": NodeKind.DIR,
111 b"README": 2,
112 b".hgignore": 2,
113 b"MANIFEST.in": 2,
114 b".travis.yml": 2,
115 b"vcs": 1,
116 b"tox.ini": 2,
117 b"setup.py": 2,
118 b"setup.cfg": 2,
119 b"docs": 1,
120 b"fakefile": 2,
121 b"examples": 1,
122 b"test_and_report.sh": 2,
123 b"bin": 1,
124 b".gitignore": 2,
125 b".hgtags": 2,
126 b"README.rst": 2,
127 }
128 assert commit._path_mode_cache == {}
129
130 if repo.alias == "hg":
131 assert list(commit.nodes.keys()) == [
132 b".gitignore",
133 b".hgignore",
134 b".hgtags",
135 b".travis.yml",
136 b"MANIFEST.in",
137 b"README",
138 b"README.rst",
139 b"docs",
140 b"run_test_and_report.sh",
141 b"setup.cfg",
142 b"setup.py",
143 b"test_and_report.sh",
144 b"tox.ini",
145 b"vcs",
146 ]
147
148 assert commit._path_type_cache == {
149 b"": NodeKind.DIR,
150 b".gitignore": 2,
151 b".hgignore": 2,
152 b".hgtags": 2,
153 b".travis.yml": 2,
154 b"MANIFEST.in": 2,
155 b"README": 2,
156 b"README.rst": 2,
157 b"docs": 1,
158 b"run_test_and_report.sh": 2,
159 b"setup.cfg": 2,
160 b"setup.py": 2,
161 b"test_and_report.sh": 2,
162 b"tox.ini": 2,
163 b"vcs": 1,
164 }
165 assert commit._path_mode_cache == {}
166
167 if repo.alias == "git":
168 assert list(commit.nodes.keys()) == [
169 b".gitignore",
170 b".hgignore",
171 b".hgtags",
172 b"MANIFEST.in",
173 b"README",
174 b"README.rst",
175 b"docs",
176 b"run_test_and_report.sh",
177 b"setup.cfg",
178 b"setup.py",
179 b"test_and_report.sh",
180 b"tox.ini",
181 b"vcs",
182 ]
183 assert commit._path_type_cache == {
184 b"": ["2dab7f7377e36948b5633ae0877310380fdf64ba", NodeKind.DIR],
185 b".gitignore": ["7f95b0917e31b0cce1ad96a033b1a814c420cde3", 2],
186 b".hgignore": ["9bdee5ed67ceed7c4b5589ba26dada65df151aed", 2],
187 b".hgtags": ["63aa2e892deebdfdef1845fe52e2418ade03557b", 2],
188 b"MANIFEST.in": ["6d65aca5321c26589ae197b81511445cfb353c95", 2],
189 b"README": ["92cacd285355271487b7e379dba6ca60f9a554a4", 2],
190 b"README.rst": ["8f111ab5152b9fd34f52b0fb288e1216b5d2f55e", 2],
191 b"docs": ["eaa8cef76ec4e1baf06f515ab03bfc01719a913d", 1],
192 b"run_test_and_report.sh": ["e17011bdddc8812fda96e891a1087c73054f1f25", 2],
193 b"setup.cfg": ["2e76b65bc9106ed466d71fb2cfe710352eadfb49", 2],
194 b"setup.py": ["ccda2bd076eca961059deba6e39fb591d014052a", 2],
195 b"test_and_report.sh": ["0b14056dd7dc759a9719adef3d7bb529141a740e", 2],
196 b"tox.ini": ["c1735231e7af32f089b95455162db092824715a4", 2],
197 b"vcs": ["5868c69a8d74ccf3fdc655af8f3f185bc0ff2ef6", 1],
198 }
199 assert commit._path_mode_cache == {
200 b".gitignore": 33188,
201 b".hgignore": 33188,
202 b".hgtags": 33188,
203 b"MANIFEST.in": 33188,
204 b"README": 40960,
205 b"README.rst": 33188,
206 b"docs": 16384,
207 b"run_test_and_report.sh": 33261,
208 b"setup.cfg": 33188,
209 b"setup.py": 33188,
210 b"test_and_report.sh": 33261,
211 b"tox.ini": 33188,
212 b"vcs": 16384,
213 }
214
215 def test_filenode_get_nodes_from_docs_dir(self, vcsbackend):
216 repo = vcsbackend.repo
217 commit = repo.get_commit()
218
219 # check if we start with empty nodes cache
220 assert commit.nodes == {}
221 assert commit._path_mode_cache == {}
222 assert commit._path_type_cache == {}
223
224 node_list = commit.get_nodes(b"docs")
225
226 for node in node_list:
227 assert node
@@ -45,13 +45,20 b' CELERY_EAGER = False'
45 # link to config for pyramid
45 # link to config for pyramid
46 CONFIG = {}
46 CONFIG = {}
47
47
48 class NotGivenMeta:
49
50 def __repr__(self):
51 return 'NotGivenObject()'
52 __str__ = __repr__
53
54 NotGiven = NotGivenMeta()
48
55
49 class ConfigGet:
56 class ConfigGet:
50 NotGiven = object()
51
57
52 def _get_val_or_missing(self, key, missing):
58 @classmethod
59 def _get_val_or_missing(cls, key, missing):
53 if key not in CONFIG:
60 if key not in CONFIG:
54 if missing == self.NotGiven:
61 if missing != NotGiven:
55 return missing
62 return missing
56 # we don't get key, we don't get missing value, return nothing similar as config.get(key)
63 # we don't get key, we don't get missing value, return nothing similar as config.get(key)
57 return None
64 return None
@@ -74,6 +81,12 b' class ConfigGet:'
74 val = self._get_val_or_missing(key, missing)
81 val = self._get_val_or_missing(key, missing)
75 return str2bool(val)
82 return str2bool(val)
76
83
84 def get_list(self, key, missing=NotGiven):
85 from rhodecode.lib.type_utils import aslist
86 val = self._get_val_or_missing(key, missing)
87 return aslist(val, sep=',')
88
89
77 # Populated with the settings dictionary from application init in
90 # Populated with the settings dictionary from application init in
78 # rhodecode.conf.environment.load_pyramid_environment
91 # rhodecode.conf.environment.load_pyramid_environment
79 PYRAMID_SETTINGS = {}
92 PYRAMID_SETTINGS = {}
@@ -19,6 +19,7 b''
19
19
20 import pytest
20 import pytest
21
21
22 from rhodecode.lib.str_utils import safe_str
22 from rhodecode.model.db import User, ChangesetComment
23 from rhodecode.model.db import User, ChangesetComment
23 from rhodecode.model.meta import Session
24 from rhodecode.model.meta import Session
24 from rhodecode.model.comment import CommentsModel
25 from rhodecode.model.comment import CommentsModel
@@ -36,7 +37,7 b' def make_repo_comments_factory(request):'
36 commit = repo.scm_instance()[0]
37 commit = repo.scm_instance()[0]
37
38
38 commit_id = commit.raw_id
39 commit_id = commit.raw_id
39 file_0 = commit.affected_files[0]
40 file_0 = safe_str(commit.affected_files[0])
40 comments = []
41 comments = []
41
42
42 # general
43 # general
@@ -317,8 +317,7 b' def get_repo_changeset(request, apiuser,'
317 ','.join(_changes_details_types)))
317 ','.join(_changes_details_types)))
318
318
319 vcs_repo = repo.scm_instance()
319 vcs_repo = repo.scm_instance()
320 pre_load = ['author', 'branch', 'date', 'message', 'parents',
320 pre_load = ['author', 'branch', 'date', 'message', 'parents', 'status', '_commit']
321 'status', '_commit', '_file_paths']
322
321
323 try:
322 try:
324 commit = repo.get_commit(commit_id=revision, pre_load=pre_load)
323 commit = repo.get_commit(commit_id=revision, pre_load=pre_load)
@@ -376,8 +375,7 b' def get_repo_changesets(request, apiuser'
376 ','.join(_changes_details_types)))
375 ','.join(_changes_details_types)))
377
376
378 limit = int(limit)
377 limit = int(limit)
379 pre_load = ['author', 'branch', 'date', 'message', 'parents',
378 pre_load = ['author', 'branch', 'date', 'message', 'parents', 'status', '_commit']
380 'status', '_commit', '_file_paths']
381
379
382 vcs_repo = repo.scm_instance()
380 vcs_repo = repo.scm_instance()
383 # SVN needs a special case to distinguish its index and commit id
381 # SVN needs a special case to distinguish its index and commit id
@@ -33,7 +33,7 b' from rhodecode.lib.auth import ('
33 from rhodecode.lib.graphmod import _colored, _dagwalker
33 from rhodecode.lib.graphmod import _colored, _dagwalker
34 from rhodecode.lib.helpers import RepoPage
34 from rhodecode.lib.helpers import RepoPage
35 from rhodecode.lib.utils2 import str2bool
35 from rhodecode.lib.utils2 import str2bool
36 from rhodecode.lib.str_utils import safe_int, safe_str
36 from rhodecode.lib.str_utils import safe_int, safe_str, safe_bytes
37 from rhodecode.lib.vcs.exceptions import (
37 from rhodecode.lib.vcs.exceptions import (
38 RepositoryError, CommitDoesNotExistError,
38 RepositoryError, CommitDoesNotExistError,
39 CommitError, NodeDoesNotExistError, EmptyRepositoryError)
39 CommitError, NodeDoesNotExistError, EmptyRepositoryError)
@@ -204,10 +204,9 b' class RepoChangelogView(RepoAppView):'
204 log.debug('generating changelog for path %s', f_path)
204 log.debug('generating changelog for path %s', f_path)
205 # get the history for the file !
205 # get the history for the file !
206 base_commit = self.rhodecode_vcs_repo.get_commit(commit_id)
206 base_commit = self.rhodecode_vcs_repo.get_commit(commit_id)
207
207 bytes_path = safe_bytes(f_path)
208 try:
208 try:
209 collection = base_commit.get_path_history(
209 collection = base_commit.get_path_history(bytes_path, limit=hist_limit, pre_load=pre_load)
210 f_path, limit=hist_limit, pre_load=pre_load)
211 if collection and partial_xhr:
210 if collection and partial_xhr:
212 # for ajax call we remove first one since we're looking
211 # for ajax call we remove first one since we're looking
213 # at it right now in the context of a file commit
212 # at it right now in the context of a file commit
@@ -216,7 +215,7 b' class RepoChangelogView(RepoAppView):'
216 # this node is not present at tip!
215 # this node is not present at tip!
217 try:
216 try:
218 commit = self._get_commit_or_redirect(commit_id)
217 commit = self._get_commit_or_redirect(commit_id)
219 collection = commit.get_path_history(f_path)
218 collection = commit.get_path_history(bytes_path)
220 except RepositoryError as e:
219 except RepositoryError as e:
221 h.flash(safe_str(e), category='warning')
220 h.flash(safe_str(e), category='warning')
222 redirect_url = h.route_path(
221 redirect_url = h.route_path(
@@ -310,9 +309,8 b' class RepoChangelogView(RepoAppView):'
310 log.exception(safe_str(e))
309 log.exception(safe_str(e))
311 raise HTTPFound(
310 raise HTTPFound(
312 h.route_path('repo_commits', repo_name=self.db_repo_name))
311 h.route_path('repo_commits', repo_name=self.db_repo_name))
313
312 bytes_path = safe_bytes(f_path)
314 collection = base_commit.get_path_history(
313 collection = base_commit.get_path_history(bytes_path, limit=hist_limit, pre_load=pre_load)
315 f_path, limit=hist_limit, pre_load=pre_load)
316 collection = list(reversed(collection))
314 collection = list(reversed(collection))
317 else:
315 else:
318 collection = self.rhodecode_vcs_repo.get_commits(
316 collection = self.rhodecode_vcs_repo.get_commits(
@@ -89,8 +89,7 b' class RepoCommitsView(RepoAppView):'
89 commit_range = commit_id_range.split('...')[:2]
89 commit_range = commit_id_range.split('...')[:2]
90
90
91 try:
91 try:
92 pre_load = ['affected_files', 'author', 'branch', 'date',
92 pre_load = ['author', 'branch', 'date', 'message', 'parents']
93 'message', 'parents']
94 if self.rhodecode_vcs_repo.alias == 'hg':
93 if self.rhodecode_vcs_repo.alias == 'hg':
95 pre_load += ['hidden', 'obsolete', 'phase']
94 pre_load += ['hidden', 'obsolete', 'phase']
96
95
@@ -100,8 +99,7 b' class RepoCommitsView(RepoAppView):'
100 pre_load=pre_load, translate_tags=False)
99 pre_load=pre_load, translate_tags=False)
101 commits = list(commits)
100 commits = list(commits)
102 else:
101 else:
103 commits = [self.rhodecode_vcs_repo.get_commit(
102 commits = [self.rhodecode_vcs_repo.get_commit(commit_id=commit_id_range, pre_load=pre_load)]
104 commit_id=commit_id_range, pre_load=pre_load)]
105
103
106 c.commit_ranges = commits
104 c.commit_ranges = commits
107 if not c.commit_ranges:
105 if not c.commit_ranges:
@@ -187,12 +187,14 b' class RepoFilesView(RepoAppView):'
187 default_commit_id = self.db_repo.landing_ref_name
187 default_commit_id = self.db_repo.landing_ref_name
188 default_f_path = '/'
188 default_f_path = '/'
189
189
190 commit_id = self.request.matchdict.get(
190 commit_id = self.request.matchdict.get('commit_id', default_commit_id)
191 'commit_id', default_commit_id)
192 f_path = self._get_f_path(self.request.matchdict, default_f_path)
191 f_path = self._get_f_path(self.request.matchdict, default_f_path)
193 return commit_id, f_path
194
192
195 def _get_default_encoding(self, c):
193 bytes_path = safe_bytes(f_path)
194 return commit_id, f_path, bytes_path
195
196 @classmethod
197 def _get_default_encoding(cls, c):
196 enc_list = getattr(c, 'default_encodings', [])
198 enc_list = getattr(c, 'default_encodings', [])
197 return enc_list[0] if enc_list else 'UTF-8'
199 return enc_list[0] if enc_list else 'UTF-8'
198
200
@@ -361,21 +363,21 b' class RepoFilesView(RepoAppView):'
361 from rhodecode import CONFIG
363 from rhodecode import CONFIG
362 _ = self.request.translate
364 _ = self.request.translate
363 self.load_default_context()
365 self.load_default_context()
366
367 subrepos = self.request.GET.get('subrepos') == 'true'
368 with_hash = str2bool(self.request.GET.get('with_hash', '1'))
369
364 default_at_path = '/'
370 default_at_path = '/'
365 fname = self.request.matchdict['fname']
371 fname = self.request.matchdict['fname']
366 subrepos = self.request.GET.get('subrepos') == 'true'
367 with_hash = str2bool(self.request.GET.get('with_hash', '1'))
368 at_path = self.request.GET.get('at_path') or default_at_path
372 at_path = self.request.GET.get('at_path') or default_at_path
369
373
370 if not self.db_repo.enable_downloads:
374 if not self.db_repo.enable_downloads:
371 return Response(_('Downloads disabled'))
375 return Response(_('Downloads disabled'))
372
376
373 try:
377 try:
374 commit_id, ext, fileformat, content_type = \
378 commit_id, ext, file_format, content_type = _get_archive_spec(fname)
375 _get_archive_spec(fname)
376 except ValueError:
379 except ValueError:
377 return Response(_('Unknown archive type for: `{}`').format(
380 return Response(_('Unknown archive type for: `{}`').format(h.escape(fname)))
378 h.escape(fname)))
379
381
380 try:
382 try:
381 commit = self.rhodecode_vcs_repo.get_commit(commit_id)
383 commit = self.rhodecode_vcs_repo.get_commit(commit_id)
@@ -391,7 +393,7 b' class RepoFilesView(RepoAppView):'
391 raise HTTPFound(self.request.current_route_path(fname=fname))
393 raise HTTPFound(self.request.current_route_path(fname=fname))
392
394
393 try:
395 try:
394 at_path = commit.get_node(at_path).path or default_at_path
396 at_path = commit.get_node(safe_bytes(at_path)).path or default_at_path
395 except Exception:
397 except Exception:
396 return Response(_('No node at path {} for this repository').format(h.escape(at_path)))
398 return Response(_('No node at path {} for this repository').format(h.escape(at_path)))
397
399
@@ -440,7 +442,7 b' class RepoFilesView(RepoAppView):'
440 with d_cache.get_lock(reentrant_lock_key):
442 with d_cache.get_lock(reentrant_lock_key):
441 try:
443 try:
442 commit.archive_repo(archive_name_key, archive_dir_name=archive_dir_name,
444 commit.archive_repo(archive_name_key, archive_dir_name=archive_dir_name,
443 kind=fileformat, subrepos=subrepos,
445 kind=file_format, subrepos=subrepos,
444 archive_at_path=at_path, cache_config=d_cache_conf)
446 archive_at_path=at_path, cache_config=d_cache_conf)
445 except ImproperArchiveTypeError:
447 except ImproperArchiveTypeError:
446 return _('Unknown archive type')
448 return _('Unknown archive type')
@@ -484,7 +486,7 b' class RepoFilesView(RepoAppView):'
484 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
486 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
485 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
487 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
486 try:
488 try:
487 node = commit.get_node(f_path)
489 node = commit.get_node(safe_bytes(f_path))
488 if node.is_dir():
490 if node.is_dir():
489 raise NodeError(f'{node} path is a {type(node)} not a file')
491 raise NodeError(f'{node} path is a {type(node)} not a file')
490 except NodeDoesNotExistError:
492 except NodeDoesNotExistError:
@@ -648,7 +650,7 b' class RepoFilesView(RepoAppView):'
648 # overwrite auto rendering by setting this GET flag
650 # overwrite auto rendering by setting this GET flag
649 c.renderer = view_name == 'repo_files:rendered' or not self.request.GET.get('no-render', False)
651 c.renderer = view_name == 'repo_files:rendered' or not self.request.GET.get('no-render', False)
650
652
651 commit_id, f_path = self._get_commit_and_path()
653 commit_id, f_path, bytes_path = self._get_commit_and_path()
652
654
653 c.commit = self._get_commit_or_redirect(commit_id)
655 c.commit = self._get_commit_or_redirect(commit_id)
654 c.branch = self.request.GET.get('branch', None)
656 c.branch = self.request.GET.get('branch', None)
@@ -657,7 +659,8 b' class RepoFilesView(RepoAppView):'
657
659
658 # files or dirs
660 # files or dirs
659 try:
661 try:
660 c.file = c.commit.get_node(f_path, pre_load=['is_binary', 'size', 'data'])
662
663 c.file = c.commit.get_node(bytes_path, pre_load=['is_binary', 'size', 'data'])
661
664
662 c.file_author = True
665 c.file_author = True
663 c.file_tree = ''
666 c.file_tree = ''
@@ -666,11 +669,9 b' class RepoFilesView(RepoAppView):'
666 try:
669 try:
667 prev_commit = c.commit.prev(c.branch)
670 prev_commit = c.commit.prev(c.branch)
668 c.prev_commit = prev_commit
671 c.prev_commit = prev_commit
669 c.url_prev = h.route_path(
672 c.url_prev = h.route_path('repo_files', repo_name=self.db_repo_name, commit_id=prev_commit.raw_id, f_path=f_path)
670 'repo_files', repo_name=self.db_repo_name,
671 commit_id=prev_commit.raw_id, f_path=f_path)
672 if c.branch:
673 if c.branch:
673 c.url_prev += '?branch=%s' % c.branch
674 c.url_prev += f'?branch={c.branch}'
674 except (CommitDoesNotExistError, VCSError):
675 except (CommitDoesNotExistError, VCSError):
675 c.url_prev = '#'
676 c.url_prev = '#'
676 c.prev_commit = EmptyCommit()
677 c.prev_commit = EmptyCommit()
@@ -679,11 +680,9 b' class RepoFilesView(RepoAppView):'
679 try:
680 try:
680 next_commit = c.commit.next(c.branch)
681 next_commit = c.commit.next(c.branch)
681 c.next_commit = next_commit
682 c.next_commit = next_commit
682 c.url_next = h.route_path(
683 c.url_next = h.route_path('repo_files', repo_name=self.db_repo_name, commit_id=next_commit.raw_id, f_path=f_path)
683 'repo_files', repo_name=self.db_repo_name,
684 commit_id=next_commit.raw_id, f_path=f_path)
685 if c.branch:
684 if c.branch:
686 c.url_next += '?branch=%s' % c.branch
685 c.url_next += f'?branch={c.branch}'
687 except (CommitDoesNotExistError, VCSError):
686 except (CommitDoesNotExistError, VCSError):
688 c.url_next = '#'
687 c.url_next = '#'
689 c.next_commit = EmptyCommit()
688 c.next_commit = EmptyCommit()
@@ -739,8 +738,7 b' class RepoFilesView(RepoAppView):'
739 c.file_tree = self._get_tree_at_commit(c, c.commit.raw_id, f_path, at_rev=at_rev)
738 c.file_tree = self._get_tree_at_commit(c, c.commit.raw_id, f_path, at_rev=at_rev)
740
739
741 c.readme_data, c.readme_file = \
740 c.readme_data, c.readme_file = \
742 self._get_readme_data(self.db_repo, c.visual.default_renderer,
741 self._get_readme_data(self.db_repo, c.visual.default_renderer, c.commit.raw_id, bytes_path)
743 c.commit.raw_id, f_path)
744
742
745 except RepositoryError as e:
743 except RepositoryError as e:
746 h.flash(h.escape(safe_str(e)), category='error')
744 h.flash(h.escape(safe_str(e)), category='error')
@@ -759,24 +757,24 b' class RepoFilesView(RepoAppView):'
759 def repo_files_annotated_previous(self):
757 def repo_files_annotated_previous(self):
760 self.load_default_context()
758 self.load_default_context()
761
759
762 commit_id, f_path = self._get_commit_and_path()
760 commit_id, bytes_path, bytes_path = self._get_commit_and_path()
763 commit = self._get_commit_or_redirect(commit_id)
761 commit = self._get_commit_or_redirect(commit_id)
764 prev_commit_id = commit.raw_id
762 prev_commit_id = commit.raw_id
765 line_anchor = self.request.GET.get('line_anchor')
763 line_anchor = self.request.GET.get('line_anchor')
766 is_file = False
764 is_file = False
767 try:
765 try:
768 _file = commit.get_node(f_path)
766 _file = commit.get_node(bytes_path)
769 is_file = _file.is_file()
767 is_file = _file.is_file()
770 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
768 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
771 pass
769 pass
772
770
773 if is_file:
771 if is_file:
774 history = commit.get_path_history(f_path)
772 history = commit.get_path_history(bytes_path)
775 prev_commit_id = history[1].raw_id \
773 prev_commit_id = history[1].raw_id \
776 if len(history) > 1 else prev_commit_id
774 if len(history) > 1 else prev_commit_id
777 prev_url = h.route_path(
775 prev_url = h.route_path(
778 'repo_files:annotated', repo_name=self.db_repo_name,
776 'repo_files:annotated', repo_name=self.db_repo_name,
779 commit_id=prev_commit_id, f_path=f_path,
777 commit_id=prev_commit_id, f_path=bytes_path,
780 _anchor=f'L{line_anchor}')
778 _anchor=f'L{line_anchor}')
781
779
782 raise HTTPFound(prev_url)
780 raise HTTPFound(prev_url)
@@ -792,10 +790,10 b' class RepoFilesView(RepoAppView):'
792 """
790 """
793 c = self.load_default_context()
791 c = self.load_default_context()
794
792
795 commit_id, f_path = self._get_commit_and_path()
793 commit_id, f_path, bytes_path = self._get_commit_and_path()
796 commit = self._get_commit_or_redirect(commit_id)
794 commit = self._get_commit_or_redirect(commit_id)
797 try:
795 try:
798 dir_node = commit.get_node(f_path)
796 dir_node = commit.get_node(bytes_path)
799 except RepositoryError as e:
797 except RepositoryError as e:
800 return Response(f'error: {h.escape(safe_str(e))}')
798 return Response(f'error: {h.escape(safe_str(e))}')
801
799
@@ -816,9 +814,9 b' class RepoFilesView(RepoAppView):'
816 safe_path = f_name.replace('"', '\\"')
814 safe_path = f_name.replace('"', '\\"')
817 encoded_path = urllib.parse.quote(f_name)
815 encoded_path = urllib.parse.quote(f_name)
818
816
819 headers = "attachment; " \
817 headers = f"attachment; " \
820 "filename=\"{}\"; " \
818 f"filename=\"{safe_path}\"; " \
821 "filename*=UTF-8\'\'{}".format(safe_path, encoded_path)
819 f"filename*=UTF-8\'\'{encoded_path}"
822
820
823 return header_safe_str(headers)
821 return header_safe_str(headers)
824
822
@@ -832,9 +830,9 b' class RepoFilesView(RepoAppView):'
832 """
830 """
833 c = self.load_default_context()
831 c = self.load_default_context()
834
832
835 commit_id, f_path = self._get_commit_and_path()
833 commit_id, f_path, bytes_path = self._get_commit_and_path()
836 commit = self._get_commit_or_redirect(commit_id)
834 commit = self._get_commit_or_redirect(commit_id)
837 file_node = self._get_filenode_or_redirect(commit, f_path)
835 file_node = self._get_filenode_or_redirect(commit, bytes_path)
838
836
839 raw_mimetype_mapping = {
837 raw_mimetype_mapping = {
840 # map original mimetype to a mimetype used for "show as raw"
838 # map original mimetype to a mimetype used for "show as raw"
@@ -892,9 +890,9 b' class RepoFilesView(RepoAppView):'
892 def repo_file_download(self):
890 def repo_file_download(self):
893 c = self.load_default_context()
891 c = self.load_default_context()
894
892
895 commit_id, f_path = self._get_commit_and_path()
893 commit_id, f_path, bytes_path = self._get_commit_and_path()
896 commit = self._get_commit_or_redirect(commit_id)
894 commit = self._get_commit_or_redirect(commit_id)
897 file_node = self._get_filenode_or_redirect(commit, f_path)
895 file_node = self._get_filenode_or_redirect(commit, bytes_path)
898
896
899 if self.request.GET.get('lf'):
897 if self.request.GET.get('lf'):
900 # only if lf get flag is passed, we download this file
898 # only if lf get flag is passed, we download this file
@@ -920,8 +918,7 b' class RepoFilesView(RepoAppView):'
920
918
921 def _get_nodelist_at_commit(self, repo_name, repo_id, commit_id, f_path):
919 def _get_nodelist_at_commit(self, repo_name, repo_id, commit_id, f_path):
922
920
923 cache_seconds = safe_int(
921 cache_seconds = rhodecode.ConfigGet().get_int('rc_cache.cache_repo.expiration_time')
924 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
925 cache_on = cache_seconds > 0
922 cache_on = cache_seconds > 0
926 log.debug(
923 log.debug(
927 'Computing FILE SEARCH for repo_id %s commit_id `%s` and path `%s`'
924 'Computing FILE SEARCH for repo_id %s commit_id `%s` and path `%s`'
@@ -933,21 +930,17 b' class RepoFilesView(RepoAppView):'
933
930
934 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, condition=cache_on)
931 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, condition=cache_on)
935 def compute_file_search(_name_hash, _repo_id, _commit_id, _f_path):
932 def compute_file_search(_name_hash, _repo_id, _commit_id, _f_path):
936 log.debug('Generating cached nodelist for repo_id:%s, %s, %s',
933 log.debug('Generating cached nodelist for repo_id:%s, %s, %s', _repo_id, commit_id, f_path)
937 _repo_id, commit_id, f_path)
938 try:
934 try:
939 _d, _f = ScmModel().get_quick_filter_nodes(repo_name, _commit_id, _f_path)
935 _d, _f = ScmModel().get_quick_filter_nodes(repo_name, _commit_id, _f_path)
940 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
936 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
941 log.exception(safe_str(e))
937 log.exception(safe_str(e))
942 h.flash(h.escape(safe_str(e)), category='error')
938 h.flash(h.escape(safe_str(e)), category='error')
943 raise HTTPFound(h.route_path(
939 raise HTTPFound(h.route_path('repo_files', repo_name=self.db_repo_name, commit_id='tip', f_path='/'))
944 'repo_files', repo_name=self.db_repo_name,
945 commit_id='tip', f_path='/'))
946
940
947 return _d + _f
941 return _d + _f
948
942
949 result = compute_file_search(self.db_repo.repo_name_hash, self.db_repo.repo_id,
943 result = compute_file_search(self.db_repo.repo_name_hash, self.db_repo.repo_id, commit_id, f_path)
950 commit_id, f_path)
951 return filter(lambda n: self.path_filter.path_access_allowed(n['name']), result)
944 return filter(lambda n: self.path_filter.path_access_allowed(n['name']), result)
952
945
953 @LoginRequired()
946 @LoginRequired()
@@ -956,7 +949,7 b' class RepoFilesView(RepoAppView):'
956 def repo_nodelist(self):
949 def repo_nodelist(self):
957 self.load_default_context()
950 self.load_default_context()
958
951
959 commit_id, f_path = self._get_commit_and_path()
952 commit_id, f_path, bytes_path = self._get_commit_and_path()
960 commit = self._get_commit_or_redirect(commit_id)
953 commit = self._get_commit_or_redirect(commit_id)
961
954
962 metadata = self._get_nodelist_at_commit(
955 metadata = self._get_nodelist_at_commit(
@@ -996,10 +989,10 b' class RepoFilesView(RepoAppView):'
996 if commits is None:
989 if commits is None:
997 pre_load = ["author", "branch"]
990 pre_load = ["author", "branch"]
998 try:
991 try:
999 commits = tip.get_path_history(f_path, pre_load=pre_load)
992 commits = tip.get_path_history(safe_bytes(f_path), pre_load=pre_load)
1000 except (NodeDoesNotExistError, CommitError):
993 except (NodeDoesNotExistError, CommitError):
1001 # this node is not present at tip!
994 # this node is not present at tip!
1002 commits = commit_obj.get_path_history(f_path, pre_load=pre_load)
995 commits = commit_obj.get_path_history(safe_bytes(f_path), pre_load=pre_load)
1003
996
1004 history = []
997 history = []
1005 commits_group = ([], _("Changesets"))
998 commits_group = ([], _("Changesets"))
@@ -1040,9 +1033,9 b' class RepoFilesView(RepoAppView):'
1040 def repo_file_history(self):
1033 def repo_file_history(self):
1041 self.load_default_context()
1034 self.load_default_context()
1042
1035
1043 commit_id, f_path = self._get_commit_and_path()
1036 commit_id, f_path, bytes_path = self._get_commit_and_path()
1044 commit = self._get_commit_or_redirect(commit_id)
1037 commit = self._get_commit_or_redirect(commit_id)
1045 file_node = self._get_filenode_or_redirect(commit, f_path)
1038 file_node = self._get_filenode_or_redirect(commit, bytes_path)
1046
1039
1047 if file_node.is_file():
1040 if file_node.is_file():
1048 file_history, _hist = self._get_node_history(commit, f_path)
1041 file_history, _hist = self._get_node_history(commit, f_path)
@@ -1083,9 +1076,9 b' class RepoFilesView(RepoAppView):'
1083 def repo_file_authors(self):
1076 def repo_file_authors(self):
1084 c = self.load_default_context()
1077 c = self.load_default_context()
1085
1078
1086 commit_id, f_path = self._get_commit_and_path()
1079 commit_id, f_path, bytes_path = self._get_commit_and_path()
1087 commit = self._get_commit_or_redirect(commit_id)
1080 commit = self._get_commit_or_redirect(commit_id)
1088 file_node = self._get_filenode_or_redirect(commit, f_path)
1081 file_node = self._get_filenode_or_redirect(commit, bytes_path)
1089
1082
1090 if not file_node.is_file():
1083 if not file_node.is_file():
1091 raise HTTPBadRequest()
1084 raise HTTPBadRequest()
@@ -1124,10 +1117,9 b' class RepoFilesView(RepoAppView):'
1124 def repo_files_check_head(self):
1117 def repo_files_check_head(self):
1125 self.load_default_context()
1118 self.load_default_context()
1126
1119
1127 commit_id, f_path = self._get_commit_and_path()
1120 commit_id, f_path, bytes_path = self._get_commit_and_path()
1128 _branch_name, _sha_commit_id, is_head = \
1121 _branch_name, _sha_commit_id, is_head = \
1129 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1122 self._is_valid_head(commit_id, self.rhodecode_vcs_repo, landing_ref=self.db_repo.landing_ref_name)
1130 landing_ref=self.db_repo.landing_ref_name)
1131
1123
1132 new_path = self.request.POST.get('path')
1124 new_path = self.request.POST.get('path')
1133 operation = self.request.POST.get('operation')
1125 operation = self.request.POST.get('operation')
@@ -1138,12 +1130,10 b' class RepoFilesView(RepoAppView):'
1138 try:
1130 try:
1139 commit_obj = self.rhodecode_vcs_repo.get_commit(commit_id)
1131 commit_obj = self.rhodecode_vcs_repo.get_commit(commit_id)
1140 # NOTE(dan): construct whole path without leading /
1132 # NOTE(dan): construct whole path without leading /
1141 file_node = commit_obj.get_node(new_f_path)
1133 file_node = commit_obj.get_node(safe_bytes(new_f_path))
1142 if file_node is not None:
1134 if file_node:
1143 path_exist = new_f_path
1135 path_exist = new_f_path
1144 except EmptyRepositoryError:
1136 except (EmptyRepositoryError, NodeDoesNotExistError):
1145 pass
1146 except Exception:
1147 pass
1137 pass
1148
1138
1149 return {
1139 return {
@@ -1158,7 +1148,7 b' class RepoFilesView(RepoAppView):'
1158 def repo_files_remove_file(self):
1148 def repo_files_remove_file(self):
1159 _ = self.request.translate
1149 _ = self.request.translate
1160 c = self.load_default_context()
1150 c = self.load_default_context()
1161 commit_id, f_path = self._get_commit_and_path()
1151 commit_id, f_path, bytes_path = self._get_commit_and_path()
1162
1152
1163 self._ensure_not_locked()
1153 self._ensure_not_locked()
1164 _branch_name, _sha_commit_id, is_head = \
1154 _branch_name, _sha_commit_id, is_head = \
@@ -1169,7 +1159,7 b' class RepoFilesView(RepoAppView):'
1169 self.check_branch_permission(_branch_name)
1159 self.check_branch_permission(_branch_name)
1170
1160
1171 c.commit = self._get_commit_or_redirect(commit_id)
1161 c.commit = self._get_commit_or_redirect(commit_id)
1172 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1162 c.file = self._get_filenode_or_redirect(c.commit, bytes_path)
1173
1163
1174 c.default_message = _(
1164 c.default_message = _(
1175 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1165 'Deleted file {} via RhodeCode Enterprise').format(f_path)
@@ -1184,7 +1174,7 b' class RepoFilesView(RepoAppView):'
1184 _ = self.request.translate
1174 _ = self.request.translate
1185
1175
1186 c = self.load_default_context()
1176 c = self.load_default_context()
1187 commit_id, f_path = self._get_commit_and_path()
1177 commit_id, f_path, bytes_path = self._get_commit_and_path()
1188
1178
1189 self._ensure_not_locked()
1179 self._ensure_not_locked()
1190 _branch_name, _sha_commit_id, is_head = \
1180 _branch_name, _sha_commit_id, is_head = \
@@ -1195,10 +1185,9 b' class RepoFilesView(RepoAppView):'
1195 self.check_branch_permission(_branch_name)
1185 self.check_branch_permission(_branch_name)
1196
1186
1197 c.commit = self._get_commit_or_redirect(commit_id)
1187 c.commit = self._get_commit_or_redirect(commit_id)
1198 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1188 c.file = self._get_filenode_or_redirect(c.commit, bytes_path)
1199
1189
1200 c.default_message = _(
1190 c.default_message = _('Deleted file {} via RhodeCode Enterprise').format(f_path)
1201 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1202 c.f_path = f_path
1191 c.f_path = f_path
1203 node_path = f_path
1192 node_path = f_path
1204 author = self._rhodecode_db_user.full_contact
1193 author = self._rhodecode_db_user.full_contact
@@ -1232,7 +1221,7 b' class RepoFilesView(RepoAppView):'
1232 def repo_files_edit_file(self):
1221 def repo_files_edit_file(self):
1233 _ = self.request.translate
1222 _ = self.request.translate
1234 c = self.load_default_context()
1223 c = self.load_default_context()
1235 commit_id, f_path = self._get_commit_and_path()
1224 commit_id, f_path, bytes_path = self._get_commit_and_path()
1236
1225
1237 self._ensure_not_locked()
1226 self._ensure_not_locked()
1238 _branch_name, _sha_commit_id, is_head = \
1227 _branch_name, _sha_commit_id, is_head = \
@@ -1243,7 +1232,7 b' class RepoFilesView(RepoAppView):'
1243 self.check_branch_permission(_branch_name, commit_id=commit_id)
1232 self.check_branch_permission(_branch_name, commit_id=commit_id)
1244
1233
1245 c.commit = self._get_commit_or_redirect(commit_id)
1234 c.commit = self._get_commit_or_redirect(commit_id)
1246 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1235 c.file = self._get_filenode_or_redirect(c.commit, bytes_path)
1247
1236
1248 if c.file.is_binary:
1237 if c.file.is_binary:
1249 files_url = h.route_path(
1238 files_url = h.route_path(
@@ -1263,12 +1252,12 b' class RepoFilesView(RepoAppView):'
1263 def repo_files_update_file(self):
1252 def repo_files_update_file(self):
1264 _ = self.request.translate
1253 _ = self.request.translate
1265 c = self.load_default_context()
1254 c = self.load_default_context()
1266 commit_id, f_path = self._get_commit_and_path()
1255 commit_id, f_path, bytes_path = self._get_commit_and_path()
1267
1256
1268 self._ensure_not_locked()
1257 self._ensure_not_locked()
1269
1258
1270 c.commit = self._get_commit_or_redirect(commit_id)
1259 c.commit = self._get_commit_or_redirect(commit_id)
1271 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1260 c.file = self._get_filenode_or_redirect(c.commit, bytes_path)
1272
1261
1273 if c.file.is_binary:
1262 if c.file.is_binary:
1274 raise HTTPFound(h.route_path('repo_files', repo_name=self.db_repo_name,
1263 raise HTTPFound(h.route_path('repo_files', repo_name=self.db_repo_name,
@@ -1345,7 +1334,7 b' class RepoFilesView(RepoAppView):'
1345 def repo_files_add_file(self):
1334 def repo_files_add_file(self):
1346 _ = self.request.translate
1335 _ = self.request.translate
1347 c = self.load_default_context()
1336 c = self.load_default_context()
1348 commit_id, f_path = self._get_commit_and_path()
1337 commit_id, f_path, bytes_path = self._get_commit_and_path()
1349
1338
1350 self._ensure_not_locked()
1339 self._ensure_not_locked()
1351
1340
@@ -1381,7 +1370,7 b' class RepoFilesView(RepoAppView):'
1381 def repo_files_create_file(self):
1370 def repo_files_create_file(self):
1382 _ = self.request.translate
1371 _ = self.request.translate
1383 c = self.load_default_context()
1372 c = self.load_default_context()
1384 commit_id, f_path = self._get_commit_and_path()
1373 commit_id, f_path, bytes_path = self._get_commit_and_path()
1385
1374
1386 self._ensure_not_locked()
1375 self._ensure_not_locked()
1387
1376
@@ -1450,8 +1439,7 b' class RepoFilesView(RepoAppView):'
1450 author=author,
1439 author=author,
1451 )
1440 )
1452
1441
1453 h.flash(_('Successfully committed new file `{}`').format(
1442 h.flash(_('Successfully committed new file `{}`').format(h.escape(node_path)), category='success')
1454 h.escape(node_path)), category='success')
1455
1443
1456 default_redirect_url = h.route_path(
1444 default_redirect_url = h.route_path(
1457 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1445 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
@@ -1475,7 +1463,7 b' class RepoFilesView(RepoAppView):'
1475 def repo_files_upload_file(self):
1463 def repo_files_upload_file(self):
1476 _ = self.request.translate
1464 _ = self.request.translate
1477 c = self.load_default_context()
1465 c = self.load_default_context()
1478 commit_id, f_path = self._get_commit_and_path()
1466 commit_id, f_path, bytes_path = self._get_commit_and_path()
1479
1467
1480 self._ensure_not_locked()
1468 self._ensure_not_locked()
1481
1469
@@ -1604,7 +1592,7 b' class RepoFilesView(RepoAppView):'
1604 def repo_files_replace_file(self):
1592 def repo_files_replace_file(self):
1605 _ = self.request.translate
1593 _ = self.request.translate
1606 c = self.load_default_context()
1594 c = self.load_default_context()
1607 commit_id, f_path = self._get_commit_and_path()
1595 commit_id, f_path, bytes_path = self._get_commit_and_path()
1608
1596
1609 self._ensure_not_locked()
1597 self._ensure_not_locked()
1610
1598
@@ -39,7 +39,6 b' def configure_vcs(config):'
39
39
40 conf.settings.HOOKS_PROTOCOL = config['vcs.hooks.protocol.v2']
40 conf.settings.HOOKS_PROTOCOL = config['vcs.hooks.protocol.v2']
41 conf.settings.HOOKS_HOST = config['vcs.hooks.host']
41 conf.settings.HOOKS_HOST = config['vcs.hooks.host']
42 conf.settings.DEFAULT_ENCODINGS = config['default_encoding']
43 conf.settings.ALIASES[:] = config['vcs.backends']
42 conf.settings.ALIASES[:] = config['vcs.backends']
44 conf.settings.SVN_COMPATIBLE_VERSION = config['vcs.svn.compatible_version']
43 conf.settings.SVN_COMPATIBLE_VERSION = config['vcs.svn.compatible_version']
45
44
@@ -478,7 +478,9 b' class DiffSet(object):'
478 log.debug('rendering diff for %r', patch['filename'])
478 log.debug('rendering diff for %r', patch['filename'])
479
479
480 source_filename = patch['original_filename']
480 source_filename = patch['original_filename']
481 source_filename_bytes = patch['original_filename_bytes']
481 target_filename = patch['filename']
482 target_filename = patch['filename']
483 target_filename_bytes = patch['filename_bytes']
482
484
483 source_lexer = plain_text_lexer
485 source_lexer = plain_text_lexer
484 target_lexer = plain_text_lexer
486 target_lexer = plain_text_lexer
@@ -491,12 +493,12 b' class DiffSet(object):'
491 if (source_filename and patch['operation'] in ('D', 'M')
493 if (source_filename and patch['operation'] in ('D', 'M')
492 and source_filename not in self.source_nodes):
494 and source_filename not in self.source_nodes):
493 self.source_nodes[source_filename] = (
495 self.source_nodes[source_filename] = (
494 self.source_node_getter(source_filename))
496 self.source_node_getter(source_filename_bytes))
495
497
496 if (target_filename and patch['operation'] in ('A', 'M')
498 if (target_filename and patch['operation'] in ('A', 'M')
497 and target_filename not in self.target_nodes):
499 and target_filename not in self.target_nodes):
498 self.target_nodes[target_filename] = (
500 self.target_nodes[target_filename] = (
499 self.target_node_getter(target_filename))
501 self.target_node_getter(target_filename_bytes))
500
502
501 elif hl_mode == self.HL_FAST:
503 elif hl_mode == self.HL_FAST:
502 source_lexer = self._get_lexer_for_filename(source_filename)
504 source_lexer = self._get_lexer_for_filename(source_filename)
@@ -558,6 +560,7 b' class DiffSet(object):'
558 })
560 })
559
561
560 file_chunks = patch['chunks'][1:]
562 file_chunks = patch['chunks'][1:]
563
561 for i, hunk in enumerate(file_chunks, 1):
564 for i, hunk in enumerate(file_chunks, 1):
562 hunkbit = self.parse_hunk(hunk, source_file, target_file)
565 hunkbit = self.parse_hunk(hunk, source_file, target_file)
563 hunkbit.source_file_path = source_file_path
566 hunkbit.source_file_path = source_file_path
@@ -593,12 +596,13 b' class DiffSet(object):'
593 return filediff
596 return filediff
594
597
595 def parse_hunk(self, hunk, source_file, target_file):
598 def parse_hunk(self, hunk, source_file, target_file):
599
596 result = AttributeDict(dict(
600 result = AttributeDict(dict(
597 source_start=hunk['source_start'],
601 source_start=hunk['source_start'],
598 source_length=hunk['source_length'],
602 source_length=hunk['source_length'],
599 target_start=hunk['target_start'],
603 target_start=hunk['target_start'],
600 target_length=hunk['target_length'],
604 target_length=hunk['target_length'],
601 section_header=hunk['section_header'],
605 section_header=safe_str(hunk['section_header']),
602 lines=[],
606 lines=[],
603 ))
607 ))
604 before, after = [], []
608 before, after = [], []
@@ -455,7 +455,9 b' class DiffProcessor(object):'
455 return arg
455 return arg
456
456
457 for chunk in self._diff.chunks():
457 for chunk in self._diff.chunks():
458 bytes_head = chunk.header
458 head = chunk.header_as_str
459 head = chunk.header_as_str
460
459 log.debug('parsing diff chunk %r', chunk)
461 log.debug('parsing diff chunk %r', chunk)
460
462
461 raw_diff = chunk.raw
463 raw_diff = chunk.raw
@@ -598,10 +600,17 b' class DiffProcessor(object):'
598
600
599 chunks.insert(0, frag)
601 chunks.insert(0, frag)
600
602
601 original_filename = safe_str(head['a_path'])
603 original_filename = head['a_path']
604 original_filename_bytes = bytes_head['a_path']
605
606 filename = head['b_path']
607 filename_bytes = bytes_head['b_path']
608
602 _files.append({
609 _files.append({
603 'original_filename': original_filename,
610 'original_filename': original_filename,
604 'filename': safe_str(head['b_path']),
611 'original_filename_bytes': original_filename_bytes,
612 'filename': filename,
613 'filename_bytes': filename_bytes,
605 'old_revision': head['a_blob_id'],
614 'old_revision': head['a_blob_id'],
606 'new_revision': head['b_blob_id'],
615 'new_revision': head['b_blob_id'],
607 'chunks': chunks,
616 'chunks': chunks,
@@ -84,7 +84,6 b' def check_locked_repo(extras, check_same'
84 user = User.get_by_username(extras.username)
84 user = User.get_by_username(extras.username)
85 output = ''
85 output = ''
86 if extras.locked_by[0] and (not check_same_user or user.user_id != extras.locked_by[0]):
86 if extras.locked_by[0] and (not check_same_user or user.user_id != extras.locked_by[0]):
87
88 locked_by = User.get(extras.locked_by[0]).username
87 locked_by = User.get(extras.locked_by[0]).username
89 reason = extras.locked_by[2]
88 reason = extras.locked_by[2]
90 # this exception is interpreted in git/hg middlewares and based
89 # this exception is interpreted in git/hg middlewares and based
@@ -67,10 +67,7 b' def base64_to_str(text: str | bytes) -> '
67
67
68
68
69 def get_default_encodings() -> list[str]:
69 def get_default_encodings() -> list[str]:
70 return aslist(rhodecode.CONFIG.get('default_encoding', 'utf8'), sep=',')
70 return rhodecode.ConfigGet().get_list('default_encoding', missing='utf8')
71
72
73 DEFAULT_ENCODINGS = get_default_encodings()
74
71
75
72
76 def safe_str(str_, to_encoding=None) -> str:
73 def safe_str(str_, to_encoding=None) -> str:
@@ -87,7 +84,7 b' def safe_str(str_, to_encoding=None) -> '
87 if not isinstance(str_, bytes):
84 if not isinstance(str_, bytes):
88 return str(str_)
85 return str(str_)
89
86
90 to_encoding = to_encoding or DEFAULT_ENCODINGS
87 to_encoding = to_encoding or get_default_encodings()
91 if not isinstance(to_encoding, (list, tuple)):
88 if not isinstance(to_encoding, (list, tuple)):
92 to_encoding = [to_encoding]
89 to_encoding = [to_encoding]
93
90
@@ -120,7 +117,7 b' def safe_bytes(str_, from_encoding=None)'
120 for enc in from_encoding:
117 for enc in from_encoding:
121 try:
118 try:
122 return str_.encode(enc)
119 return str_.encode(enc)
123 except UnicodeDecodeError:
120 except (UnicodeDecodeError, UnicodeEncodeError):
124 pass
121 pass
125
122
126 return str_.encode(from_encoding[0], 'replace')
123 return str_.encode(from_encoding[0], 'replace')
@@ -139,7 +139,7 b' class CurlSession(object):'
139
139
140 try:
140 try:
141 curl.perform()
141 curl.perform()
142 except pycurl.error as exc:
142 except pycurl.error:
143 log.error('Failed to call endpoint url: %s using pycurl', url)
143 log.error('Failed to call endpoint url: %s using pycurl', url)
144 raise
144 raise
145
145
This diff has been collapsed as it changes many lines, (1633 lines changed) Show them Hide them
@@ -19,6 +19,7 b''
19 """
19 """
20 Base module for all VCS systems
20 Base module for all VCS systems
21 """
21 """
22
22 import os
23 import os
23 import re
24 import re
24 import time
25 import time
@@ -39,19 +40,25 b' from rhodecode.lib.utils2 import safe_st'
39 from rhodecode.lib.vcs.utils import author_name, author_email
40 from rhodecode.lib.vcs.utils import author_name, author_email
40 from rhodecode.lib.vcs.conf import settings
41 from rhodecode.lib.vcs.conf import settings
41 from rhodecode.lib.vcs.exceptions import (
42 from rhodecode.lib.vcs.exceptions import (
42 CommitError, EmptyRepositoryError, NodeAlreadyAddedError,
43 CommitError,
43 NodeAlreadyChangedError, NodeAlreadyExistsError, NodeAlreadyRemovedError,
44 EmptyRepositoryError,
44 NodeDoesNotExistError, NodeNotChangedError, VCSError,
45 NodeAlreadyAddedError,
45 ImproperArchiveTypeError, BranchDoesNotExistError, CommitDoesNotExistError,
46 NodeAlreadyChangedError,
46 RepositoryError)
47 NodeAlreadyExistsError,
48 NodeAlreadyRemovedError,
49 NodeDoesNotExistError,
50 NodeNotChangedError,
51 VCSError,
52 ImproperArchiveTypeError,
53 BranchDoesNotExistError,
54 CommitDoesNotExistError,
55 RepositoryError,
56 )
47
57
48
58
49 log = logging.getLogger(__name__)
59 log = logging.getLogger(__name__)
50
60
51
61 EMPTY_COMMIT_ID = "0" * 40
52 FILEMODE_DEFAULT = 0o100644
53 FILEMODE_EXECUTABLE = 0o100755
54 EMPTY_COMMIT_ID = '0' * 40
55
62
56
63
57 @dataclasses.dataclass
64 @dataclasses.dataclass
@@ -67,12 +74,12 b' class Reference:'
67
74
68 @property
75 @property
69 def branch(self):
76 def branch(self):
70 if self.type == 'branch':
77 if self.type == "branch":
71 return self.name
78 return self.name
72
79
73 @property
80 @property
74 def bookmark(self):
81 def bookmark(self):
75 if self.type == 'book':
82 if self.type == "book":
76 return self.name
83 return self.name
77
84
78 @property
85 @property
@@ -80,11 +87,7 b' class Reference:'
80 return reference_to_unicode(self)
87 return reference_to_unicode(self)
81
88
82 def asdict(self):
89 def asdict(self):
83 return dict(
90 return dict(type=self.type, name=self.name, commit_id=self.commit_id)
84 type=self.type,
85 name=self.name,
86 commit_id=self.commit_id
87 )
88
91
89
92
90 def unicode_to_reference(raw: str):
93 def unicode_to_reference(raw: str):
@@ -93,7 +96,7 b' def unicode_to_reference(raw: str):'
93 If unicode evaluates to False it returns None.
96 If unicode evaluates to False it returns None.
94 """
97 """
95 if raw:
98 if raw:
96 refs = raw.split(':')
99 refs = raw.split(":")
97 return Reference(*refs)
100 return Reference(*refs)
98 else:
101 else:
99 return None
102 return None
@@ -105,7 +108,7 b' def reference_to_unicode(ref: Reference)'
105 If reference is None it returns None.
108 If reference is None it returns None.
106 """
109 """
107 if ref:
110 if ref:
108 return ':'.join(ref)
111 return ":".join(ref)
109 else:
112 else:
110 return None
113 return None
111
114
@@ -194,47 +197,44 b' class UpdateFailureReason(object):'
194
197
195
198
196 class MergeResponse(object):
199 class MergeResponse(object):
197
198 # uses .format(**metadata) for variables
200 # uses .format(**metadata) for variables
199 MERGE_STATUS_MESSAGES = {
201 MERGE_STATUS_MESSAGES = {
200 MergeFailureReason.NONE: lazy_ugettext(
202 MergeFailureReason.NONE: lazy_ugettext("This pull request can be automatically merged."),
201 'This pull request can be automatically merged.'),
202 MergeFailureReason.UNKNOWN: lazy_ugettext(
203 MergeFailureReason.UNKNOWN: lazy_ugettext(
203 'This pull request cannot be merged because of an unhandled exception. '
204 "This pull request cannot be merged because of an unhandled exception. " "{exception}"
204 '{exception}'),
205 ),
205 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
206 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
206 'This pull request cannot be merged because of merge conflicts. {unresolved_files}'),
207 "This pull request cannot be merged because of merge conflicts. {unresolved_files}"
208 ),
207 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
209 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
208 'This pull request could not be merged because push to '
210 "This pull request could not be merged because push to " "target:`{target}@{merge_commit}` failed."
209 'target:`{target}@{merge_commit}` failed.'),
211 ),
210 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
212 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
211 'This pull request cannot be merged because the target '
213 "This pull request cannot be merged because the target " "`{target_ref.name}` is not a head."
212 '`{target_ref.name}` is not a head.'),
214 ),
213 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
215 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
214 'This pull request cannot be merged because the source contains '
216 "This pull request cannot be merged because the source contains " "more branches than the target."
215 'more branches than the target.'),
217 ),
216 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
218 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
217 'This pull request cannot be merged because the target `{target_ref.name}` '
219 "This pull request cannot be merged because the target `{target_ref.name}` "
218 'has multiple heads: `{heads}`.'),
220 "has multiple heads: `{heads}`."
221 ),
219 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
222 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
220 'This pull request cannot be merged because the target repository is '
223 "This pull request cannot be merged because the target repository is " "locked by {locked_by}."
221 'locked by {locked_by}.'),
224 ),
222
223 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
225 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
224 'This pull request cannot be merged because the target '
226 "This pull request cannot be merged because the target " "reference `{target_ref.name}` is missing."
225 'reference `{target_ref.name}` is missing.'),
227 ),
226 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
228 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
227 'This pull request cannot be merged because the source '
229 "This pull request cannot be merged because the source " "reference `{source_ref.name}` is missing."
228 'reference `{source_ref.name}` is missing.'),
230 ),
229 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
231 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
230 'This pull request cannot be merged because of conflicts related '
232 "This pull request cannot be merged because of conflicts related " "to sub repositories."
231 'to sub repositories.'),
233 ),
232
233 # Deprecations
234 # Deprecations
234 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
235 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
235 'This pull request cannot be merged because the target or the '
236 "This pull request cannot be merged because the target or the " "source reference is missing."
236 'source reference is missing.'),
237 ),
237
238 }
238 }
239
239
240 def __init__(self, possible, executed, merge_ref: Reference, failure_reason, metadata=None):
240 def __init__(self, possible, executed, merge_ref: Reference, failure_reason, metadata=None):
@@ -245,19 +245,20 b' class MergeResponse(object):'
245 self.metadata = metadata or {}
245 self.metadata = metadata or {}
246
246
247 def __repr__(self):
247 def __repr__(self):
248 return f'<MergeResponse:{self.label} {self.failure_reason}>'
248 return f"<MergeResponse:{self.label} {self.failure_reason}>"
249
249
250 def __eq__(self, other):
250 def __eq__(self, other):
251 same_instance = isinstance(other, self.__class__)
251 same_instance = isinstance(other, self.__class__)
252 return same_instance \
252 return (
253 and self.possible == other.possible \
253 same_instance
254 and self.executed == other.executed \
254 and self.possible == other.possible
255 and self.failure_reason == other.failure_reason
255 and self.executed == other.executed
256 and self.failure_reason == other.failure_reason
257 )
256
258
257 @property
259 @property
258 def label(self):
260 def label(self):
259 label_dict = dict((v, k) for k, v in MergeFailureReason.__dict__.items() if
261 label_dict = dict((v, k) for k, v in MergeFailureReason.__dict__.items() if not k.startswith("_"))
260 not k.startswith('_'))
261 return label_dict.get(self.failure_reason)
262 return label_dict.get(self.failure_reason)
262
263
263 @property
264 @property
@@ -270,13 +271,12 b' class MergeResponse(object):'
270 try:
271 try:
271 return msg.format(**self.metadata)
272 return msg.format(**self.metadata)
272 except Exception:
273 except Exception:
273 log.exception('Failed to format %s message', self)
274 log.exception("Failed to format %s message", self)
274 return msg
275 return msg
275
276
276 def asdict(self):
277 def asdict(self):
277 data = {}
278 data = {}
278 for k in ['possible', 'executed', 'merge_ref', 'failure_reason',
279 for k in ["possible", "executed", "merge_ref", "failure_reason", "merge_status_message"]:
279 'merge_status_message']:
280 data[k] = getattr(self, k)
280 data[k] = getattr(self, k)
281 return data
281 return data
282
282
@@ -289,607 +289,7 b' class SourceRefMissing(ValueError):'
289 pass
289 pass
290
290
291
291
292 class BaseRepository(object):
292 class BaseCommit:
293 """
294 Base Repository for final backends
295
296 .. attribute:: DEFAULT_BRANCH_NAME
297
298 name of default branch (i.e. "trunk" for svn, "master" for git etc.
299
300 .. attribute:: commit_ids
301
302 list of all available commit ids, in ascending order
303
304 .. attribute:: path
305
306 absolute path to the repository
307
308 .. attribute:: bookmarks
309
310 Mapping from name to :term:`Commit ID` of the bookmark. Empty in case
311 there are no bookmarks or the backend implementation does not support
312 bookmarks.
313
314 .. attribute:: tags
315
316 Mapping from name to :term:`Commit ID` of the tag.
317
318 """
319
320 DEFAULT_BRANCH_NAME = None
321 DEFAULT_CONTACT = "Unknown"
322 DEFAULT_DESCRIPTION = "unknown"
323 EMPTY_COMMIT_ID = '0' * 40
324 COMMIT_ID_PAT = re.compile(r'[0-9a-fA-F]{40}')
325
326 path = None
327
328 _is_empty = None
329 _commit_ids = {}
330
331 def __init__(self, repo_path, config=None, create=False, **kwargs):
332 """
333 Initializes repository. Raises RepositoryError if repository could
334 not be find at the given ``repo_path`` or directory at ``repo_path``
335 exists and ``create`` is set to True.
336
337 :param repo_path: local path of the repository
338 :param config: repository configuration
339 :param create=False: if set to True, would try to create repository.
340 :param src_url=None: if set, should be proper url from which repository
341 would be cloned; requires ``create`` parameter to be set to True -
342 raises RepositoryError if src_url is set and create evaluates to
343 False
344 """
345 raise NotImplementedError
346
347 def __repr__(self):
348 return f'<{self.__class__.__name__} at {self.path}>'
349
350 def __len__(self):
351 return self.count()
352
353 def __eq__(self, other):
354 same_instance = isinstance(other, self.__class__)
355 return same_instance and other.path == self.path
356
357 def __ne__(self, other):
358 return not self.__eq__(other)
359
360 def get_create_shadow_cache_pr_path(self, db_repo):
361 path = db_repo.cached_diffs_dir
362 if not os.path.exists(path):
363 os.makedirs(path, 0o755)
364 return path
365
366 @classmethod
367 def get_default_config(cls, default=None):
368 config = Config()
369 if default and isinstance(default, list):
370 for section, key, val in default:
371 config.set(section, key, val)
372 return config
373
374 @LazyProperty
375 def _remote(self):
376 raise NotImplementedError
377
378 def _heads(self, branch=None):
379 return []
380
381 @LazyProperty
382 def EMPTY_COMMIT(self):
383 return EmptyCommit(self.EMPTY_COMMIT_ID)
384
385 @LazyProperty
386 def alias(self):
387 for k, v in settings.BACKENDS.items():
388 if v.split('.')[-1] == str(self.__class__.__name__):
389 return k
390
391 @LazyProperty
392 def name(self):
393 return safe_str(os.path.basename(self.path))
394
395 @LazyProperty
396 def description(self):
397 raise NotImplementedError
398
399 def refs(self):
400 """
401 returns a `dict` with branches, bookmarks, tags, and closed_branches
402 for this repository
403 """
404 return dict(
405 branches=self.branches,
406 branches_closed=self.branches_closed,
407 tags=self.tags,
408 bookmarks=self.bookmarks
409 )
410
411 @LazyProperty
412 def branches(self):
413 """
414 A `dict` which maps branch names to commit ids.
415 """
416 raise NotImplementedError
417
418 @LazyProperty
419 def branches_closed(self):
420 """
421 A `dict` which maps tags names to commit ids.
422 """
423 raise NotImplementedError
424
425 @LazyProperty
426 def bookmarks(self):
427 """
428 A `dict` which maps tags names to commit ids.
429 """
430 raise NotImplementedError
431
432 @LazyProperty
433 def tags(self):
434 """
435 A `dict` which maps tags names to commit ids.
436 """
437 raise NotImplementedError
438
439 @LazyProperty
440 def size(self):
441 """
442 Returns combined size in bytes for all repository files
443 """
444 tip = self.get_commit()
445 return tip.size
446
447 def size_at_commit(self, commit_id):
448 commit = self.get_commit(commit_id)
449 return commit.size
450
451 def _check_for_empty(self):
452 no_commits = len(self._commit_ids) == 0
453 if no_commits:
454 # check on remote to be sure
455 return self._remote.is_empty()
456 else:
457 return False
458
459 def is_empty(self):
460 if rhodecode.is_test:
461 return self._check_for_empty()
462
463 if self._is_empty is None:
464 # cache empty for production, but not tests
465 self._is_empty = self._check_for_empty()
466
467 return self._is_empty
468
469 @staticmethod
470 def check_url(url, config):
471 """
472 Function will check given url and try to verify if it's a valid
473 link.
474 """
475 raise NotImplementedError
476
477 @staticmethod
478 def is_valid_repository(path):
479 """
480 Check if given `path` contains a valid repository of this backend
481 """
482 raise NotImplementedError
483
484 # ==========================================================================
485 # COMMITS
486 # ==========================================================================
487
488 @CachedProperty
489 def commit_ids(self):
490 raise NotImplementedError
491
492 def append_commit_id(self, commit_id):
493 if commit_id not in self.commit_ids:
494 self._rebuild_cache(self.commit_ids + [commit_id])
495
496 # clear cache
497 self._invalidate_prop_cache('commit_ids')
498 self._is_empty = False
499
500 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None,
501 translate_tag=None, maybe_unreachable=False, reference_obj=None):
502 """
503 Returns instance of `BaseCommit` class. If `commit_id` and `commit_idx`
504 are both None, most recent commit is returned.
505
506 :param pre_load: Optional. List of commit attributes to load.
507
508 :raises ``EmptyRepositoryError``: if there are no commits
509 """
510 raise NotImplementedError
511
512 def __iter__(self):
513 for commit_id in self.commit_ids:
514 yield self.get_commit(commit_id=commit_id)
515
516 def get_commits(
517 self, start_id=None, end_id=None, start_date=None, end_date=None,
518 branch_name=None, show_hidden=False, pre_load=None, translate_tags=None):
519 """
520 Returns iterator of `BaseCommit` objects from start to end
521 not inclusive. This should behave just like a list, ie. end is not
522 inclusive.
523
524 :param start_id: None or str, must be a valid commit id
525 :param end_id: None or str, must be a valid commit id
526 :param start_date:
527 :param end_date:
528 :param branch_name:
529 :param show_hidden:
530 :param pre_load:
531 :param translate_tags:
532 """
533 raise NotImplementedError
534
535 def __getitem__(self, key):
536 """
537 Allows index based access to the commit objects of this repository.
538 """
539 pre_load = ["author", "branch", "date", "message", "parents"]
540 if isinstance(key, slice):
541 return self._get_range(key, pre_load)
542 return self.get_commit(commit_idx=key, pre_load=pre_load)
543
544 def _get_range(self, slice_obj, pre_load):
545 for commit_id in self.commit_ids.__getitem__(slice_obj):
546 yield self.get_commit(commit_id=commit_id, pre_load=pre_load)
547
548 def count(self):
549 return len(self.commit_ids)
550
551 def tag(self, name, user, commit_id=None, message=None, date=None, **opts):
552 """
553 Creates and returns a tag for the given ``commit_id``.
554
555 :param name: name for new tag
556 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
557 :param commit_id: commit id for which new tag would be created
558 :param message: message of the tag's commit
559 :param date: date of tag's commit
560
561 :raises TagAlreadyExistError: if tag with same name already exists
562 """
563 raise NotImplementedError
564
565 def remove_tag(self, name, user, message=None, date=None):
566 """
567 Removes tag with the given ``name``.
568
569 :param name: name of the tag to be removed
570 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
571 :param message: message of the tag's removal commit
572 :param date: date of tag's removal commit
573
574 :raises TagDoesNotExistError: if tag with given name does not exists
575 """
576 raise NotImplementedError
577
578 def get_diff(
579 self, commit1, commit2, path=None, ignore_whitespace=False,
580 context=3, path1=None):
581 """
582 Returns (git like) *diff*, as plain text. Shows changes introduced by
583 `commit2` since `commit1`.
584
585 :param commit1: Entry point from which diff is shown. Can be
586 ``self.EMPTY_COMMIT`` - in this case, patch showing all
587 the changes since empty state of the repository until `commit2`
588 :param commit2: Until which commit changes should be shown.
589 :param path: Can be set to a path of a file to create a diff of that
590 file. If `path1` is also set, this value is only associated to
591 `commit2`.
592 :param ignore_whitespace: If set to ``True``, would not show whitespace
593 changes. Defaults to ``False``.
594 :param context: How many lines before/after changed lines should be
595 shown. Defaults to ``3``.
596 :param path1: Can be set to a path to associate with `commit1`. This
597 parameter works only for backends which support diff generation for
598 different paths. Other backends will raise a `ValueError` if `path1`
599 is set and has a different value than `path`.
600 :param file_path: filter this diff by given path pattern
601 """
602 raise NotImplementedError
603
604 def strip(self, commit_id, branch=None):
605 """
606 Strip given commit_id from the repository
607 """
608 raise NotImplementedError
609
610 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
611 """
612 Return a latest common ancestor commit if one exists for this repo
613 `commit_id1` vs `commit_id2` from `repo2`.
614
615 :param commit_id1: Commit it from this repository to use as a
616 target for the comparison.
617 :param commit_id2: Source commit id to use for comparison.
618 :param repo2: Source repository to use for comparison.
619 """
620 raise NotImplementedError
621
622 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
623 """
624 Compare this repository's revision `commit_id1` with `commit_id2`.
625
626 Returns a tuple(commits, ancestor) that would be merged from
627 `commit_id2`. Doing a normal compare (``merge=False``), ``None``
628 will be returned as ancestor.
629
630 :param commit_id1: Commit it from this repository to use as a
631 target for the comparison.
632 :param commit_id2: Source commit id to use for comparison.
633 :param repo2: Source repository to use for comparison.
634 :param merge: If set to ``True`` will do a merge compare which also
635 returns the common ancestor.
636 :param pre_load: Optional. List of commit attributes to load.
637 """
638 raise NotImplementedError
639
640 def merge(self, repo_id, workspace_id, target_ref, source_repo, source_ref,
641 user_name='', user_email='', message='', dry_run=False,
642 use_rebase=False, close_branch=False):
643 """
644 Merge the revisions specified in `source_ref` from `source_repo`
645 onto the `target_ref` of this repository.
646
647 `source_ref` and `target_ref` are named tupls with the following
648 fields `type`, `name` and `commit_id`.
649
650 Returns a MergeResponse named tuple with the following fields
651 'possible', 'executed', 'source_commit', 'target_commit',
652 'merge_commit'.
653
654 :param repo_id: `repo_id` target repo id.
655 :param workspace_id: `workspace_id` unique identifier.
656 :param target_ref: `target_ref` points to the commit on top of which
657 the `source_ref` should be merged.
658 :param source_repo: The repository that contains the commits to be
659 merged.
660 :param source_ref: `source_ref` points to the topmost commit from
661 the `source_repo` which should be merged.
662 :param user_name: Merge commit `user_name`.
663 :param user_email: Merge commit `user_email`.
664 :param message: Merge commit `message`.
665 :param dry_run: If `True` the merge will not take place.
666 :param use_rebase: If `True` commits from the source will be rebased
667 on top of the target instead of being merged.
668 :param close_branch: If `True` branch will be close before merging it
669 """
670 if dry_run:
671 message = message or settings.MERGE_DRY_RUN_MESSAGE
672 user_email = user_email or settings.MERGE_DRY_RUN_EMAIL
673 user_name = user_name or settings.MERGE_DRY_RUN_USER
674 else:
675 if not user_name:
676 raise ValueError('user_name cannot be empty')
677 if not user_email:
678 raise ValueError('user_email cannot be empty')
679 if not message:
680 raise ValueError('message cannot be empty')
681
682 try:
683 return self._merge_repo(
684 repo_id, workspace_id, target_ref, source_repo,
685 source_ref, message, user_name, user_email, dry_run=dry_run,
686 use_rebase=use_rebase, close_branch=close_branch)
687 except RepositoryError as exc:
688 log.exception('Unexpected failure when running merge, dry-run=%s', dry_run)
689 return MergeResponse(
690 False, False, None, MergeFailureReason.UNKNOWN,
691 metadata={'exception': str(exc)})
692
693 def _merge_repo(self, repo_id, workspace_id, target_ref,
694 source_repo, source_ref, merge_message,
695 merger_name, merger_email, dry_run=False,
696 use_rebase=False, close_branch=False):
697 """Internal implementation of merge."""
698 raise NotImplementedError
699
700 def _maybe_prepare_merge_workspace(
701 self, repo_id, workspace_id, target_ref, source_ref):
702 """
703 Create the merge workspace.
704
705 :param workspace_id: `workspace_id` unique identifier.
706 """
707 raise NotImplementedError
708
709 @classmethod
710 def _get_legacy_shadow_repository_path(cls, repo_path, workspace_id):
711 """
712 Legacy version that was used before. We still need it for
713 backward compat
714 """
715 return os.path.join(
716 os.path.dirname(repo_path),
717 f'.__shadow_{os.path.basename(repo_path)}_{workspace_id}')
718
719 @classmethod
720 def _get_shadow_repository_path(cls, repo_path, repo_id, workspace_id):
721 # The name of the shadow repository must start with '.', so it is
722 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
723 legacy_repository_path = cls._get_legacy_shadow_repository_path(repo_path, workspace_id)
724 if os.path.exists(legacy_repository_path):
725 return legacy_repository_path
726 else:
727 return os.path.join(
728 os.path.dirname(repo_path),
729 f'.__shadow_repo_{repo_id}_{workspace_id}')
730
731 def cleanup_merge_workspace(self, repo_id, workspace_id):
732 """
733 Remove merge workspace.
734
735 This function MUST not fail in case there is no workspace associated to
736 the given `workspace_id`.
737
738 :param workspace_id: `workspace_id` unique identifier.
739 """
740 shadow_repository_path = self._get_shadow_repository_path(
741 self.path, repo_id, workspace_id)
742 shadow_repository_path_del = '{}.{}.delete'.format(
743 shadow_repository_path, time.time())
744
745 # move the shadow repo, so it never conflicts with the one used.
746 # we use this method because shutil.rmtree had some edge case problems
747 # removing symlinked repositories
748 if not os.path.isdir(shadow_repository_path):
749 return
750
751 shutil.move(shadow_repository_path, shadow_repository_path_del)
752 try:
753 shutil.rmtree(shadow_repository_path_del, ignore_errors=False)
754 except Exception:
755 log.exception('Failed to gracefully remove shadow repo under %s',
756 shadow_repository_path_del)
757 shutil.rmtree(shadow_repository_path_del, ignore_errors=True)
758
759 # ========== #
760 # COMMIT API #
761 # ========== #
762
763 @LazyProperty
764 def in_memory_commit(self):
765 """
766 Returns :class:`InMemoryCommit` object for this repository.
767 """
768 raise NotImplementedError
769
770 # ======================== #
771 # UTILITIES FOR SUBCLASSES #
772 # ======================== #
773
774 def _validate_diff_commits(self, commit1, commit2):
775 """
776 Validates that the given commits are related to this repository.
777
778 Intended as a utility for sub classes to have a consistent validation
779 of input parameters in methods like :meth:`get_diff`.
780 """
781 self._validate_commit(commit1)
782 self._validate_commit(commit2)
783 if (isinstance(commit1, EmptyCommit) and
784 isinstance(commit2, EmptyCommit)):
785 raise ValueError("Cannot compare two empty commits")
786
787 def _validate_commit(self, commit):
788 if not isinstance(commit, BaseCommit):
789 raise TypeError(
790 "%s is not of type BaseCommit" % repr(commit))
791 if commit.repository != self and not isinstance(commit, EmptyCommit):
792 raise ValueError(
793 "Commit %s must be a valid commit from this repository %s, "
794 "related to this repository instead %s." %
795 (commit, self, commit.repository))
796
797 def _validate_commit_id(self, commit_id):
798 if not isinstance(commit_id, str):
799 raise TypeError(f"commit_id must be a string value got {type(commit_id)} instead")
800
801 def _validate_commit_idx(self, commit_idx):
802 if not isinstance(commit_idx, int):
803 raise TypeError(f"commit_idx must be a numeric value, got {type(commit_idx)}")
804
805 def _validate_branch_name(self, branch_name):
806 if branch_name and branch_name not in self.branches_all:
807 msg = (f"Branch {branch_name} not found in {self}")
808 raise BranchDoesNotExistError(msg)
809
810 #
811 # Supporting deprecated API parts
812 # TODO: johbo: consider to move this into a mixin
813 #
814
815 @property
816 def EMPTY_CHANGESET(self):
817 warnings.warn(
818 "Use EMPTY_COMMIT or EMPTY_COMMIT_ID instead", DeprecationWarning)
819 return self.EMPTY_COMMIT_ID
820
821 @property
822 def revisions(self):
823 warnings.warn("Use commits attribute instead", DeprecationWarning)
824 return self.commit_ids
825
826 @revisions.setter
827 def revisions(self, value):
828 warnings.warn("Use commits attribute instead", DeprecationWarning)
829 self.commit_ids = value
830
831 def get_changeset(self, revision=None, pre_load=None):
832 warnings.warn("Use get_commit instead", DeprecationWarning)
833 commit_id = None
834 commit_idx = None
835 if isinstance(revision, str):
836 commit_id = revision
837 else:
838 commit_idx = revision
839 return self.get_commit(
840 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
841
842 def get_changesets(
843 self, start=None, end=None, start_date=None, end_date=None,
844 branch_name=None, pre_load=None):
845 warnings.warn("Use get_commits instead", DeprecationWarning)
846 start_id = self._revision_to_commit(start)
847 end_id = self._revision_to_commit(end)
848 return self.get_commits(
849 start_id=start_id, end_id=end_id, start_date=start_date,
850 end_date=end_date, branch_name=branch_name, pre_load=pre_load)
851
852 def _revision_to_commit(self, revision):
853 """
854 Translates a revision to a commit_id
855
856 Helps to support the old changeset based API which allows to use
857 commit ids and commit indices interchangeable.
858 """
859 if revision is None:
860 return revision
861
862 if isinstance(revision, str):
863 commit_id = revision
864 else:
865 commit_id = self.commit_ids[revision]
866 return commit_id
867
868 @property
869 def in_memory_changeset(self):
870 warnings.warn("Use in_memory_commit instead", DeprecationWarning)
871 return self.in_memory_commit
872
873 def get_path_permissions(self, username):
874 """
875 Returns a path permission checker or None if not supported
876
877 :param username: session user name
878 :return: an instance of BasePathPermissionChecker or None
879 """
880 return None
881
882 def install_hooks(self, force=False):
883 return self._remote.install_hooks(force)
884
885 def get_hooks_info(self):
886 return self._remote.get_hooks_info()
887
888 def vcsserver_invalidate_cache(self, delete=False):
889 return self._remote.vcsserver_invalidate_cache(delete)
890
891
892 class BaseCommit(object):
893 """
293 """
894 Each backend should implement it's commit representation.
294 Each backend should implement it's commit representation.
895
295
@@ -933,6 +333,7 b' class BaseCommit(object):'
933 list of parent commits
333 list of parent commits
934
334
935 """
335 """
336
936 repository = None
337 repository = None
937 branch = None
338 branch = None
938
339
@@ -942,7 +343,7 b' class BaseCommit(object):'
942 value as ``None``.
343 value as ``None``.
943 """
344 """
944
345
945 _ARCHIVE_PREFIX_TEMPLATE = '{repo_name}-{short_id}'
346 _ARCHIVE_PREFIX_TEMPLATE = "{repo_name}-{short_id}"
946 """
347 """
947 This template is used to generate a default prefix for repository archives
348 This template is used to generate a default prefix for repository archives
948 if no prefix has been specified.
349 if no prefix has been specified.
@@ -952,7 +353,7 b' class BaseCommit(object):'
952 return self.__str__()
353 return self.__str__()
953
354
954 def __str__(self):
355 def __str__(self):
955 return f'<{self.__class__.__name__} at {self.idx}:{self.short_id}>'
356 return f"<{self.__class__.__name__} at {self.idx}:{self.short_id}>"
956
357
957 def __eq__(self, other):
358 def __eq__(self, other):
958 same_instance = isinstance(other, self.__class__)
359 same_instance = isinstance(other, self.__class__)
@@ -962,26 +363,26 b' class BaseCommit(object):'
962 parents = []
363 parents = []
963 try:
364 try:
964 for parent in self.parents:
365 for parent in self.parents:
965 parents.append({'raw_id': parent.raw_id})
366 parents.append({"raw_id": parent.raw_id})
966 except NotImplementedError:
367 except NotImplementedError:
967 # empty commit doesn't have parents implemented
368 # empty commit doesn't have parents implemented
968 pass
369 pass
969
370
970 return {
371 return {
971 'short_id': self.short_id,
372 "short_id": self.short_id,
972 'raw_id': self.raw_id,
373 "raw_id": self.raw_id,
973 'revision': self.idx,
374 "revision": self.idx,
974 'message': self.message,
375 "message": self.message,
975 'date': self.date,
376 "date": self.date,
976 'author': self.author,
377 "author": self.author,
977 'parents': parents,
378 "parents": parents,
978 'branch': self.branch
379 "branch": self.branch,
979 }
380 }
980
381
981 def __getstate__(self):
382 def __getstate__(self):
982 d = self.__dict__.copy()
383 d = self.__dict__.copy()
983 d.pop('_remote', None)
384 d.pop("_remote", None)
984 d.pop('repository', None)
385 d.pop("repository", None)
985 return d
386 return d
986
387
987 def get_remote(self):
388 def get_remote(self):
@@ -992,9 +393,9 b' class BaseCommit(object):'
992
393
993 def _get_refs(self):
394 def _get_refs(self):
994 return {
395 return {
995 'branches': [self.branch] if self.branch else [],
396 "branches": [self.branch] if self.branch else [],
996 'bookmarks': getattr(self, 'bookmarks', []),
397 "bookmarks": getattr(self, "bookmarks", []),
997 'tags': self.tags
398 "tags": self.tags,
998 }
399 }
999
400
1000 @LazyProperty
401 @LazyProperty
@@ -1118,7 +519,7 b' class BaseCommit(object):'
1118 """
519 """
1119 raise NotImplementedError
520 raise NotImplementedError
1120
521
1121 def is_link(self, path):
522 def is_link(self, path: bytes):
1122 """
523 """
1123 Returns ``True`` if given `path` is a symlink
524 Returns ``True`` if given `path` is a symlink
1124 """
525 """
@@ -1154,25 +555,24 b' class BaseCommit(object):'
1154 """
555 """
1155 raise NotImplementedError
556 raise NotImplementedError
1156
557
1157 def get_path_commit(self, path, pre_load=None):
558 def get_path_commit(self, path: bytes, pre_load=None):
1158 """
559 """
1159 Returns last commit of the file at the given `path`.
560 Returns last commit of the file at the given `path`.
1160
561
1161 :param pre_load: Optional. List of commit attributes to load.
1162 """
562 """
1163 commits = self.get_path_history(path, limit=1, pre_load=pre_load)
563 commits = self.get_path_history(path, limit=1, pre_load=pre_load)
1164 if not commits:
564 if not commits:
1165 raise RepositoryError(
565 raise RepositoryError(
1166 'Failed to fetch history for path {}. '
566 f"Failed to fetch history for path {path}. Please check if such path exists in your repository"
1167 'Please check if such path exists in your repository'.format(
567 )
1168 path))
1169 return commits[0]
568 return commits[0]
1170
569
1171 def get_path_history(self, path, limit=None, pre_load=None):
570 def get_path_history(self, path: bytes, limit=None, pre_load=None):
1172 """
571 """
1173 Returns history of file as reversed list of :class:`BaseCommit`
572 Returns history of file as reversed list of :class:`BaseCommit`
1174 objects for which file at given `path` has been modified.
573 objects for which file at given `path` has been modified.
1175
574
575 :param path: file path or dir path
1176 :param limit: Optional. Allows to limit the size of the returned
576 :param limit: Optional. Allows to limit the size of the returned
1177 history. This is intended as a hint to the underlying backend, so
577 history. This is intended as a hint to the underlying backend, so
1178 that it can apply optimizations depending on the limit.
578 that it can apply optimizations depending on the limit.
@@ -1180,16 +580,17 b' class BaseCommit(object):'
1180 """
580 """
1181 raise NotImplementedError
581 raise NotImplementedError
1182
582
1183 def get_file_annotate(self, path, pre_load=None):
583 def get_file_annotate(self, path: bytes, pre_load=None):
1184 """
584 """
1185 Returns a generator of four element tuples with
585 Returns a generator of four element tuples with
1186 lineno, sha, commit lazy loader and line
586 lineno, sha, commit lazy loader and line
1187
587
588 :param path: file path
1188 :param pre_load: Optional. List of commit attributes to load.
589 :param pre_load: Optional. List of commit attributes to load.
1189 """
590 """
1190 raise NotImplementedError
591 raise NotImplementedError
1191
592
1192 def get_nodes(self, path, pre_load=None):
593 def get_nodes(self, path: bytes, pre_load=None):
1193 """
594 """
1194 Returns combined ``DirNode`` and ``FileNode`` objects list representing
595 Returns combined ``DirNode`` and ``FileNode`` objects list representing
1195 state of commit at the given ``path``.
596 state of commit at the given ``path``.
@@ -1199,7 +600,7 b' class BaseCommit(object):'
1199 """
600 """
1200 raise NotImplementedError
601 raise NotImplementedError
1201
602
1202 def get_node(self, path):
603 def get_node(self, path: bytes, pre_load=None):
1203 """
604 """
1204 Returns ``Node`` object from the given ``path``.
605 Returns ``Node`` object from the given ``path``.
1205
606
@@ -1208,16 +609,24 b' class BaseCommit(object):'
1208 """
609 """
1209 raise NotImplementedError
610 raise NotImplementedError
1210
611
1211 def get_largefile_node(self, path):
612 def get_largefile_node(self, path: bytes):
1212 """
613 """
1213 Returns the path to largefile from Mercurial/Git-lfs storage.
614 Returns the path to largefile from Mercurial/Git-lfs storage.
1214 or None if it's not a largefile node
615 or None if it's not a largefile node
1215 """
616 """
1216 return None
617 return None
1217
618
1218 def archive_repo(self, archive_name_key, kind='tgz', subrepos=None,
619 def archive_repo(
1219 archive_dir_name=None, write_metadata=False, mtime=None,
620 self,
1220 archive_at_path='/', cache_config=None):
621 archive_name_key,
622 kind="tgz",
623 subrepos=None,
624 archive_dir_name=None,
625 write_metadata=False,
626 mtime=None,
627 archive_at_path="/",
628 cache_config=None,
629 ):
1221 """
630 """
1222 Creates an archive containing the contents of the repository.
631 Creates an archive containing the contents of the repository.
1223
632
@@ -1237,27 +646,26 b' class BaseCommit(object):'
1237 cache_config = cache_config or {}
646 cache_config = cache_config or {}
1238 allowed_kinds = [x[0] for x in settings.ARCHIVE_SPECS]
647 allowed_kinds = [x[0] for x in settings.ARCHIVE_SPECS]
1239 if kind not in allowed_kinds:
648 if kind not in allowed_kinds:
1240 raise ImproperArchiveTypeError(
649 raise ImproperArchiveTypeError(f"Archive kind ({kind}) not supported use one of {allowed_kinds}")
1241 f'Archive kind ({kind}) not supported use one of {allowed_kinds}')
1242
650
1243 archive_dir_name = self._validate_archive_prefix(archive_dir_name)
651 archive_dir_name = self._validate_archive_prefix(archive_dir_name)
1244 mtime = mtime is not None or time.mktime(self.date.timetuple())
652 mtime = mtime is not None or time.mktime(self.date.timetuple())
1245 commit_id = self.raw_id
653 commit_id = self.raw_id
1246
654
1247 return self.repository._remote.archive_repo(
655 return self.repository._remote.archive_repo(
1248 archive_name_key, kind, mtime, archive_at_path,
656 archive_name_key, kind, mtime, archive_at_path, archive_dir_name, commit_id, cache_config
1249 archive_dir_name, commit_id, cache_config)
657 )
1250
658
1251 def _validate_archive_prefix(self, archive_dir_name):
659 def _validate_archive_prefix(self, archive_dir_name):
1252 if archive_dir_name is None:
660 if archive_dir_name is None:
1253 archive_dir_name = self._ARCHIVE_PREFIX_TEMPLATE.format(
661 archive_dir_name = self._ARCHIVE_PREFIX_TEMPLATE.format(
1254 repo_name=safe_str(self.repository.name),
662 repo_name=safe_str(self.repository.name), short_id=self.short_id
1255 short_id=self.short_id)
663 )
1256 elif not isinstance(archive_dir_name, str):
664 elif not isinstance(archive_dir_name, str):
1257 raise ValueError(f"archive_dir_name is not str object but: {type(archive_dir_name)}")
665 raise ValueError(f"archive_dir_name is not str object but: {type(archive_dir_name)}")
1258 elif archive_dir_name.startswith('/'):
666 elif archive_dir_name.startswith("/"):
1259 raise VCSError("Prefix cannot start with leading slash")
667 raise VCSError("Prefix cannot start with leading slash")
1260 elif archive_dir_name.strip() == '':
668 elif archive_dir_name.strip() == "":
1261 raise VCSError("Prefix cannot be empty")
669 raise VCSError("Prefix cannot be empty")
1262 elif not archive_dir_name.isascii():
670 elif not archive_dir_name.isascii():
1263 raise VCSError("Prefix cannot contain non ascii characters")
671 raise VCSError("Prefix cannot contain non ascii characters")
@@ -1268,7 +676,7 b' class BaseCommit(object):'
1268 """
676 """
1269 Returns ``RootNode`` object for this commit.
677 Returns ``RootNode`` object for this commit.
1270 """
678 """
1271 return self.get_node('')
679 return self.get_node(b"")
1272
680
1273 def next(self, branch=None):
681 def next(self, branch=None):
1274 """
682 """
@@ -1292,8 +700,7 b' class BaseCommit(object):'
1292
700
1293 def _find_next(self, indexes, branch=None):
701 def _find_next(self, indexes, branch=None):
1294 if branch and self.branch != branch:
702 if branch and self.branch != branch:
1295 raise VCSError('Branch option used on commit not belonging '
703 raise VCSError("Branch option used on commit not belonging " "to that branch")
1296 'to that branch')
1297
704
1298 for next_idx in indexes:
705 for next_idx in indexes:
1299 commit = self.repository.get_commit(commit_idx=next_idx)
706 commit = self.repository.get_commit(commit_idx=next_idx)
@@ -1307,41 +714,17 b' class BaseCommit(object):'
1307 Returns a `Diff` object representing the change made by this commit.
714 Returns a `Diff` object representing the change made by this commit.
1308 """
715 """
1309 parent = self.first_parent
716 parent = self.first_parent
1310 diff = self.repository.get_diff(
717 diff = self.repository.get_diff(parent, self, ignore_whitespace=ignore_whitespace, context=context)
1311 parent, self,
1312 ignore_whitespace=ignore_whitespace,
1313 context=context)
1314 return diff
718 return diff
1315
719
1316 @LazyProperty
720 @LazyProperty
1317 def added(self):
1318 """
1319 Returns list of added ``FileNode`` objects.
1320 """
1321 raise NotImplementedError
1322
1323 @LazyProperty
1324 def changed(self):
1325 """
1326 Returns list of modified ``FileNode`` objects.
1327 """
1328 raise NotImplementedError
1329
1330 @LazyProperty
1331 def removed(self):
1332 """
1333 Returns list of removed ``FileNode`` objects.
1334 """
1335 raise NotImplementedError
1336
1337 @LazyProperty
1338 def size(self):
721 def size(self):
1339 """
722 """
1340 Returns total number of bytes from contents of all filenodes.
723 Returns total number of bytes from contents of all filenodes.
1341 """
724 """
1342 return sum(node.size for node in self.get_filenodes_generator())
725 return sum(node.size for node in self.get_filenodes_generator())
1343
726
1344 def walk(self, topurl=''):
727 def walk(self, top_url=b""):
1345 """
728 """
1346 Similar to os.walk method. Insted of filesystem it walks through
729 Similar to os.walk method. Insted of filesystem it walks through
1347 commit starting at given ``topurl``. Returns generator of tuples
730 commit starting at given ``topurl``. Returns generator of tuples
@@ -1349,10 +732,10 b' class BaseCommit(object):'
1349 """
732 """
1350 from rhodecode.lib.vcs.nodes import DirNode
733 from rhodecode.lib.vcs.nodes import DirNode
1351
734
1352 if isinstance(topurl, DirNode):
735 if isinstance(top_url, DirNode):
1353 top_node = topurl
736 top_node = top_url
1354 else:
737 else:
1355 top_node = self.get_node(topurl)
738 top_node = self.get_node(top_url)
1356
739
1357 has_default_pre_load = False
740 has_default_pre_load = False
1358 if isinstance(top_node, DirNode):
741 if isinstance(top_node, DirNode):
@@ -1381,15 +764,20 b' class BaseCommit(object):'
1381
764
1382 def no_node_at_path(self, path):
765 def no_node_at_path(self, path):
1383 return NodeDoesNotExistError(
766 return NodeDoesNotExistError(
1384 f"There is no file nor directory at the given path: "
767 f"There is no file nor directory at the given path: " f"`{safe_str(path)}` at commit {self.short_id}"
1385 f"`{safe_str(path)}` at commit {self.short_id}")
768 )
1386
769
1387 def _fix_path(self, path: str) -> str:
770 @classmethod
771 def _fix_path(cls, path: bytes) -> bytes:
1388 """
772 """
1389 Paths are stored without trailing slash so we need to get rid off it if
773 Paths are stored without trailing slash so we need to get rid off it if needed.
1390 needed.
774 It also validates that path is a bytestring for support of mixed encodings...
1391 """
775 """
1392 return safe_str(path).rstrip('/')
776
777 if not isinstance(path, bytes):
778 raise ValueError(f'path=`{safe_str(path)}` must be bytes')
779
780 return path.rstrip(b"/")
1393
781
1394 #
782 #
1395 # Deprecated API based on changesets
783 # Deprecated API based on changesets
@@ -1410,17 +798,644 b' class BaseCommit(object):'
1410 return self.get_path_commit(path)
798 return self.get_path_commit(path)
1411
799
1412
800
801 class BaseRepository(object):
802 """
803 Base Repository for final backends
804
805 .. attribute:: DEFAULT_BRANCH_NAME
806
807 name of default branch (i.e. "trunk" for svn, "master" for git etc.
808
809 .. attribute:: commit_ids
810
811 list of all available commit ids, in ascending order
812
813 .. attribute:: path
814
815 absolute path to the repository
816
817 .. attribute:: bookmarks
818
819 Mapping from name to :term:`Commit ID` of the bookmark. Empty in case
820 there are no bookmarks or the backend implementation does not support
821 bookmarks.
822
823 .. attribute:: tags
824
825 Mapping from name to :term:`Commit ID` of the tag.
826
827 """
828
829 DEFAULT_BRANCH_NAME = None
830 DEFAULT_CONTACT = "Unknown"
831 DEFAULT_DESCRIPTION = "unknown"
832 EMPTY_COMMIT_ID = "0" * 40
833 COMMIT_ID_PAT = re.compile(r"[0-9a-fA-F]{40}")
834
835 path = None
836
837 _is_empty = None
838 _commit_ids = {}
839
840 def __init__(self, repo_path, config=None, create=False, **kwargs):
841 """
842 Initializes repository. Raises RepositoryError if repository could
843 not be find at the given ``repo_path`` or directory at ``repo_path``
844 exists and ``create`` is set to True.
845
846 :param repo_path: local path of the repository
847 :param config: repository configuration
848 :param create=False: if set to True, would try to create repository.
849 :param src_url=None: if set, should be proper url from which repository
850 would be cloned; requires ``create`` parameter to be set to True -
851 raises RepositoryError if src_url is set and create evaluates to
852 False
853 """
854 raise NotImplementedError
855
856 def __repr__(self):
857 return f"<{self.__class__.__name__} at {self.path}>"
858
859 def __len__(self):
860 return self.count()
861
862 def __eq__(self, other):
863 same_instance = isinstance(other, self.__class__)
864 return same_instance and other.path == self.path
865
866 def __ne__(self, other):
867 return not self.__eq__(other)
868
869 @classmethod
870 def get_create_shadow_cache_pr_path(cls, db_repo):
871 path = db_repo.cached_diffs_dir
872 if not os.path.exists(path):
873 os.makedirs(path, 0o755)
874 return path
875
876 @classmethod
877 def get_default_config(cls, default=None):
878 config = Config()
879 if default and isinstance(default, list):
880 for section, key, val in default:
881 config.set(section, key, val)
882 return config
883
884 @LazyProperty
885 def _remote(self):
886 raise NotImplementedError
887
888 def _heads(self, branch=None):
889 return []
890
891 @LazyProperty
892 def EMPTY_COMMIT(self):
893 return EmptyCommit(self.EMPTY_COMMIT_ID)
894
895 @LazyProperty
896 def alias(self):
897 for k, v in settings.BACKENDS.items():
898 if v.split(".")[-1] == str(self.__class__.__name__):
899 return k
900
901 @LazyProperty
902 def name(self):
903 return safe_str(os.path.basename(self.path))
904
905 @LazyProperty
906 def description(self):
907 raise NotImplementedError
908
909 def refs(self):
910 """
911 returns a `dict` with branches, bookmarks, tags, and closed_branches
912 for this repository
913 """
914 return dict(
915 branches=self.branches, branches_closed=self.branches_closed, tags=self.tags, bookmarks=self.bookmarks
916 )
917
918 @LazyProperty
919 def branches(self):
920 """
921 A `dict` which maps branch names to commit ids.
922 """
923 raise NotImplementedError
924
925 @LazyProperty
926 def branches_closed(self):
927 """
928 A `dict` which maps tags names to commit ids.
929 """
930 raise NotImplementedError
931
932 @LazyProperty
933 def bookmarks(self):
934 """
935 A `dict` which maps tags names to commit ids.
936 """
937 raise NotImplementedError
938
939 @LazyProperty
940 def tags(self):
941 """
942 A `dict` which maps tags names to commit ids.
943 """
944 raise NotImplementedError
945
946 @LazyProperty
947 def size(self):
948 """
949 Returns combined size in bytes for all repository files
950 """
951 tip = self.get_commit()
952 return tip.size
953
954 def size_at_commit(self, commit_id):
955 commit = self.get_commit(commit_id)
956 return commit.size
957
958 def _check_for_empty(self):
959 no_commits = len(self._commit_ids) == 0
960 if no_commits:
961 # check on remote to be sure
962 return self._remote.is_empty()
963 else:
964 return False
965
966 def is_empty(self):
967 if rhodecode.is_test:
968 return self._check_for_empty()
969
970 if self._is_empty is None:
971 # cache empty for production, but not tests
972 self._is_empty = self._check_for_empty()
973
974 return self._is_empty
975
976 @staticmethod
977 def check_url(url, config):
978 """
979 Function will check given url and try to verify if it's a valid
980 link.
981 """
982 raise NotImplementedError
983
984 @staticmethod
985 def is_valid_repository(path):
986 """
987 Check if given `path` contains a valid repository of this backend
988 """
989 raise NotImplementedError
990
991 # ==========================================================================
992 # COMMITS
993 # ==========================================================================
994
995 @CachedProperty
996 def commit_ids(self):
997 raise NotImplementedError
998
999 def append_commit_id(self, commit_id):
1000 if commit_id not in self.commit_ids:
1001 self._rebuild_cache(self.commit_ids + [commit_id])
1002
1003 # clear cache
1004 self._invalidate_prop_cache("commit_ids")
1005 self._is_empty = False
1006
1007 def get_commit(
1008 self,
1009 commit_id=None,
1010 commit_idx=None,
1011 pre_load=None,
1012 translate_tag=None,
1013 maybe_unreachable=False,
1014 reference_obj=None,
1015 ) -> BaseCommit:
1016 """
1017 Returns instance of `BaseCommit` class. If `commit_id` and `commit_idx`
1018 are both None, most recent commit is returned.
1019
1020 :param pre_load: Optional. List of commit attributes to load.
1021
1022 :raises ``EmptyRepositoryError``: if there are no commits
1023 """
1024 raise NotImplementedError
1025
1026 def __iter__(self):
1027 for commit_id in self.commit_ids:
1028 yield self.get_commit(commit_id=commit_id)
1029
1030 def get_commits(
1031 self,
1032 start_id=None,
1033 end_id=None,
1034 start_date=None,
1035 end_date=None,
1036 branch_name=None,
1037 show_hidden=False,
1038 pre_load=None,
1039 translate_tags=None,
1040 ):
1041 """
1042 Returns iterator of `BaseCommit` objects from start to end
1043 not inclusive. This should behave just like a list, ie. end is not
1044 inclusive.
1045
1046 :param start_id: None or str, must be a valid commit id
1047 :param end_id: None or str, must be a valid commit id
1048 :param start_date:
1049 :param end_date:
1050 :param branch_name:
1051 :param show_hidden:
1052 :param pre_load:
1053 :param translate_tags:
1054 """
1055 raise NotImplementedError
1056
1057 def __getitem__(self, key):
1058 """
1059 Allows index based access to the commit objects of this repository.
1060 """
1061 pre_load = ["author", "branch", "date", "message", "parents"]
1062 if isinstance(key, slice):
1063 return self._get_range(key, pre_load)
1064 return self.get_commit(commit_idx=key, pre_load=pre_load)
1065
1066 def _get_range(self, slice_obj, pre_load):
1067 for commit_id in self.commit_ids.__getitem__(slice_obj):
1068 yield self.get_commit(commit_id=commit_id, pre_load=pre_load)
1069
1070 def count(self):
1071 return len(self.commit_ids)
1072
1073 def tag(self, name, user, commit_id=None, message=None, date=None, **opts):
1074 """
1075 Creates and returns a tag for the given ``commit_id``.
1076
1077 :param name: name for new tag
1078 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
1079 :param commit_id: commit id for which new tag would be created
1080 :param message: message of the tag's commit
1081 :param date: date of tag's commit
1082
1083 :raises TagAlreadyExistError: if tag with same name already exists
1084 """
1085 raise NotImplementedError
1086
1087 def remove_tag(self, name, user, message=None, date=None):
1088 """
1089 Removes tag with the given ``name``.
1090
1091 :param name: name of the tag to be removed
1092 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
1093 :param message: message of the tag's removal commit
1094 :param date: date of tag's removal commit
1095
1096 :raises TagDoesNotExistError: if tag with given name does not exists
1097 """
1098 raise NotImplementedError
1099
1100 def get_diff(self, commit1, commit2, path=None, ignore_whitespace=False, context=3, path1=None):
1101 """
1102 Returns (git like) *diff*, as plain text. Shows changes introduced by
1103 `commit2` since `commit1`.
1104
1105 :param commit1: Entry point from which diff is shown. Can be
1106 ``self.EMPTY_COMMIT`` - in this case, patch showing all
1107 the changes since empty state of the repository until `commit2`
1108 :param commit2: Until which commit changes should be shown.
1109 :param path: Can be set to a path of a file to create a diff of that
1110 file. If `path1` is also set, this value is only associated to
1111 `commit2`.
1112 :param ignore_whitespace: If set to ``True``, would not show whitespace
1113 changes. Defaults to ``False``.
1114 :param context: How many lines before/after changed lines should be
1115 shown. Defaults to ``3``.
1116 :param path1: Can be set to a path to associate with `commit1`. This
1117 parameter works only for backends which support diff generation for
1118 different paths. Other backends will raise a `ValueError` if `path1`
1119 is set and has a different value than `path`.
1120 :param file_path: filter this diff by given path pattern
1121 """
1122 raise NotImplementedError
1123
1124 def strip(self, commit_id, branch=None):
1125 """
1126 Strip given commit_id from the repository
1127 """
1128 raise NotImplementedError
1129
1130 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
1131 """
1132 Return a latest common ancestor commit if one exists for this repo
1133 `commit_id1` vs `commit_id2` from `repo2`.
1134
1135 :param commit_id1: Commit it from this repository to use as a
1136 target for the comparison.
1137 :param commit_id2: Source commit id to use for comparison.
1138 :param repo2: Source repository to use for comparison.
1139 """
1140 raise NotImplementedError
1141
1142 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
1143 """
1144 Compare this repository's revision `commit_id1` with `commit_id2`.
1145
1146 Returns a tuple(commits, ancestor) that would be merged from
1147 `commit_id2`. Doing a normal compare (``merge=False``), ``None``
1148 will be returned as ancestor.
1149
1150 :param commit_id1: Commit it from this repository to use as a
1151 target for the comparison.
1152 :param commit_id2: Source commit id to use for comparison.
1153 :param repo2: Source repository to use for comparison.
1154 :param merge: If set to ``True`` will do a merge compare which also
1155 returns the common ancestor.
1156 :param pre_load: Optional. List of commit attributes to load.
1157 """
1158 raise NotImplementedError
1159
1160 def merge(
1161 self,
1162 repo_id,
1163 workspace_id,
1164 target_ref,
1165 source_repo,
1166 source_ref,
1167 user_name="",
1168 user_email="",
1169 message="",
1170 dry_run=False,
1171 use_rebase=False,
1172 close_branch=False,
1173 ):
1174 """
1175 Merge the revisions specified in `source_ref` from `source_repo`
1176 onto the `target_ref` of this repository.
1177
1178 `source_ref` and `target_ref` are named tupls with the following
1179 fields `type`, `name` and `commit_id`.
1180
1181 Returns a MergeResponse named tuple with the following fields
1182 'possible', 'executed', 'source_commit', 'target_commit',
1183 'merge_commit'.
1184
1185 :param repo_id: `repo_id` target repo id.
1186 :param workspace_id: `workspace_id` unique identifier.
1187 :param target_ref: `target_ref` points to the commit on top of which
1188 the `source_ref` should be merged.
1189 :param source_repo: The repository that contains the commits to be
1190 merged.
1191 :param source_ref: `source_ref` points to the topmost commit from
1192 the `source_repo` which should be merged.
1193 :param user_name: Merge commit `user_name`.
1194 :param user_email: Merge commit `user_email`.
1195 :param message: Merge commit `message`.
1196 :param dry_run: If `True` the merge will not take place.
1197 :param use_rebase: If `True` commits from the source will be rebased
1198 on top of the target instead of being merged.
1199 :param close_branch: If `True` branch will be close before merging it
1200 """
1201 if dry_run:
1202 message = message or settings.MERGE_DRY_RUN_MESSAGE
1203 user_email = user_email or settings.MERGE_DRY_RUN_EMAIL
1204 user_name = user_name or settings.MERGE_DRY_RUN_USER
1205 else:
1206 if not user_name:
1207 raise ValueError("user_name cannot be empty")
1208 if not user_email:
1209 raise ValueError("user_email cannot be empty")
1210 if not message:
1211 raise ValueError("message cannot be empty")
1212
1213 try:
1214 return self._merge_repo(
1215 repo_id,
1216 workspace_id,
1217 target_ref,
1218 source_repo,
1219 source_ref,
1220 message,
1221 user_name,
1222 user_email,
1223 dry_run=dry_run,
1224 use_rebase=use_rebase,
1225 close_branch=close_branch,
1226 )
1227 except RepositoryError as exc:
1228 log.exception("Unexpected failure when running merge, dry-run=%s", dry_run)
1229 return MergeResponse(False, False, None, MergeFailureReason.UNKNOWN, metadata={"exception": str(exc)})
1230
1231 def _merge_repo(
1232 self,
1233 repo_id,
1234 workspace_id,
1235 target_ref,
1236 source_repo,
1237 source_ref,
1238 merge_message,
1239 merger_name,
1240 merger_email,
1241 dry_run=False,
1242 use_rebase=False,
1243 close_branch=False,
1244 ):
1245 """Internal implementation of merge."""
1246 raise NotImplementedError
1247
1248 def _maybe_prepare_merge_workspace(self, repo_id, workspace_id, target_ref, source_ref):
1249 """
1250 Create the merge workspace.
1251
1252 :param workspace_id: `workspace_id` unique identifier.
1253 """
1254 raise NotImplementedError
1255
1256 @classmethod
1257 def _get_legacy_shadow_repository_path(cls, repo_path, workspace_id):
1258 """
1259 Legacy version that was used before. We still need it for
1260 backward compat
1261 """
1262 return os.path.join(os.path.dirname(repo_path), f".__shadow_{os.path.basename(repo_path)}_{workspace_id}")
1263
1264 @classmethod
1265 def _get_shadow_repository_path(cls, repo_path, repo_id, workspace_id):
1266 # The name of the shadow repository must start with '.', so it is
1267 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
1268 legacy_repository_path = cls._get_legacy_shadow_repository_path(repo_path, workspace_id)
1269 if os.path.exists(legacy_repository_path):
1270 return legacy_repository_path
1271 else:
1272 return os.path.join(os.path.dirname(repo_path), f".__shadow_repo_{repo_id}_{workspace_id}")
1273
1274 def cleanup_merge_workspace(self, repo_id, workspace_id):
1275 """
1276 Remove merge workspace.
1277
1278 This function MUST not fail in case there is no workspace associated to
1279 the given `workspace_id`.
1280
1281 :param workspace_id: `workspace_id` unique identifier.
1282 """
1283 shadow_repository_path = self._get_shadow_repository_path(self.path, repo_id, workspace_id)
1284 shadow_repository_path_del = "{}.{}.delete".format(shadow_repository_path, time.time())
1285
1286 # move the shadow repo, so it never conflicts with the one used.
1287 # we use this method because shutil.rmtree had some edge case problems
1288 # removing symlinked repositories
1289 if not os.path.isdir(shadow_repository_path):
1290 return
1291
1292 shutil.move(shadow_repository_path, shadow_repository_path_del)
1293 try:
1294 shutil.rmtree(shadow_repository_path_del, ignore_errors=False)
1295 except Exception:
1296 log.exception("Failed to gracefully remove shadow repo under %s", shadow_repository_path_del)
1297 shutil.rmtree(shadow_repository_path_del, ignore_errors=True)
1298
1299 # ========== #
1300 # COMMIT API #
1301 # ========== #
1302
1303 @LazyProperty
1304 def in_memory_commit(self):
1305 """
1306 Returns :class:`InMemoryCommit` object for this repository.
1307 """
1308 raise NotImplementedError
1309
1310 # ======================== #
1311 # UTILITIES FOR SUBCLASSES #
1312 # ======================== #
1313
1314 def _validate_diff_commits(self, commit1, commit2):
1315 """
1316 Validates that the given commits are related to this repository.
1317
1318 Intended as a utility for sub classes to have a consistent validation
1319 of input parameters in methods like :meth:`get_diff`.
1320 """
1321 self._validate_commit(commit1)
1322 self._validate_commit(commit2)
1323 if isinstance(commit1, EmptyCommit) and isinstance(commit2, EmptyCommit):
1324 raise ValueError("Cannot compare two empty commits")
1325
1326 def _validate_commit(self, commit):
1327 if not isinstance(commit, BaseCommit):
1328 raise TypeError("%s is not of type BaseCommit" % repr(commit))
1329 if commit.repository != self and not isinstance(commit, EmptyCommit):
1330 raise ValueError(
1331 "Commit %s must be a valid commit from this repository %s, "
1332 "related to this repository instead %s." % (commit, self, commit.repository)
1333 )
1334
1335 def _validate_commit_id(self, commit_id):
1336 if not isinstance(commit_id, str):
1337 raise TypeError(f"commit_id must be a string value got {type(commit_id)} instead")
1338
1339 def _validate_commit_idx(self, commit_idx):
1340 if not isinstance(commit_idx, int):
1341 raise TypeError(f"commit_idx must be a numeric value, got {type(commit_idx)}")
1342
1343 def _validate_branch_name(self, branch_name):
1344 if branch_name and branch_name not in self.branches_all:
1345 msg = f"Branch {branch_name} not found in {self}"
1346 raise BranchDoesNotExistError(msg)
1347
1348 #
1349 # Supporting deprecated API parts
1350 # TODO: johbo: consider to move this into a mixin
1351 #
1352
1353 @property
1354 def EMPTY_CHANGESET(self):
1355 warnings.warn("Use EMPTY_COMMIT or EMPTY_COMMIT_ID instead", DeprecationWarning)
1356 return self.EMPTY_COMMIT_ID
1357
1358 @property
1359 def revisions(self):
1360 warnings.warn("Use commits attribute instead", DeprecationWarning)
1361 return self.commit_ids
1362
1363 @revisions.setter
1364 def revisions(self, value):
1365 warnings.warn("Use commits attribute instead", DeprecationWarning)
1366 self.commit_ids = value
1367
1368 def get_changeset(self, revision=None, pre_load=None):
1369 warnings.warn("Use get_commit instead", DeprecationWarning)
1370 commit_id = None
1371 commit_idx = None
1372 if isinstance(revision, str):
1373 commit_id = revision
1374 else:
1375 commit_idx = revision
1376 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
1377
1378 def get_changesets(self, start=None, end=None, start_date=None, end_date=None, branch_name=None, pre_load=None):
1379 warnings.warn("Use get_commits instead", DeprecationWarning)
1380 start_id = self._revision_to_commit(start)
1381 end_id = self._revision_to_commit(end)
1382 return self.get_commits(
1383 start_id=start_id,
1384 end_id=end_id,
1385 start_date=start_date,
1386 end_date=end_date,
1387 branch_name=branch_name,
1388 pre_load=pre_load,
1389 )
1390
1391 def _revision_to_commit(self, revision):
1392 """
1393 Translates a revision to a commit_id
1394
1395 Helps to support the old changeset based API which allows to use
1396 commit ids and commit indices interchangeable.
1397 """
1398 if revision is None:
1399 return revision
1400
1401 if isinstance(revision, str):
1402 commit_id = revision
1403 else:
1404 commit_id = self.commit_ids[revision]
1405 return commit_id
1406
1407 @property
1408 def in_memory_changeset(self):
1409 warnings.warn("Use in_memory_commit instead", DeprecationWarning)
1410 return self.in_memory_commit
1411
1412 def get_path_permissions(self, username):
1413 """
1414 Returns a path permission checker or None if not supported
1415
1416 :param username: session user name
1417 :return: an instance of BasePathPermissionChecker or None
1418 """
1419 return None
1420
1421 def install_hooks(self, force=False):
1422 return self._remote.install_hooks(force)
1423
1424 def get_hooks_info(self):
1425 return self._remote.get_hooks_info()
1426
1427 def vcsserver_invalidate_cache(self, delete=False):
1428 return self._remote.vcsserver_invalidate_cache(delete)
1429
1430
1413 class BaseChangesetClass(type):
1431 class BaseChangesetClass(type):
1414
1415 def __instancecheck__(self, instance):
1432 def __instancecheck__(self, instance):
1416 return isinstance(instance, BaseCommit)
1433 return isinstance(instance, BaseCommit)
1417
1434
1418
1435
1419 class BaseChangeset(BaseCommit, metaclass=BaseChangesetClass):
1436 class BaseChangeset(BaseCommit, metaclass=BaseChangesetClass):
1420
1421 def __new__(cls, *args, **kwargs):
1437 def __new__(cls, *args, **kwargs):
1422 warnings.warn(
1438 warnings.warn("Use BaseCommit instead of BaseChangeset", DeprecationWarning)
1423 "Use BaseCommit instead of BaseChangeset", DeprecationWarning)
1424 return super().__new__(cls, *args, **kwargs)
1439 return super().__new__(cls, *args, **kwargs)
1425
1440
1426
1441
@@ -1441,8 +1456,7 b' class BaseInMemoryCommit(object):'
1441 list of ``FileNode`` objects marked as *changed*
1456 list of ``FileNode`` objects marked as *changed*
1442
1457
1443 ``removed``
1458 ``removed``
1444 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
1459 list of ``FileNode`` objects marked to be *removed*
1445 *removed*
1446
1460
1447 ``parents``
1461 ``parents``
1448 list of :class:`BaseCommit` instances representing parents of
1462 list of :class:`BaseCommit` instances representing parents of
@@ -1469,9 +1483,7 b' class BaseInMemoryCommit(object):'
1469 # Check if not already marked as *added* first
1483 # Check if not already marked as *added* first
1470 for node in filenodes:
1484 for node in filenodes:
1471 if node.path in (n.path for n in self.added):
1485 if node.path in (n.path for n in self.added):
1472 raise NodeAlreadyAddedError(
1486 raise NodeAlreadyAddedError(f"Such FileNode {node.path} is already marked for addition")
1473 "Such FileNode %s is already marked for addition"
1474 % node.path)
1475 for node in filenodes:
1487 for node in filenodes:
1476 self.added.append(node)
1488 self.added.append(node)
1477
1489
@@ -1490,24 +1502,19 b' class BaseInMemoryCommit(object):'
1490 """
1502 """
1491 for node in filenodes:
1503 for node in filenodes:
1492 if node.path in (n.path for n in self.removed):
1504 if node.path in (n.path for n in self.removed):
1493 raise NodeAlreadyRemovedError(
1505 raise NodeAlreadyRemovedError("Node at %s is already marked as removed" % node.path)
1494 "Node at %s is already marked as removed" % node.path)
1495 try:
1506 try:
1496 self.repository.get_commit()
1507 self.repository.get_commit()
1497 except EmptyRepositoryError:
1508 except EmptyRepositoryError:
1498 raise EmptyRepositoryError(
1509 raise EmptyRepositoryError("Nothing to change - try to *add* new nodes rather than " "changing them")
1499 "Nothing to change - try to *add* new nodes rather than "
1500 "changing them")
1501 for node in filenodes:
1510 for node in filenodes:
1502 if node.path in (n.path for n in self.changed):
1511 if node.path in (n.path for n in self.changed):
1503 raise NodeAlreadyChangedError(
1512 raise NodeAlreadyChangedError("Node at '%s' is already marked as changed" % node.path)
1504 "Node at '%s' is already marked as changed" % node.path)
1505 self.changed.append(node)
1513 self.changed.append(node)
1506
1514
1507 def remove(self, *filenodes):
1515 def remove(self, *filenodes):
1508 """
1516 """
1509 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
1517 Marks given ``FileNode`` objects to be *removed* in next commit.
1510 *removed* in next commit.
1511
1518
1512 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
1519 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
1513 be *removed*
1520 be *removed*
@@ -1516,11 +1523,9 b' class BaseInMemoryCommit(object):'
1516 """
1523 """
1517 for node in filenodes:
1524 for node in filenodes:
1518 if node.path in (n.path for n in self.removed):
1525 if node.path in (n.path for n in self.removed):
1519 raise NodeAlreadyRemovedError(
1526 raise NodeAlreadyRemovedError("Node is already marked to for removal at %s" % node.path)
1520 "Node is already marked to for removal at %s" % node.path)
1521 if node.path in (n.path for n in self.changed):
1527 if node.path in (n.path for n in self.changed):
1522 raise NodeAlreadyChangedError(
1528 raise NodeAlreadyChangedError("Node is already marked to be changed at %s" % node.path)
1523 "Node is already marked to be changed at %s" % node.path)
1524 # We only mark node as *removed* - real removal is done by
1529 # We only mark node as *removed* - real removal is done by
1525 # commit method
1530 # commit method
1526 self.removed.append(node)
1531 self.removed.append(node)
@@ -1575,12 +1580,11 b' class BaseInMemoryCommit(object):'
1575 for p in parents:
1580 for p in parents:
1576 for node in self.added:
1581 for node in self.added:
1577 try:
1582 try:
1578 p.get_node(node.path)
1583 p.get_node(node.bytes_path)
1579 except NodeDoesNotExistError:
1584 except NodeDoesNotExistError:
1580 pass
1585 pass
1581 else:
1586 else:
1582 raise NodeAlreadyExistsError(
1587 raise NodeAlreadyExistsError(f"Node `{node.path}` already exists at {p}")
1583 f"Node `{node.path}` already exists at {p}")
1584
1588
1585 # Check nodes marked as changed
1589 # Check nodes marked as changed
1586 missing = set(self.changed)
1590 missing = set(self.changed)
@@ -1590,7 +1594,7 b' class BaseInMemoryCommit(object):'
1590 for p in parents:
1594 for p in parents:
1591 for node in self.changed:
1595 for node in self.changed:
1592 try:
1596 try:
1593 old = p.get_node(node.path)
1597 old = p.get_node(node.bytes_path)
1594 missing.remove(node)
1598 missing.remove(node)
1595 # if content actually changed, remove node from not_changed
1599 # if content actually changed, remove node from not_changed
1596 if old.content != node.content:
1600 if old.content != node.content:
@@ -1598,24 +1602,23 b' class BaseInMemoryCommit(object):'
1598 except NodeDoesNotExistError:
1602 except NodeDoesNotExistError:
1599 pass
1603 pass
1600 if self.changed and missing:
1604 if self.changed and missing:
1601 raise NodeDoesNotExistError(
1605 raise NodeDoesNotExistError(f"Node `{node.path}` marked as modified but missing in parents: {parents}")
1602 f"Node `{node.path}` marked as modified but missing in parents: {parents}")
1603
1606
1604 if self.changed and not_changed:
1607 if self.changed and not_changed:
1605 raise NodeNotChangedError(
1608 raise NodeNotChangedError(
1606 "Node `%s` wasn't actually changed (parents: %s)"
1609 "Node `%s` wasn't actually changed (parents: %s)" % (not_changed.pop().path, parents)
1607 % (not_changed.pop().path, parents))
1610 )
1608
1611
1609 # Check nodes marked as removed
1612 # Check nodes marked as removed
1610 if self.removed and not parents:
1613 if self.removed and not parents:
1611 raise NodeDoesNotExistError(
1614 raise NodeDoesNotExistError(
1612 "Cannot remove node at %s as there "
1615 "Cannot remove node at %s as there " "were no parents specified" % self.removed[0].path
1613 "were no parents specified" % self.removed[0].path)
1616 )
1614 really_removed = set()
1617 really_removed = set()
1615 for p in parents:
1618 for p in parents:
1616 for node in self.removed:
1619 for node in self.removed:
1617 try:
1620 try:
1618 p.get_node(node.path)
1621 p.get_node(node.bytes_path)
1619 really_removed.add(node)
1622 really_removed.add(node)
1620 except CommitError:
1623 except CommitError:
1621 pass
1624 pass
@@ -1623,8 +1626,8 b' class BaseInMemoryCommit(object):'
1623 if not_removed:
1626 if not_removed:
1624 # TODO: johbo: This code branch does not seem to be covered
1627 # TODO: johbo: This code branch does not seem to be covered
1625 raise NodeDoesNotExistError(
1628 raise NodeDoesNotExistError(
1626 "Cannot remove node at %s from "
1629 "Cannot remove node at %s from " "following parents: %s" % (not_removed, parents)
1627 "following parents: %s" % (not_removed, parents))
1630 )
1628
1631
1629 def commit(self, message, author, parents=None, branch=None, date=None, **kwargs):
1632 def commit(self, message, author, parents=None, branch=None, date=None, **kwargs):
1630 """
1633 """
@@ -1652,16 +1655,13 b' class BaseInMemoryCommit(object):'
1652
1655
1653
1656
1654 class BaseInMemoryChangesetClass(type):
1657 class BaseInMemoryChangesetClass(type):
1655
1656 def __instancecheck__(self, instance):
1658 def __instancecheck__(self, instance):
1657 return isinstance(instance, BaseInMemoryCommit)
1659 return isinstance(instance, BaseInMemoryCommit)
1658
1660
1659
1661
1660 class BaseInMemoryChangeset(BaseInMemoryCommit, metaclass=BaseInMemoryChangesetClass):
1662 class BaseInMemoryChangeset(BaseInMemoryCommit, metaclass=BaseInMemoryChangesetClass):
1661
1662 def __new__(cls, *args, **kwargs):
1663 def __new__(cls, *args, **kwargs):
1663 warnings.warn(
1664 warnings.warn("Use BaseCommit instead of BaseInMemoryCommit", DeprecationWarning)
1664 "Use BaseCommit instead of BaseInMemoryCommit", DeprecationWarning)
1665 return super().__new__(cls, *args, **kwargs)
1665 return super().__new__(cls, *args, **kwargs)
1666
1666
1667
1667
@@ -1671,9 +1671,7 b' class EmptyCommit(BaseCommit):'
1671 an EmptyCommit
1671 an EmptyCommit
1672 """
1672 """
1673
1673
1674 def __init__(
1674 def __init__(self, commit_id=EMPTY_COMMIT_ID, repo=None, alias=None, idx=-1, message="", author="", date=None):
1675 self, commit_id=EMPTY_COMMIT_ID, repo=None, alias=None, idx=-1,
1676 message='', author='', date=None):
1677 self._empty_commit_id = commit_id
1675 self._empty_commit_id = commit_id
1678 # TODO: johbo: Solve idx parameter, default value does not make
1676 # TODO: johbo: Solve idx parameter, default value does not make
1679 # too much sense
1677 # too much sense
@@ -1697,6 +1695,7 b' class EmptyCommit(BaseCommit):'
1697 def branch(self):
1695 def branch(self):
1698 if self.alias:
1696 if self.alias:
1699 from rhodecode.lib.vcs.backends import get_backend
1697 from rhodecode.lib.vcs.backends import get_backend
1698
1700 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1699 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1701
1700
1702 @LazyProperty
1701 @LazyProperty
@@ -1711,7 +1710,7 b' class EmptyCommit(BaseCommit):'
1711 return self
1710 return self
1712
1711
1713 def get_file_content(self, path) -> bytes:
1712 def get_file_content(self, path) -> bytes:
1714 return b''
1713 return b""
1715
1714
1716 def get_file_content_streamed(self, path):
1715 def get_file_content_streamed(self, path):
1717 yield self.get_file_content(path)
1716 yield self.get_file_content(path)
@@ -1721,27 +1720,29 b' class EmptyCommit(BaseCommit):'
1721
1720
1722
1721
1723 class EmptyChangesetClass(type):
1722 class EmptyChangesetClass(type):
1724
1725 def __instancecheck__(self, instance):
1723 def __instancecheck__(self, instance):
1726 return isinstance(instance, EmptyCommit)
1724 return isinstance(instance, EmptyCommit)
1727
1725
1728
1726
1729 class EmptyChangeset(EmptyCommit, metaclass=EmptyChangesetClass):
1727 class EmptyChangeset(EmptyCommit, metaclass=EmptyChangesetClass):
1730
1731 def __new__(cls, *args, **kwargs):
1728 def __new__(cls, *args, **kwargs):
1732 warnings.warn(
1729 warnings.warn("Use EmptyCommit instead of EmptyChangeset", DeprecationWarning)
1733 "Use EmptyCommit instead of EmptyChangeset", DeprecationWarning)
1734 return super(EmptyCommit, cls).__new__(cls, *args, **kwargs)
1730 return super(EmptyCommit, cls).__new__(cls, *args, **kwargs)
1735
1731
1736 def __init__(self, cs=EMPTY_COMMIT_ID, repo=None, requested_revision=None,
1732 def __init__(
1737 alias=None, revision=-1, message='', author='', date=None):
1733 self,
1734 cs=EMPTY_COMMIT_ID,
1735 repo=None,
1736 requested_revision=None,
1737 alias=None,
1738 revision=-1,
1739 message="",
1740 author="",
1741 date=None,
1742 ):
1738 if requested_revision is not None:
1743 if requested_revision is not None:
1739 warnings.warn(
1744 warnings.warn("Parameter requested_revision not supported anymore", DeprecationWarning)
1740 "Parameter requested_revision not supported anymore",
1745 super().__init__(commit_id=cs, repo=repo, alias=alias, idx=revision, message=message, author=author, date=date)
1741 DeprecationWarning)
1742 super().__init__(
1743 commit_id=cs, repo=repo, alias=alias, idx=revision,
1744 message=message, author=author, date=date)
1745
1746
1746 @property
1747 @property
1747 def revision(self):
1748 def revision(self):
@@ -1760,11 +1761,11 b' class EmptyRepository(BaseRepository):'
1760
1761
1761 def get_diff(self, *args, **kwargs):
1762 def get_diff(self, *args, **kwargs):
1762 from rhodecode.lib.vcs.backends.git.diff import GitDiff
1763 from rhodecode.lib.vcs.backends.git.diff import GitDiff
1763 return GitDiff(b'')
1764
1765 return GitDiff(b"")
1764
1766
1765
1767
1766 class CollectionGenerator(object):
1768 class CollectionGenerator(object):
1767
1768 def __init__(self, repo, commit_ids, collection_size=None, pre_load=None, translate_tag=None):
1769 def __init__(self, repo, commit_ids, collection_size=None, pre_load=None, translate_tag=None):
1769 self.repo = repo
1770 self.repo = repo
1770 self.commit_ids = commit_ids
1771 self.commit_ids = commit_ids
@@ -1786,26 +1787,22 b' class CollectionGenerator(object):'
1786 """
1787 """
1787 Allows backends to override the way commits are generated.
1788 Allows backends to override the way commits are generated.
1788 """
1789 """
1789 return self.repo.get_commit(
1790 return self.repo.get_commit(commit_id=commit_id, pre_load=self.pre_load, translate_tag=self.translate_tag)
1790 commit_id=commit_id, pre_load=self.pre_load,
1791 translate_tag=self.translate_tag)
1792
1791
1793 def __getitem__(self, key):
1792 def __getitem__(self, key):
1794 """Return either a single element by index, or a sliced collection."""
1793 """Return either a single element by index, or a sliced collection."""
1795
1794
1796 if isinstance(key, slice):
1795 if isinstance(key, slice):
1797 commit_ids = self.commit_ids[key.start:key.stop]
1796 commit_ids = self.commit_ids[key.start : key.stop]
1798
1797
1799 else:
1798 else:
1800 # single item
1799 # single item
1801 commit_ids = self.commit_ids[key]
1800 commit_ids = self.commit_ids[key]
1802
1801
1803 return self.__class__(
1802 return self.__class__(self.repo, commit_ids, pre_load=self.pre_load, translate_tag=self.translate_tag)
1804 self.repo, commit_ids, pre_load=self.pre_load,
1805 translate_tag=self.translate_tag)
1806
1803
1807 def __repr__(self):
1804 def __repr__(self):
1808 return '<CollectionGenerator[len:%s]>' % (self.__len__())
1805 return "<CollectionGenerator[len:%s]>" % (self.__len__())
1809
1806
1810
1807
1811 class Config(object):
1808 class Config(object):
@@ -1826,8 +1823,7 b' class Config(object):'
1826 return clone
1823 return clone
1827
1824
1828 def __repr__(self):
1825 def __repr__(self):
1829 return '<Config({} sections) at {}>'.format(
1826 return "<Config({} sections) at {}>".format(len(self._values), hex(id(self)))
1830 len(self._values), hex(id(self)))
1831
1827
1832 def items(self, section):
1828 def items(self, section):
1833 return self._values.get(section, {}).items()
1829 return self._values.get(section, {}).items()
@@ -1844,7 +1840,7 b' class Config(object):'
1844
1840
1845 def drop_option(self, section, option):
1841 def drop_option(self, section, option):
1846 if section not in self._values:
1842 if section not in self._values:
1847 raise ValueError(f'Section {section} does not exist')
1843 raise ValueError(f"Section {section} does not exist")
1848 del self._values[section][option]
1844 del self._values[section][option]
1849
1845
1850 def serialize(self):
1846 def serialize(self):
@@ -1855,8 +1851,7 b' class Config(object):'
1855 items = []
1851 items = []
1856 for section in self._values:
1852 for section in self._values:
1857 for option, value in self._values[section].items():
1853 for option, value in self._values[section].items():
1858 items.append(
1854 items.append((safe_str(section), safe_str(option), safe_str(value)))
1859 (safe_str(section), safe_str(option), safe_str(value)))
1860 return items
1855 return items
1861
1856
1862
1857
@@ -1867,12 +1862,13 b' class Diff(object):'
1867 Subclasses have to provide a backend specific value for
1862 Subclasses have to provide a backend specific value for
1868 :attr:`_header_re` and :attr:`_meta_re`.
1863 :attr:`_header_re` and :attr:`_meta_re`.
1869 """
1864 """
1865
1870 _meta_re = None
1866 _meta_re = None
1871 _header_re: bytes = re.compile(br"")
1867 _header_re: bytes = re.compile(rb"")
1872
1868
1873 def __init__(self, raw_diff: bytes):
1869 def __init__(self, raw_diff: bytes):
1874 if not isinstance(raw_diff, bytes):
1870 if not isinstance(raw_diff, bytes):
1875 raise Exception(f'raw_diff must be bytes - got {type(raw_diff)}')
1871 raise Exception(f"raw_diff must be bytes - got {type(raw_diff)}")
1876
1872
1877 self.raw = memoryview(raw_diff)
1873 self.raw = memoryview(raw_diff)
1878
1874
@@ -1886,7 +1882,7 b' class Diff(object):'
1886 we can detect last chunk as this was also has special rule
1882 we can detect last chunk as this was also has special rule
1887 """
1883 """
1888
1884
1889 diff_parts = (b'\n' + bytes(self.raw)).split(b'\ndiff --git')
1885 diff_parts = (b"\n" + bytes(self.raw)).split(b"\ndiff --git")
1890
1886
1891 chunks = diff_parts[1:]
1887 chunks = diff_parts[1:]
1892 total_chunks = len(chunks)
1888 total_chunks = len(chunks)
@@ -1894,44 +1890,46 b' class Diff(object):'
1894 def diff_iter(_chunks):
1890 def diff_iter(_chunks):
1895 for cur_chunk, chunk in enumerate(_chunks, start=1):
1891 for cur_chunk, chunk in enumerate(_chunks, start=1):
1896 yield DiffChunk(chunk, self, cur_chunk == total_chunks)
1892 yield DiffChunk(chunk, self, cur_chunk == total_chunks)
1893
1897 return diff_iter(chunks)
1894 return diff_iter(chunks)
1898
1895
1899
1896
1900 class DiffChunk(object):
1897 class DiffChunk(object):
1901
1902 def __init__(self, chunk: bytes, diff_obj: Diff, is_last_chunk: bool):
1898 def __init__(self, chunk: bytes, diff_obj: Diff, is_last_chunk: bool):
1903 self.diff_obj = diff_obj
1899 self.diff_obj = diff_obj
1904
1900
1905 # since we split by \ndiff --git that part is lost from original diff
1901 # since we split by \ndiff --git that part is lost from original diff
1906 # we need to re-apply it at the end, EXCEPT ! if it's last chunk
1902 # we need to re-apply it at the end, EXCEPT ! if it's last chunk
1907 if not is_last_chunk:
1903 if not is_last_chunk:
1908 chunk += b'\n'
1904 chunk += b"\n"
1909 header_re = self.diff_obj.get_header_re()
1905 header_re = self.diff_obj.get_header_re()
1906
1910 match = header_re.match(chunk)
1907 match = header_re.match(chunk)
1911 self.header = match.groupdict()
1908 self.header = match.groupdict()
1912 self.diff = chunk[match.end():]
1909 self.diff = chunk[match.end() :]
1913 self.raw = chunk
1910 self.raw = chunk
1914
1911
1915 @property
1912 @property
1916 def header_as_str(self):
1913 def header_as_str(self):
1917 if self.header:
1914 if self.header:
1915
1918 def safe_str_on_bytes(val):
1916 def safe_str_on_bytes(val):
1919 if isinstance(val, bytes):
1917 if isinstance(val, bytes):
1920 return safe_str(val)
1918 return safe_str(val)
1921 return val
1919 return val
1920
1922 return {safe_str(k): safe_str_on_bytes(v) for k, v in self.header.items()}
1921 return {safe_str(k): safe_str_on_bytes(v) for k, v in self.header.items()}
1923
1922
1924 def __repr__(self):
1923 def __repr__(self):
1925 return f'DiffChunk({self.header_as_str})'
1924 return f"DiffChunk({self.header_as_str})"
1926
1925
1927
1926
1928 class BasePathPermissionChecker(object):
1927 class BasePathPermissionChecker(object):
1929
1930 @staticmethod
1928 @staticmethod
1931 def create_from_patterns(includes, excludes):
1929 def create_from_patterns(includes, excludes):
1932 if includes and '*' in includes and not excludes:
1930 if includes and "*" in includes and not excludes:
1933 return AllPathPermissionChecker()
1931 return AllPathPermissionChecker()
1934 elif excludes and '*' in excludes:
1932 elif excludes and "*" in excludes:
1935 return NonePathPermissionChecker()
1933 return NonePathPermissionChecker()
1936 else:
1934 else:
1937 return PatternPathPermissionChecker(includes, excludes)
1935 return PatternPathPermissionChecker(includes, excludes)
@@ -1945,7 +1943,6 b' class BasePathPermissionChecker(object):'
1945
1943
1946
1944
1947 class AllPathPermissionChecker(BasePathPermissionChecker):
1945 class AllPathPermissionChecker(BasePathPermissionChecker):
1948
1949 @property
1946 @property
1950 def has_full_access(self):
1947 def has_full_access(self):
1951 return True
1948 return True
@@ -1955,7 +1952,6 b' class AllPathPermissionChecker(BasePathP'
1955
1952
1956
1953
1957 class NonePathPermissionChecker(BasePathPermissionChecker):
1954 class NonePathPermissionChecker(BasePathPermissionChecker):
1958
1959 @property
1955 @property
1960 def has_full_access(self):
1956 def has_full_access(self):
1961 return False
1957 return False
@@ -1965,18 +1961,15 b' class NonePathPermissionChecker(BasePath'
1965
1961
1966
1962
1967 class PatternPathPermissionChecker(BasePathPermissionChecker):
1963 class PatternPathPermissionChecker(BasePathPermissionChecker):
1968
1969 def __init__(self, includes, excludes):
1964 def __init__(self, includes, excludes):
1970 self.includes = includes
1965 self.includes = includes
1971 self.excludes = excludes
1966 self.excludes = excludes
1972 self.includes_re = [] if not includes else [
1967 self.includes_re = [] if not includes else [re.compile(fnmatch.translate(pattern)) for pattern in includes]
1973 re.compile(fnmatch.translate(pattern)) for pattern in includes]
1968 self.excludes_re = [] if not excludes else [re.compile(fnmatch.translate(pattern)) for pattern in excludes]
1974 self.excludes_re = [] if not excludes else [
1975 re.compile(fnmatch.translate(pattern)) for pattern in excludes]
1976
1969
1977 @property
1970 @property
1978 def has_full_access(self):
1971 def has_full_access(self):
1979 return '*' in self.includes and not self.excludes
1972 return "*" in self.includes and not self.excludes
1980
1973
1981 def has_access(self, path):
1974 def has_access(self, path):
1982 for regex in self.excludes_re:
1975 for regex in self.excludes_re:
@@ -21,8 +21,8 b' GIT commit module'
21 """
21 """
22
22
23 import io
23 import io
24 import stat
25 import configparser
24 import configparser
25 import logging
26 from itertools import chain
26 from itertools import chain
27
27
28 from zope.cachedescriptors.property import Lazy as LazyProperty
28 from zope.cachedescriptors.property import Lazy as LazyProperty
@@ -32,9 +32,16 b' from rhodecode.lib.str_utils import safe'
32 from rhodecode.lib.vcs.backends import base
32 from rhodecode.lib.vcs.backends import base
33 from rhodecode.lib.vcs.exceptions import CommitError, NodeDoesNotExistError
33 from rhodecode.lib.vcs.exceptions import CommitError, NodeDoesNotExistError
34 from rhodecode.lib.vcs.nodes import (
34 from rhodecode.lib.vcs.nodes import (
35 FileNode, DirNode, NodeKind, RootNode, SubModuleNode,
35 FileNode,
36 ChangedFileNodesGenerator, AddedFileNodesGenerator,
36 DirNode,
37 RemovedFileNodesGenerator, LargeFileNode)
37 NodeKind,
38 RootNode,
39 SubModuleNode,
40 LargeFileNode,
41 )
42 from rhodecode.lib.vcs_common import FILEMODE_LINK
43
44 log = logging.getLogger(__name__)
38
45
39
46
40 class GitCommit(base.BaseCommit):
47 class GitCommit(base.BaseCommit):
@@ -50,13 +57,11 b' class GitCommit(base.BaseCommit):'
50 # done through a more complex tree walk on parents
57 # done through a more complex tree walk on parents
51 "status",
58 "status",
52 # mercurial specific property not supported here
59 # mercurial specific property not supported here
53 "_file_paths",
60 "obsolete",
54 # mercurial specific property not supported here
55 'obsolete',
56 # mercurial specific property not supported here
61 # mercurial specific property not supported here
57 'phase',
62 "phase",
58 # mercurial specific property not supported here
63 # mercurial specific property not supported here
59 'hidden'
64 "hidden",
60 ]
65 ]
61
66
62 def __init__(self, repository, raw_id, idx, pre_load=None):
67 def __init__(self, repository, raw_id, idx, pre_load=None):
@@ -69,17 +74,16 b' class GitCommit(base.BaseCommit):'
69 self._set_bulk_properties(pre_load)
74 self._set_bulk_properties(pre_load)
70
75
71 # caches
76 # caches
72 self._stat_modes = {} # stat info for paths
73 self._paths = {} # path processed with parse_tree
74 self.nodes = {}
77 self.nodes = {}
78 self._path_mode_cache = {} # path stats cache, e.g filemode etc
79 self._path_type_cache = {} # path type dir/file/link etc cache
80
75 self._submodules = None
81 self._submodules = None
76
82
77 def _set_bulk_properties(self, pre_load):
83 def _set_bulk_properties(self, pre_load):
78
79 if not pre_load:
84 if not pre_load:
80 return
85 return
81 pre_load = [entry for entry in pre_load
86 pre_load = [entry for entry in pre_load if entry not in self._filter_pre_load]
82 if entry not in self._filter_pre_load]
83 if not pre_load:
87 if not pre_load:
84 return
88 return
85
89
@@ -102,7 +106,7 b' class GitCommit(base.BaseCommit):'
102
106
103 @LazyProperty
107 @LazyProperty
104 def _tree_id(self):
108 def _tree_id(self):
105 return self._remote[self._commit['tree']]['id']
109 return self._remote[self._commit["tree"]]["id"]
106
110
107 @LazyProperty
111 @LazyProperty
108 def id(self):
112 def id(self):
@@ -134,13 +138,12 b' class GitCommit(base.BaseCommit):'
134 """
138 """
135 Returns modified, added, removed, deleted files for current commit
139 Returns modified, added, removed, deleted files for current commit
136 """
140 """
137 return self.changed, self.added, self.removed
141 added, modified, deleted = self._changes_cache
142 return list(modified), list(modified), list(deleted)
138
143
139 @LazyProperty
144 @LazyProperty
140 def tags(self):
145 def tags(self):
141 tags = [safe_str(name) for name,
146 tags = [safe_str(name) for name, commit_id in self.repository.tags.items() if commit_id == self.raw_id]
142 commit_id in self.repository.tags.items()
143 if commit_id == self.raw_id]
144 return tags
147 return tags
145
148
146 @LazyProperty
149 @LazyProperty
@@ -161,47 +164,33 b' class GitCommit(base.BaseCommit):'
161 branches = self._remote.branch(self.raw_id)
164 branches = self._remote.branch(self.raw_id)
162 return self._set_branch(branches)
165 return self._set_branch(branches)
163
166
164 def _get_tree_id_for_path(self, path):
167 def _get_path_tree_id_and_type(self, path: bytes):
165
168
166 path = safe_str(path)
169 if path in self._path_type_cache:
167 if path in self._paths:
170 return self._path_type_cache[path]
168 return self._paths[path]
169
170 tree_id = self._tree_id
171
171
172 path = path.strip('/')
172 if path == b"":
173 if path == '':
173 self._path_type_cache[b""] = [self._tree_id, NodeKind.DIR]
174 data = [tree_id, "tree"]
174 return self._path_type_cache[path]
175 self._paths[''] = data
176 return data
177
175
178 tree_id, tree_type, tree_mode = \
176 tree_id, tree_type, tree_mode = self._remote.tree_and_type_for_path(self.raw_id, path)
179 self._remote.tree_and_type_for_path(self.raw_id, path)
180 if tree_id is None:
177 if tree_id is None:
181 raise self.no_node_at_path(path)
178 raise self.no_node_at_path(path)
182
179
183 self._paths[path] = [tree_id, tree_type]
180 self._path_type_cache[path] = [tree_id, tree_type]
184 self._stat_modes[path] = tree_mode
181 self._path_mode_cache[path] = tree_mode
185
182
186 if path not in self._paths:
183 return self._path_type_cache[path]
187 raise self.no_node_at_path(path)
188
189 return self._paths[path]
190
184
191 def _get_kind(self, path):
185 def _get_kind(self, path):
192 tree_id, type_ = self._get_tree_id_for_path(path)
186 path = self._fix_path(path)
193 if type_ == 'blob':
187 _, path_type = self._get_path_tree_id_and_type(path)
194 return NodeKind.FILE
188 return path_type
195 elif type_ == 'tree':
196 return NodeKind.DIR
197 elif type_ == 'link':
198 return NodeKind.SUBMODULE
199 return None
200
189
201 def _assert_is_path(self, path):
190 def _assert_is_path(self, path):
202 path = self._fix_path(path)
191 path = self._fix_path(path)
203 if self._get_kind(path) != NodeKind.FILE:
192 if self._get_kind(path) != NodeKind.FILE:
204 raise CommitError(f"File does not exist for commit {self.raw_id} at '{path}'")
193 raise CommitError(f"File at path={path} does not exist for commit {self.raw_id}")
205 return path
194 return path
206
195
207 def _get_file_nodes(self):
196 def _get_file_nodes(self):
@@ -237,15 +226,19 b' class GitCommit(base.BaseCommit):'
237 path = self._assert_is_path(path)
226 path = self._assert_is_path(path)
238
227
239 # ensure path is traversed
228 # ensure path is traversed
240 self._get_tree_id_for_path(path)
229 self._get_path_tree_id_and_type(path)
230
231 return self._path_mode_cache[path]
241
232
242 return self._stat_modes[path]
233 def is_link(self, path: bytes):
234 path = self._assert_is_path(path)
235 if path not in self._path_mode_cache:
236 self._path_mode_cache[path] = self._remote.fctx_flags(self.raw_id, path)
243
237
244 def is_link(self, path):
238 return self._path_mode_cache[path] == FILEMODE_LINK
245 return stat.S_ISLNK(self.get_file_mode(path))
246
239
247 def is_node_binary(self, path):
240 def is_node_binary(self, path):
248 tree_id, _ = self._get_tree_id_for_path(path)
241 tree_id, _ = self._get_path_tree_id_and_type(path)
249 return self._remote.is_binary(tree_id)
242 return self._remote.is_binary(tree_id)
250
243
251 def node_md5_hash(self, path):
244 def node_md5_hash(self, path):
@@ -256,19 +249,19 b' class GitCommit(base.BaseCommit):'
256 """
249 """
257 Returns content of the file at given `path`.
250 Returns content of the file at given `path`.
258 """
251 """
259 tree_id, _ = self._get_tree_id_for_path(path)
252 tree_id, _ = self._get_path_tree_id_and_type(path)
260 return self._remote.blob_as_pretty_string(tree_id)
253 return self._remote.blob_as_pretty_string(tree_id)
261
254
262 def get_file_content_streamed(self, path):
255 def get_file_content_streamed(self, path):
263 tree_id, _ = self._get_tree_id_for_path(path)
256 tree_id, _ = self._get_path_tree_id_and_type(path)
264 stream_method = getattr(self._remote, 'stream:blob_as_pretty_string')
257 stream_method = getattr(self._remote, "stream:blob_as_pretty_string")
265 return stream_method(tree_id)
258 return stream_method(tree_id)
266
259
267 def get_file_size(self, path):
260 def get_file_size(self, path):
268 """
261 """
269 Returns size of the file at given `path`.
262 Returns size of the file at given `path`.
270 """
263 """
271 tree_id, _ = self._get_tree_id_for_path(path)
264 tree_id, _ = self._get_path_tree_id_and_type(path)
272 return self._remote.blob_raw_length(tree_id)
265 return self._remote.blob_raw_length(tree_id)
273
266
274 def get_path_history(self, path, limit=None, pre_load=None):
267 def get_path_history(self, path, limit=None, pre_load=None):
@@ -276,12 +269,9 b' class GitCommit(base.BaseCommit):'
276 Returns history of file as reversed list of `GitCommit` objects for
269 Returns history of file as reversed list of `GitCommit` objects for
277 which file at given `path` has been modified.
270 which file at given `path` has been modified.
278 """
271 """
279
280 path = self._assert_is_path(path)
272 path = self._assert_is_path(path)
281 hist = self._remote.node_history(self.raw_id, path, limit)
273 history = self._remote.node_history(self.raw_id, path, limit)
282 return [
274 return [self.repository.get_commit(commit_id=commit_id, pre_load=pre_load) for commit_id in history]
283 self.repository.get_commit(commit_id=commit_id, pre_load=pre_load)
284 for commit_id in hist]
285
275
286 def get_file_annotate(self, path, pre_load=None):
276 def get_file_annotate(self, path, pre_load=None):
287 """
277 """
@@ -293,95 +283,105 b' class GitCommit(base.BaseCommit):'
293
283
294 for ln_no, commit_id, content in result:
284 for ln_no, commit_id, content in result:
295 yield (
285 yield (
296 ln_no, commit_id,
286 ln_no,
287 commit_id,
297 lambda: self.repository.get_commit(commit_id=commit_id, pre_load=pre_load),
288 lambda: self.repository.get_commit(commit_id=commit_id, pre_load=pre_load),
298 content)
289 content,
290 )
299
291
300 def get_nodes(self, path, pre_load=None):
292 def get_nodes(self, path: bytes, pre_load=None):
301
293
302 if self._get_kind(path) != NodeKind.DIR:
294 if self._get_kind(path) != NodeKind.DIR:
303 raise CommitError(
295 raise CommitError(f"Directory does not exist for commit {self.raw_id} at '{path}'")
304 f"Directory does not exist for commit {self.raw_id} at '{path}'")
305 path = self._fix_path(path)
296 path = self._fix_path(path)
306
297
307 tree_id, _ = self._get_tree_id_for_path(path)
298 # call and check tree_id for this path
308
299 tree_id, _ = self._get_path_tree_id_and_type(path)
309 dirnodes = []
310 filenodes = []
311
300
312 # extracted tree ID gives us our files...
301 path_nodes = []
313 str_path = safe_str(path) # libgit operates on bytes
302
314 for name, stat_, id_, type_ in self._remote.tree_items(tree_id):
303 for bytes_name, stat_, tree_item_id, node_kind in self._remote.tree_items(tree_id):
315 if type_ == 'link':
304 if node_kind is None:
316 url = self._get_submodule_url('/'.join((str_path, name)))
305 raise CommitError(f"Requested object type={node_kind} cannot be determined")
317 dirnodes.append(SubModuleNode(
318 name, url=url, commit=id_, alias=self.repository.alias))
319 continue
320
306
321 if str_path != '':
307 if path != b"":
322 obj_path = '/'.join((str_path, name))
308 obj_path = b"/".join((path, bytes_name))
323 else:
309 else:
324 obj_path = name
310 obj_path = bytes_name
325 if obj_path not in self._stat_modes:
326 self._stat_modes[obj_path] = stat_
327
311
328 if type_ == 'tree':
312 # cache file mode for git, since we have it already
329 dirnodes.append(DirNode(safe_bytes(obj_path), commit=self))
313 if obj_path not in self._path_mode_cache:
330 elif type_ == 'blob':
314 self._path_mode_cache[obj_path] = stat_
331 filenodes.append(FileNode(safe_bytes(obj_path), commit=self, mode=stat_, pre_load=pre_load))
315
332 else:
316 # cache type
333 raise CommitError(f"Requested object should be Tree or Blob, is {type_}")
317 if node_kind not in self._path_type_cache:
318 self._path_type_cache[obj_path] = [tree_item_id, node_kind]
334
319
335 nodes = dirnodes + filenodes
320 entry = None
336 for node in nodes:
321 if obj_path in self.nodes:
337 if node.path not in self.nodes:
322 entry = self.nodes[obj_path]
338 self.nodes[node.path] = node
323 else:
339 nodes.sort()
324 if node_kind == NodeKind.SUBMODULE:
340 return nodes
325 url = self._get_submodule_url(b"/".join((path, bytes_name)))
326 entry= SubModuleNode(bytes_name, url=url, commit=tree_item_id, alias=self.repository.alias)
327 elif node_kind == NodeKind.DIR:
328 entry = DirNode(safe_bytes(obj_path), commit=self)
329 elif node_kind == NodeKind.FILE:
330 entry = FileNode(safe_bytes(obj_path), commit=self, mode=stat_, pre_load=pre_load)
341
331
342 def get_node(self, path, pre_load=None):
332 if entry:
333 self.nodes[obj_path] = entry
334 path_nodes.append(entry)
335
336 path_nodes.sort()
337 return path_nodes
338
339 def get_node(self, path: bytes, pre_load=None):
343 path = self._fix_path(path)
340 path = self._fix_path(path)
344 if path not in self.nodes:
341
345 try:
342 # use cached, if we have one
346 tree_id, type_ = self._get_tree_id_for_path(path)
343 if path in self.nodes:
347 except CommitError:
344 return self.nodes[path]
348 raise NodeDoesNotExistError(
349 f"Cannot find one of parents' directories for a given "
350 f"path: {path}")
351
345
352 if type_ in ['link', 'commit']:
346 try:
347 tree_id, path_type = self._get_path_tree_id_and_type(path)
348 except CommitError:
349 raise NodeDoesNotExistError(f"Cannot find one of parents' directories for a given path: {path}")
350
351 if path == b"":
352 node = RootNode(commit=self)
353 else:
354 if path_type == NodeKind.SUBMODULE:
353 url = self._get_submodule_url(path)
355 url = self._get_submodule_url(path)
354 node = SubModuleNode(path, url=url, commit=tree_id,
356 node = SubModuleNode(path, url=url, commit=tree_id, alias=self.repository.alias)
355 alias=self.repository.alias)
357 elif path_type == NodeKind.DIR:
356 elif type_ == 'tree':
358 node = DirNode(safe_bytes(path), commit=self)
357 if path == '':
359 elif path_type == NodeKind.FILE:
358 node = RootNode(commit=self)
359 else:
360 node = DirNode(safe_bytes(path), commit=self)
361 elif type_ == 'blob':
362 node = FileNode(safe_bytes(path), commit=self, pre_load=pre_load)
360 node = FileNode(safe_bytes(path), commit=self, pre_load=pre_load)
363 self._stat_modes[path] = node.mode
361 self._path_mode_cache[path] = node.mode
364 else:
362 else:
365 raise self.no_node_at_path(path)
363 raise self.no_node_at_path(path)
366
364
367 # cache node
365 # cache node
368 self.nodes[path] = node
366 self.nodes[path] = node
369
370 return self.nodes[path]
367 return self.nodes[path]
371
368
372 def get_largefile_node(self, path):
369 def get_largefile_node(self, path: bytes):
373 tree_id, _ = self._get_tree_id_for_path(path)
370 tree_id, _ = self._get_path_tree_id_and_type(path)
374 pointer_spec = self._remote.is_large_file(tree_id)
371 pointer_spec = self._remote.is_large_file(tree_id)
375
372
376 if pointer_spec:
373 if pointer_spec:
377 # content of that file regular FileNode is the hash of largefile
374 # content of that file regular FileNode is the hash of largefile
378 file_id = pointer_spec.get('oid_hash')
375 file_id = pointer_spec.get("oid_hash")
379 if self._remote.in_largefiles_store(file_id):
376 if not self._remote.in_largefiles_store(file_id):
380 lf_path = self._remote.store_path(file_id)
377 log.warning(f'Largefile oid={file_id} not found in store')
381 return LargeFileNode(safe_bytes(lf_path), commit=self, org_path=path)
378 return None
379
380 lf_path = self._remote.store_path(file_id)
381 return LargeFileNode(safe_bytes(lf_path), commit=self, org_path=path)
382
382
383 @LazyProperty
383 @LazyProperty
384 def affected_files(self):
384 def affected_files(self) -> list[bytes]:
385 """
385 """
386 Gets a fast accessible file changes for given commit
386 Gets a fast accessible file changes for given commit
387 """
387 """
@@ -389,7 +389,7 b' class GitCommit(base.BaseCommit):'
389 return list(added.union(modified).union(deleted))
389 return list(added.union(modified).union(deleted))
390
390
391 @LazyProperty
391 @LazyProperty
392 def _changes_cache(self):
392 def _changes_cache(self) -> tuple[set, set, set]:
393 added = set()
393 added = set()
394 modified = set()
394 modified = set()
395 deleted = set()
395 deleted = set()
@@ -416,53 +416,22 b' class GitCommit(base.BaseCommit):'
416 :param status: one of: *added*, *modified* or *deleted*
416 :param status: one of: *added*, *modified* or *deleted*
417 """
417 """
418 added, modified, deleted = self._changes_cache
418 added, modified, deleted = self._changes_cache
419 return sorted({
419 return sorted({"added": list(added), "modified": list(modified), "deleted": list(deleted)}[status])
420 'added': list(added),
421 'modified': list(modified),
422 'deleted': list(deleted)}[status]
423 )
424
425 @LazyProperty
426 def added(self):
427 """
428 Returns list of added ``FileNode`` objects.
429 """
430 if not self.parents:
431 return list(self._get_file_nodes())
432 return AddedFileNodesGenerator(self.added_paths, self)
433
420
434 @LazyProperty
421 @LazyProperty
435 def added_paths(self):
422 def added_paths(self):
436 return [n for n in self._get_paths_for_status('added')]
423 return [n for n in self._get_paths_for_status("added")]
437
438 @LazyProperty
439 def changed(self):
440 """
441 Returns list of modified ``FileNode`` objects.
442 """
443 if not self.parents:
444 return []
445 return ChangedFileNodesGenerator(self.changed_paths, self)
446
424
447 @LazyProperty
425 @LazyProperty
448 def changed_paths(self):
426 def changed_paths(self):
449 return [n for n in self._get_paths_for_status('modified')]
427 return [n for n in self._get_paths_for_status("modified")]
450
451 @LazyProperty
452 def removed(self):
453 """
454 Returns list of removed ``FileNode`` objects.
455 """
456 if not self.parents:
457 return []
458 return RemovedFileNodesGenerator(self.removed_paths, self)
459
428
460 @LazyProperty
429 @LazyProperty
461 def removed_paths(self):
430 def removed_paths(self):
462 return [n for n in self._get_paths_for_status('deleted')]
431 return [n for n in self._get_paths_for_status("deleted")]
463
432
464 def _get_submodule_url(self, submodule_path):
433 def _get_submodule_url(self, submodule_path: bytes):
465 git_modules_path = '.gitmodules'
434 git_modules_path = b".gitmodules"
466
435
467 if self._submodules is None:
436 if self._submodules is None:
468 self._submodules = {}
437 self._submodules = {}
@@ -476,9 +445,9 b' class GitCommit(base.BaseCommit):'
476 parser.read_file(io.StringIO(submodules_node.str_content))
445 parser.read_file(io.StringIO(submodules_node.str_content))
477
446
478 for section in parser.sections():
447 for section in parser.sections():
479 path = parser.get(section, 'path')
448 path = parser.get(section, "path")
480 url = parser.get(section, 'url')
449 url = parser.get(section, "url")
481 if path and url:
450 if path and url:
482 self._submodules[path.strip('/')] = url
451 self._submodules[safe_bytes(path).strip(b"/")] = url
483
452
484 return self._submodules.get(submodule_path.strip('/'))
453 return self._submodules.get(submodule_path.strip(b"/"))
@@ -425,7 +425,7 b' class GitRepository(BaseRepository):'
425 return
425 return
426
426
427 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None,
427 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None,
428 translate_tag=True, maybe_unreachable=False, reference_obj=None):
428 translate_tag=True, maybe_unreachable=False, reference_obj=None) -> GitCommit:
429 """
429 """
430 Returns `GitCommit` object representing commit from git repository
430 Returns `GitCommit` object representing commit from git repository
431 at the given `commit_id` or head (most recent commit) if None given.
431 at the given `commit_id` or head (most recent commit) if None given.
@@ -20,20 +20,25 b''
20 HG commit module
20 HG commit module
21 """
21 """
22
22
23 import os
23 import logging
24
24
25 from zope.cachedescriptors.property import Lazy as LazyProperty
25 from zope.cachedescriptors.property import Lazy as LazyProperty
26
26
27 from rhodecode.lib.datelib import utcdate_fromtimestamp
27 from rhodecode.lib.datelib import utcdate_fromtimestamp
28 from rhodecode.lib.str_utils import safe_bytes, safe_str
28 from rhodecode.lib.str_utils import safe_bytes, safe_str
29 from rhodecode.lib.vcs import path as vcspath
30 from rhodecode.lib.vcs.backends import base
29 from rhodecode.lib.vcs.backends import base
31 from rhodecode.lib.vcs.exceptions import CommitError
30 from rhodecode.lib.vcs.exceptions import CommitError
32 from rhodecode.lib.vcs.nodes import (
31 from rhodecode.lib.vcs.nodes import (
33 AddedFileNodesGenerator, ChangedFileNodesGenerator, DirNode, FileNode,
32 DirNode,
34 NodeKind, RemovedFileNodesGenerator, RootNode, SubModuleNode,
33 FileNode,
35 LargeFileNode)
34 NodeKind,
36 from rhodecode.lib.vcs.utils.paths import get_dirs_for_path
35 RootNode,
36 SubModuleNode,
37 LargeFileNode,
38 )
39 from rhodecode.lib.vcs_common import FILEMODE_LINK
40
41 log = logging.getLogger(__name__)
37
42
38
43
39 class MercurialCommit(base.BaseCommit):
44 class MercurialCommit(base.BaseCommit):
@@ -59,13 +64,13 b' class MercurialCommit(base.BaseCommit):'
59
64
60 # caches
65 # caches
61 self.nodes = {}
66 self.nodes = {}
62 self._stat_modes = {} # stat info for paths
67 self._path_mode_cache = {} # path stats cache, e.g filemode etc
68 self._path_type_cache = {} # path type dir/file/link etc cache
63
69
64 def _set_bulk_properties(self, pre_load):
70 def _set_bulk_properties(self, pre_load):
65 if not pre_load:
71 if not pre_load:
66 return
72 return
67 pre_load = [entry for entry in pre_load
73 pre_load = [entry for entry in pre_load if entry not in self._filter_pre_load]
68 if entry not in self._filter_pre_load]
69 if not pre_load:
74 if not pre_load:
70 return
75 return
71
76
@@ -86,8 +91,7 b' class MercurialCommit(base.BaseCommit):'
86
91
87 @LazyProperty
92 @LazyProperty
88 def tags(self):
93 def tags(self):
89 tags = [name for name, commit_id in self.repository.tags.items()
94 tags = [name for name, commit_id in self.repository.tags.items() if commit_id == self.raw_id]
90 if commit_id == self.raw_id]
91 return tags
95 return tags
92
96
93 @LazyProperty
97 @LazyProperty
@@ -96,9 +100,7 b' class MercurialCommit(base.BaseCommit):'
96
100
97 @LazyProperty
101 @LazyProperty
98 def bookmarks(self):
102 def bookmarks(self):
99 bookmarks = [
103 bookmarks = [name for name, commit_id in self.repository.bookmarks.items() if commit_id == self.raw_id]
100 name for name, commit_id in self.repository.bookmarks.items()
101 if commit_id == self.raw_id]
102 return bookmarks
104 return bookmarks
103
105
104 @LazyProperty
106 @LazyProperty
@@ -122,27 +124,13 b' class MercurialCommit(base.BaseCommit):'
122 """
124 """
123 Returns modified, added, removed, deleted files for current commit
125 Returns modified, added, removed, deleted files for current commit
124 """
126 """
125 return self._remote.ctx_status(self.raw_id)
127 modified, added, deleted, *_ = self._remote.ctx_status(self.raw_id)
126
128 return modified, added, deleted
127 @LazyProperty
128 def _file_paths(self):
129 return self._remote.ctx_list(self.raw_id)
130
131 @LazyProperty
132 def _dir_paths(self):
133 dir_paths = ['']
134 dir_paths.extend(list(set(get_dirs_for_path(*self._file_paths))))
135
136 return dir_paths
137
138 @LazyProperty
139 def _paths(self):
140 return self._dir_paths + self._file_paths
141
129
142 @LazyProperty
130 @LazyProperty
143 def id(self):
131 def id(self):
144 if self.last:
132 if self.last:
145 return 'tip'
133 return "tip"
146 return self.short_id
134 return self.short_id
147
135
148 @LazyProperty
136 @LazyProperty
@@ -150,8 +138,7 b' class MercurialCommit(base.BaseCommit):'
150 return self.raw_id[:12]
138 return self.raw_id[:12]
151
139
152 def _make_commits(self, commit_ids, pre_load=None):
140 def _make_commits(self, commit_ids, pre_load=None):
153 return [self.repository.get_commit(commit_id=commit_id, pre_load=pre_load)
141 return [self.repository.get_commit(commit_id=commit_id, pre_load=pre_load) for commit_id in commit_ids]
154 for commit_id in commit_ids]
155
142
156 @LazyProperty
143 @LazyProperty
157 def parents(self):
144 def parents(self):
@@ -163,10 +150,10 b' class MercurialCommit(base.BaseCommit):'
163
150
164 def _get_phase_text(self, phase_id):
151 def _get_phase_text(self, phase_id):
165 return {
152 return {
166 0: 'public',
153 0: "public",
167 1: 'draft',
154 1: "draft",
168 2: 'secret',
155 2: "secret",
169 }.get(phase_id) or ''
156 }.get(phase_id) or ""
170
157
171 @LazyProperty
158 @LazyProperty
172 def phase(self):
159 def phase(self):
@@ -195,17 +182,14 b' class MercurialCommit(base.BaseCommit):'
195
182
196 def _get_kind(self, path):
183 def _get_kind(self, path):
197 path = self._fix_path(path)
184 path = self._fix_path(path)
198 if path in self._file_paths:
185 path_type = self._get_path_type(path)
199 return NodeKind.FILE
186 return path_type
200 elif path in self._dir_paths:
201 return NodeKind.DIR
202 else:
203 raise CommitError(f"Node does not exist at the given path '{path}'")
204
187
205 def _assert_is_path(self, path) -> str:
188 def _assert_is_path(self, path) -> str | bytes:
206 path = self._fix_path(path)
189 path = self._fix_path(path)
190
207 if self._get_kind(path) != NodeKind.FILE:
191 if self._get_kind(path) != NodeKind.FILE:
208 raise CommitError(f"File does not exist for commit {self.raw_id} at '{path}'")
192 raise CommitError(f"File at path={path} does not exist for commit {self.raw_id}")
209
193
210 return path
194 return path
211
195
@@ -214,20 +198,17 b' class MercurialCommit(base.BaseCommit):'
214 Returns stat mode of the file at the given ``path``.
198 Returns stat mode of the file at the given ``path``.
215 """
199 """
216 path = self._assert_is_path(path)
200 path = self._assert_is_path(path)
201 if path not in self._path_mode_cache:
202 self._path_mode_cache[path] = self._remote.fctx_flags(self.raw_id, path)
217
203
218 if path not in self._stat_modes:
204 return self._path_mode_cache[path]
219 self._stat_modes[path] = self._remote.fctx_flags(self.raw_id, path)
220
205
221 if 'x' in self._stat_modes[path]:
206 def is_link(self, path: bytes):
222 return base.FILEMODE_EXECUTABLE
207 path = self._assert_is_path(path)
223 return base.FILEMODE_DEFAULT
208 if path not in self._path_mode_cache:
209 self._path_mode_cache[path] = self._remote.fctx_flags(self.raw_id, path)
224
210
225 def is_link(self, path):
211 return self._path_mode_cache[path] == FILEMODE_LINK
226 path = self._assert_is_path(path)
227 if path not in self._stat_modes:
228 self._stat_modes[path] = self._remote.fctx_flags(self.raw_id, path)
229
230 return 'l' in self._stat_modes[path]
231
212
232 def is_node_binary(self, path):
213 def is_node_binary(self, path):
233 path = self._assert_is_path(path)
214 path = self._assert_is_path(path)
@@ -246,7 +227,7 b' class MercurialCommit(base.BaseCommit):'
246
227
247 def get_file_content_streamed(self, path):
228 def get_file_content_streamed(self, path):
248 path = self._assert_is_path(path)
229 path = self._assert_is_path(path)
249 stream_method = getattr(self._remote, 'stream:fctx_node_data')
230 stream_method = getattr(self._remote, "stream:fctx_node_data")
250 return stream_method(self.raw_id, path)
231 return stream_method(self.raw_id, path)
251
232
252 def get_file_size(self, path):
233 def get_file_size(self, path):
@@ -262,10 +243,8 b' class MercurialCommit(base.BaseCommit):'
262 for which file at given ``path`` has been modified.
243 for which file at given ``path`` has been modified.
263 """
244 """
264 path = self._assert_is_path(path)
245 path = self._assert_is_path(path)
265 hist = self._remote.node_history(self.raw_id, path, limit)
246 history = self._remote.node_history(self.raw_id, path, limit)
266 return [
247 return [self.repository.get_commit(commit_id=commit_id, pre_load=pre_load) for commit_id in history]
267 self.repository.get_commit(commit_id=commit_id, pre_load=pre_load)
268 for commit_id in hist]
269
248
270 def get_file_annotate(self, path, pre_load=None):
249 def get_file_annotate(self, path, pre_load=None):
271 """
250 """
@@ -276,11 +255,13 b' class MercurialCommit(base.BaseCommit):'
276
255
277 for ln_no, commit_id, content in result:
256 for ln_no, commit_id, content in result:
278 yield (
257 yield (
279 ln_no, commit_id,
258 ln_no,
259 commit_id,
280 lambda: self.repository.get_commit(commit_id=commit_id, pre_load=pre_load),
260 lambda: self.repository.get_commit(commit_id=commit_id, pre_load=pre_load),
281 content)
261 content,
262 )
282
263
283 def get_nodes(self, path, pre_load=None):
264 def get_nodes(self, path: bytes, pre_load=None):
284 """
265 """
285 Returns combined ``DirNode`` and ``FileNode`` objects list representing
266 Returns combined ``DirNode`` and ``FileNode`` objects list representing
286 state of commit at the given ``path``. If node at the given ``path``
267 state of commit at the given ``path``. If node at the given ``path``
@@ -288,59 +269,86 b' class MercurialCommit(base.BaseCommit):'
288 """
269 """
289
270
290 if self._get_kind(path) != NodeKind.DIR:
271 if self._get_kind(path) != NodeKind.DIR:
291 raise CommitError(
272 raise CommitError(f"Directory does not exist for idx {self.raw_id} at '{path}'")
292 f"Directory does not exist for idx {self.raw_id} at '{path}'")
293 path = self._fix_path(path)
273 path = self._fix_path(path)
294
274
295 filenodes = [
275 path_nodes = []
296 FileNode(safe_bytes(f), commit=self, pre_load=pre_load) for f in self._file_paths
276
297 if os.path.dirname(f) == path]
277 for obj_path, node_kind in self._remote.dir_items(self.raw_id, path):
298 # TODO: johbo: Check if this can be done in a more obvious way
278
299 dirs = path == '' and '' or [
279 if node_kind is None:
300 d for d in self._dir_paths
280 raise CommitError(f"Requested object type={node_kind} cannot be mapped to a proper type")
301 if d and vcspath.dirname(d) == path]
281
302 dirnodes = [
282 # TODO: implement it ??
303 DirNode(safe_bytes(d), commit=self) for d in dirs
283 stat_ = None
304 if os.path.dirname(d) == path]
284 # # cache file mode
285 # if obj_path not in self._path_mode_cache:
286 # self._path_mode_cache[obj_path] = stat_
287
288 # cache type
289 if node_kind not in self._path_type_cache:
290 self._path_type_cache[obj_path] = node_kind
305
291
306 alias = self.repository.alias
292 entry = None
307 for k, vals in self._submodules.items():
293 if obj_path in self.nodes:
308 if vcspath.dirname(k) == path:
294 entry = self.nodes[obj_path]
309 loc = vals[0]
295 else:
310 commit = vals[1]
296 if node_kind == NodeKind.DIR:
311 dirnodes.append(SubModuleNode(k, url=loc, commit=commit, alias=alias))
297 entry = DirNode(safe_bytes(obj_path), commit=self)
298 elif node_kind == NodeKind.FILE:
299 entry = FileNode(safe_bytes(obj_path), commit=self, mode=stat_, pre_load=pre_load)
300 if entry:
301 self.nodes[obj_path] = entry
302 path_nodes.append(entry)
312
303
313 nodes = dirnodes + filenodes
304 path_nodes.sort()
314 for node in nodes:
305 return path_nodes
315 if node.path not in self.nodes:
316 self.nodes[node.path] = node
317 nodes.sort()
318
306
319 return nodes
307 def get_node(self, path: bytes, pre_load=None):
320
321 def get_node(self, path, pre_load=None):
322 """
308 """
323 Returns `Node` object from the given `path`. If there is no node at
309 Returns `Node` object from the given `path`. If there is no node at
324 the given `path`, `NodeDoesNotExistError` would be raised.
310 the given `path`, `NodeDoesNotExistError` would be raised.
325 """
311 """
326 path = self._fix_path(path)
312 path = self._fix_path(path)
327
313
328 if path not in self.nodes:
314 # use cached, if we have one
329 if path in self._file_paths:
315 if path in self.nodes:
316 return self.nodes[path]
317
318 path_type = self._get_path_type(path)
319 if path == b"":
320 node = RootNode(commit=self)
321 else:
322 if path_type == NodeKind.DIR:
323 node = DirNode(safe_bytes(path), commit=self)
324 elif path_type == NodeKind.FILE:
330 node = FileNode(safe_bytes(path), commit=self, pre_load=pre_load)
325 node = FileNode(safe_bytes(path), commit=self, pre_load=pre_load)
331 elif path in self._dir_paths:
326 self._path_mode_cache[path] = node.mode
332 if path == '':
333 node = RootNode(commit=self)
334 else:
335 node = DirNode(safe_bytes(path), commit=self)
336 else:
327 else:
337 raise self.no_node_at_path(path)
328 raise self.no_node_at_path(path)
338
329 # cache node
339 # cache node
330 self.nodes[path] = node
340 self.nodes[path] = node
341 return self.nodes[path]
331 return self.nodes[path]
342
332
343 def get_largefile_node(self, path):
333 def _get_path_type(self, path: bytes):
334 if path in self._path_type_cache:
335 return self._path_type_cache[path]
336
337 if path == b"":
338 self._path_type_cache[b""] = NodeKind.DIR
339 return NodeKind.DIR
340
341 path_type, flags = self._remote.get_path_type(self.raw_id, path)
342
343 if not path_type:
344 raise self.no_node_at_path(path)
345
346 self._path_type_cache[path] = path_type
347 self._path_mode_cache[path] = flags
348
349 return self._path_type_cache[path]
350
351 def get_largefile_node(self, path: bytes):
344 pointer_spec = self._remote.is_large_file(self.raw_id, path)
352 pointer_spec = self._remote.is_large_file(self.raw_id, path)
345 if pointer_spec:
353 if pointer_spec:
346 # content of that file regular FileNode is the hash of largefile
354 # content of that file regular FileNode is the hash of largefile
@@ -363,40 +371,20 b' class MercurialCommit(base.BaseCommit):'
363 return self._remote.ctx_substate(self.raw_id)
371 return self._remote.ctx_substate(self.raw_id)
364
372
365 @LazyProperty
373 @LazyProperty
366 def affected_files(self):
374 def affected_files(self) -> list[bytes]:
367 """
375 """
368 Gets a fast accessible file changes for given commit
376 Gets a fast accessible file changes for given commit
369 """
377 """
370 return self._remote.ctx_files(self.raw_id)
378 return self._remote.ctx_files(self.raw_id)
371
379
372 @property
373 def added(self):
374 """
375 Returns list of added ``FileNode`` objects.
376 """
377 return AddedFileNodesGenerator(self.added_paths, self)
378
379 @LazyProperty
380 @LazyProperty
380 def added_paths(self):
381 def added_paths(self):
381 return [n for n in self.status[1]]
382 return [n for n in self.status[1]]
382
383
383 @property
384 def changed(self):
385 """
386 Returns list of modified ``FileNode`` objects.
387 """
388 return ChangedFileNodesGenerator(self.changed_paths, self)
389
390 @LazyProperty
384 @LazyProperty
391 def changed_paths(self):
385 def changed_paths(self):
392 return [n for n in self.status[0]]
386 return [n for n in self.status[0]]
393
387
394 @property
395 def removed(self):
396 """
397 Returns list of removed ``FileNode`` objects.
398 """
399 return RemovedFileNodesGenerator(self.removed_paths, self)
400
388
401 @LazyProperty
389 @LazyProperty
402 def removed_paths(self):
390 def removed_paths(self):
@@ -450,7 +450,7 b' class MercurialRepository(BaseRepository'
450 return os.path.join(self.path, '.hg', '.hgrc')
450 return os.path.join(self.path, '.hg', '.hgrc')
451
451
452 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None,
452 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None,
453 translate_tag=None, maybe_unreachable=False, reference_obj=None):
453 translate_tag=None, maybe_unreachable=False, reference_obj=None) -> MercurialCommit:
454 """
454 """
455 Returns ``MercurialCommit`` object representing repository's
455 Returns ``MercurialCommit`` object representing repository's
456 commit at the given `commit_id` or `commit_idx`.
456 commit at the given `commit_id` or `commit_idx`.
@@ -598,8 +598,7 b' class MercurialRepository(BaseRepository'
598 """
598 """
599 Create a local clone of the current repo.
599 Create a local clone of the current repo.
600 """
600 """
601 self._remote.clone(self.path, clone_path, update_after_clone=True,
601 self._remote.clone(self.path, clone_path, update_after_clone=True, hooks=False)
602 hooks=False)
603
602
604 def _update(self, revision, clean=False):
603 def _update(self, revision, clean=False):
605 """
604 """
@@ -19,8 +19,7 b''
19 """
19 """
20 SVN commit module
20 SVN commit module
21 """
21 """
22
22 import logging
23
24 import dateutil.parser
23 import dateutil.parser
25 from zope.cachedescriptors.property import Lazy as LazyProperty
24 from zope.cachedescriptors.property import Lazy as LazyProperty
26
25
@@ -28,9 +27,10 b' from rhodecode.lib.str_utils import safe'
28 from rhodecode.lib.vcs import nodes, path as vcspath
27 from rhodecode.lib.vcs import nodes, path as vcspath
29 from rhodecode.lib.vcs.backends import base
28 from rhodecode.lib.vcs.backends import base
30 from rhodecode.lib.vcs.exceptions import CommitError
29 from rhodecode.lib.vcs.exceptions import CommitError
31
30 from vcsserver.lib.vcs_common import NodeKind, FILEMODE_EXECUTABLE, FILEMODE_DEFAULT, FILEMODE_LINK
31 _SVN_PROP_TRUE = "*"
32
32
33 _SVN_PROP_TRUE = '*'
33 log = logging.getLogger(__name__)
34
34
35
35
36 class SubversionCommit(base.BaseCommit):
36 class SubversionCommit(base.BaseCommit):
@@ -53,15 +53,16 b' class SubversionCommit(base.BaseCommit):'
53 # which knows how to translate commit index and commit id
53 # which knows how to translate commit index and commit id
54 self.raw_id = commit_id
54 self.raw_id = commit_id
55 self.short_id = commit_id
55 self.short_id = commit_id
56 self.id = f'r{commit_id}'
56 self.id = f"r{commit_id}"
57
57
58 # TODO: Implement the following placeholder attributes
59 self.nodes = {}
58 self.nodes = {}
59 self._path_mode_cache = {} # path stats cache, e.g filemode etc
60 self._path_type_cache = {} # path type dir/file/link etc cache
60 self.tags = []
61 self.tags = []
61
62
62 @property
63 @property
63 def author(self):
64 def author(self):
64 return safe_str(self._properties.get('svn:author'))
65 return safe_str(self._properties.get("svn:author"))
65
66
66 @property
67 @property
67 def date(self):
68 def date(self):
@@ -69,7 +70,7 b' class SubversionCommit(base.BaseCommit):'
69
70
70 @property
71 @property
71 def message(self):
72 def message(self):
72 return safe_str(self._properties.get('svn:log'))
73 return safe_str(self._properties.get("svn:log"))
73
74
74 @LazyProperty
75 @LazyProperty
75 def _properties(self):
76 def _properties(self):
@@ -91,19 +92,46 b' class SubversionCommit(base.BaseCommit):'
91 return [child]
92 return [child]
92 return []
93 return []
93
94
94 def get_file_mode(self, path: bytes):
95 def _calculate_file_mode(self, path: bytes):
95 # Note: Subversion flags files which are executable with a special
96 # Note: Subversion flags files which are executable with a special
96 # property `svn:executable` which is set to the value ``"*"``.
97 # property `svn:executable` which is set to the value ``"*"``.
97 if self._get_file_property(path, 'svn:executable') == _SVN_PROP_TRUE:
98 if self._get_file_property(path, "svn:executable") == _SVN_PROP_TRUE:
98 return base.FILEMODE_EXECUTABLE
99 return FILEMODE_EXECUTABLE
99 else:
100 else:
100 return base.FILEMODE_DEFAULT
101 return FILEMODE_DEFAULT
102
103 def get_file_mode(self, path: bytes):
104 path = self._fix_path(path)
105
106 if path not in self._path_mode_cache:
107 self._path_mode_cache[path] = self._calculate_file_mode(path)
108
109 return self._path_mode_cache[path]
110
111 def _get_path_type(self, path: bytes):
112 if path in self._path_type_cache:
113 return self._path_type_cache[path]
101
114
102 def is_link(self, path):
115 if path == b"":
116 self._path_type_cache[b""] = NodeKind.DIR
117 return NodeKind.DIR
118
119 path_type = self._remote.get_node_type(self._svn_rev, path)
120
121 if not path_type:
122 raise self.no_node_at_path(path)
123
124 #flags = None
125 self._path_type_cache[path] = path_type
126 #self._path_mode_cache[path] = flags
127
128 return self._path_type_cache[path]
129
130 def is_link(self, path: bytes):
103 # Note: Subversion has a flag for special files, the content of the
131 # Note: Subversion has a flag for special files, the content of the
104 # file contains the type of that file.
132 # file contains the type of that file.
105 if self._get_file_property(path, 'svn:special') == _SVN_PROP_TRUE:
133 if self._get_file_property(path, "svn:special") == _SVN_PROP_TRUE:
106 return self.get_file_content(path).startswith(b'link')
134 return self.get_file_content(path).startswith(b"link")
107 return False
135 return False
108
136
109 def is_node_binary(self, path):
137 def is_node_binary(self, path):
@@ -115,8 +143,7 b' class SubversionCommit(base.BaseCommit):'
115 return self._remote.md5_hash(self._svn_rev, safe_str(path))
143 return self._remote.md5_hash(self._svn_rev, safe_str(path))
116
144
117 def _get_file_property(self, path, name):
145 def _get_file_property(self, path, name):
118 file_properties = self._remote.node_properties(
146 file_properties = self._remote.node_properties(safe_str(path), self._svn_rev)
119 safe_str(path), self._svn_rev)
120 return file_properties.get(name)
147 return file_properties.get(name)
121
148
122 def get_file_content(self, path):
149 def get_file_content(self, path):
@@ -126,7 +153,7 b' class SubversionCommit(base.BaseCommit):'
126 def get_file_content_streamed(self, path):
153 def get_file_content_streamed(self, path):
127 path = self._fix_path(path)
154 path = self._fix_path(path)
128
155
129 stream_method = getattr(self._remote, 'stream:get_file_content')
156 stream_method = getattr(self._remote, "stream:get_file_content")
130 return stream_method(self._svn_rev, safe_str(path))
157 return stream_method(self._svn_rev, safe_str(path))
131
158
132 def get_file_size(self, path):
159 def get_file_size(self, path):
@@ -134,11 +161,9 b' class SubversionCommit(base.BaseCommit):'
134 return self._remote.get_file_size(self._svn_rev, safe_str(path))
161 return self._remote.get_file_size(self._svn_rev, safe_str(path))
135
162
136 def get_path_history(self, path, limit=None, pre_load=None):
163 def get_path_history(self, path, limit=None, pre_load=None):
137 path = safe_str(self._fix_path(path))
164 path = self._fix_path(path)
138 history = self._remote.node_history(path, self._svn_rev, limit)
165 history = self._remote.node_history(self._svn_rev, safe_str(path), limit)
139 return [
166 return [self.repository.get_commit(commit_id=str(svn_rev)) for svn_rev in history]
140 self.repository.get_commit(commit_id=str(svn_rev))
141 for svn_rev in history]
142
167
143 def get_file_annotate(self, path, pre_load=None):
168 def get_file_annotate(self, path, pre_load=None):
144 result = self._remote.file_annotate(safe_str(path), self._svn_rev)
169 result = self._remote.file_annotate(safe_str(path), self._svn_rev)
@@ -146,67 +171,78 b' class SubversionCommit(base.BaseCommit):'
146 for zero_based_line_no, svn_rev, content in result:
171 for zero_based_line_no, svn_rev, content in result:
147 commit_id = str(svn_rev)
172 commit_id = str(svn_rev)
148 line_no = zero_based_line_no + 1
173 line_no = zero_based_line_no + 1
149 yield (
174 yield line_no, commit_id, lambda: self.repository.get_commit(commit_id=commit_id), content
150 line_no,
151 commit_id,
152 lambda: self.repository.get_commit(commit_id=commit_id),
153 content)
154
175
155 def get_node(self, path, pre_load=None):
176 def get_node(self, path: bytes, pre_load=None):
156 path = self._fix_path(path)
177 path = self._fix_path(path)
157 if path not in self.nodes:
178
179 # use cached, if we have one
180 if path in self.nodes:
181 return self.nodes[path]
158
182
159 if path == '':
183 path_type = self._get_path_type(path)
160 node = nodes.RootNode(commit=self)
184 if path == b"":
185 node = nodes.RootNode(commit=self)
186 else:
187 if path_type == NodeKind.DIR:
188 node = nodes.DirNode(safe_bytes(path), commit=self)
189 elif path_type == NodeKind.FILE:
190 node = nodes.FileNode(safe_bytes(path), commit=self, pre_load=pre_load)
191 self._path_mode_cache[path] = node.mode
161 else:
192 else:
162 node_type = self._remote.get_node_type(self._svn_rev, safe_str(path))
193 raise self.no_node_at_path(path)
163 if node_type == 'dir':
164 node = nodes.DirNode(safe_bytes(path), commit=self)
165 elif node_type == 'file':
166 node = nodes.FileNode(safe_bytes(path), commit=self, pre_load=pre_load)
167 else:
168 raise self.no_node_at_path(path)
169
194
170 self.nodes[path] = node
195 self.nodes[path] = node
171 return self.nodes[path]
196 return self.nodes[path]
172
197
173 def get_nodes(self, path, pre_load=None):
198 def get_nodes(self, path: bytes, pre_load=None):
174 if self._get_kind(path) != nodes.NodeKind.DIR:
199 if self._get_kind(path) != nodes.NodeKind.DIR:
175 raise CommitError(
200 raise CommitError(f"Directory does not exist for commit {self.raw_id} at '{path}'")
176 f"Directory does not exist for commit {self.raw_id} at '{path}'")
201 path = self._fix_path(path)
177 path = safe_str(self._fix_path(path))
178
202
179 path_nodes = []
203 path_nodes = []
180 for name, kind in self._remote.get_nodes(self._svn_rev, path):
204 for name, node_kind in self._remote.get_nodes(self._svn_rev, path):
181 node_path = vcspath.join(path, name)
205 obj_path = vcspath.join(path, name)
182 if kind == 'dir':
206
183 node = nodes.DirNode(safe_bytes(node_path), commit=self)
207 if node_kind is None:
184 elif kind == 'file':
208 raise CommitError(f"Requested object type={node_kind} cannot be determined")
185 node = nodes.FileNode(safe_bytes(node_path), commit=self, pre_load=pre_load)
209
210 # TODO: implement it ??
211 stat_ = None
212 # # cache file mode
213 # if obj_path not in self._path_mode_cache:
214 # self._path_mode_cache[obj_path] = stat_
215
216 # cache type
217 if node_kind not in self._path_type_cache:
218 self._path_type_cache[obj_path] = node_kind
219
220 entry = None
221 if obj_path in self.nodes:
222 entry = self.nodes[obj_path]
186 else:
223 else:
187 raise ValueError(f"Node kind {kind} not supported.")
224 if node_kind == NodeKind.DIR:
188 self.nodes[node_path] = node
225 entry = nodes.DirNode(safe_bytes(obj_path), commit=self)
189 path_nodes.append(node)
226 elif node_kind == NodeKind.FILE:
227 entry = nodes.FileNode(safe_bytes(obj_path), commit=self, mode=stat_, pre_load=pre_load)
228 if entry:
229 self.nodes[obj_path] = entry
230 path_nodes.append(entry)
190
231
232 path_nodes.sort()
191 return path_nodes
233 return path_nodes
192
234
193 def _get_kind(self, path):
235 def _get_kind(self, path):
194 path = self._fix_path(path)
236 path = self._fix_path(path)
195 kind = self._remote.get_node_type(self._svn_rev, path)
237 path_type = self._get_path_type(path)
196 if kind == 'file':
238 return path_type
197 return nodes.NodeKind.FILE
198 elif kind == 'dir':
199 return nodes.NodeKind.DIR
200 else:
201 raise CommitError(
202 f"Node does not exist at the given path '{path}'")
203
239
204 @LazyProperty
240 @LazyProperty
205 def _changes_cache(self):
241 def _changes_cache(self):
206 return self._remote.revision_changes(self._svn_rev)
242 return self._remote.revision_changes(self._svn_rev)
207
243
208 @LazyProperty
244 @LazyProperty
209 def affected_files(self):
245 def affected_files(self) -> list[bytes]:
210 changed_files = set()
246 changed_files = set()
211 for files in self._changes_cache.values():
247 for files in self._changes_cache.values():
212 changed_files.update(files)
248 changed_files.update(files)
@@ -216,29 +252,17 b' class SubversionCommit(base.BaseCommit):'
216 def id(self):
252 def id(self):
217 return self.raw_id
253 return self.raw_id
218
254
219 @property
220 def added(self):
221 return nodes.AddedFileNodesGenerator(self.added_paths, self)
222
223 @LazyProperty
255 @LazyProperty
224 def added_paths(self):
256 def added_paths(self):
225 return [n for n in self._changes_cache['added']]
257 return [n for n in self._changes_cache["added"]]
226
227 @property
228 def changed(self):
229 return nodes.ChangedFileNodesGenerator(self.changed_paths, self)
230
258
231 @LazyProperty
259 @LazyProperty
232 def changed_paths(self):
260 def changed_paths(self):
233 return [n for n in self._changes_cache['changed']]
261 return [n for n in self._changes_cache["changed"]]
234
235 @property
236 def removed(self):
237 return nodes.RemovedFileNodesGenerator(self.removed_paths, self)
238
262
239 @LazyProperty
263 @LazyProperty
240 def removed_paths(self):
264 def removed_paths(self):
241 return [n for n in self._changes_cache['removed']]
265 return [n for n in self._changes_cache["removed"]]
242
266
243
267
244 def _date_from_svn_properties(properties):
268 def _date_from_svn_properties(properties):
@@ -248,7 +272,7 b' def _date_from_svn_properties(properties'
248 :return: :class:`datetime.datetime` instance. The object is naive.
272 :return: :class:`datetime.datetime` instance. The object is naive.
249 """
273 """
250
274
251 aware_date = dateutil.parser.parse(properties.get('svn:date'))
275 aware_date = dateutil.parser.parse(properties.get("svn:date"))
252 # final_date = aware_date.astimezone(dateutil.tz.tzlocal())
276 # final_date = aware_date.astimezone(dateutil.tz.tzlocal())
253 final_date = aware_date
277 final_date = aware_date
254 return final_date.replace(tzinfo=None)
278 return final_date.replace(tzinfo=None)
@@ -30,7 +30,7 b' from zope.cachedescriptors.property impo'
30
30
31 from collections import OrderedDict
31 from collections import OrderedDict
32 from rhodecode.lib.datelib import date_astimestamp
32 from rhodecode.lib.datelib import date_astimestamp
33 from rhodecode.lib.str_utils import safe_str
33 from rhodecode.lib.str_utils import safe_str, safe_bytes
34 from rhodecode.lib.utils2 import CachedProperty
34 from rhodecode.lib.utils2 import CachedProperty
35 from rhodecode.lib.vcs import connection, path as vcspath
35 from rhodecode.lib.vcs import connection, path as vcspath
36 from rhodecode.lib.vcs.backends import base
36 from rhodecode.lib.vcs.backends import base
@@ -157,16 +157,18 b' class SubversionRepository(base.BaseRepo'
157
157
158 for pattern in self._patterns_from_section(config_section):
158 for pattern in self._patterns_from_section(config_section):
159 pattern = vcspath.sanitize(pattern)
159 pattern = vcspath.sanitize(pattern)
160 bytes_pattern = safe_bytes(pattern)
161
160 tip = self.get_commit()
162 tip = self.get_commit()
161 try:
163 try:
162 if pattern.endswith('*'):
164 if bytes_pattern.endswith(b'*'):
163 basedir = tip.get_node(vcspath.dirname(pattern))
165 basedir = tip.get_node(vcspath.dirname(bytes_pattern))
164 directories = basedir.dirs
166 directories = basedir.dirs
165 else:
167 else:
166 directories = (tip.get_node(pattern), )
168 directories = (tip.get_node(bytes_pattern), )
167 except NodeDoesNotExistError:
169 except NodeDoesNotExistError:
168 continue
170 continue
169 found_items.update((safe_str(n.path), self.commit_ids[-1]) for n in directories)
171 found_items.update((dir_node.str_path, self.commit_ids[-1]) for dir_node in directories)
170
172
171 def get_name(item):
173 def get_name(item):
172 return item[0]
174 return item[0]
@@ -216,7 +218,7 b' class SubversionRepository(base.BaseRepo'
216 def _get_commit_idx(self, commit_id):
218 def _get_commit_idx(self, commit_id):
217 try:
219 try:
218 svn_rev = int(commit_id)
220 svn_rev = int(commit_id)
219 except:
221 except Exception:
220 # TODO: johbo: this might be only one case, HEAD, check this
222 # TODO: johbo: this might be only one case, HEAD, check this
221 svn_rev = self._remote.lookup(commit_id)
223 svn_rev = self._remote.lookup(commit_id)
222 commit_idx = svn_rev - 1
224 commit_idx = svn_rev - 1
@@ -321,8 +323,7 b' class SubversionRepository(base.BaseRepo'
321 # TODO: johbo: Reconsider impact of DEFAULT_BRANCH_NAME here
323 # TODO: johbo: Reconsider impact of DEFAULT_BRANCH_NAME here
322 if branch_name not in [None, self.DEFAULT_BRANCH_NAME]:
324 if branch_name not in [None, self.DEFAULT_BRANCH_NAME]:
323 svn_rev = int(self.commit_ids[-1])
325 svn_rev = int(self.commit_ids[-1])
324 commit_ids = self._remote.node_history(
326 commit_ids = self._remote.node_history(svn_rev, branch_name, None)
325 path=branch_name, revision=svn_rev, limit=None)
326 commit_ids = [str(i) for i in reversed(commit_ids)]
327 commit_ids = [str(i) for i in reversed(commit_ids)]
327
328
328 if start_pos or end_pos:
329 if start_pos or end_pos:
@@ -27,14 +27,13 b' import time'
27 import urllib.request
27 import urllib.request
28 import urllib.error
28 import urllib.error
29 import urllib.parse
29 import urllib.parse
30 import urllib.parse
31 import uuid
30 import uuid
32 import traceback
31 import traceback
33
32
34 import pycurl
33 import pycurl
35 import msgpack
34 import msgpack
36 import requests
35 import requests
37 from requests.packages.urllib3.util.retry import Retry
36 from urllib3.util.retry import Retry
38
37
39 import rhodecode
38 import rhodecode
40 from rhodecode.lib import rc_cache
39 from rhodecode.lib import rc_cache
@@ -287,7 +286,7 b' class RemoteRepo(object):'
287 'fctx_size', 'stream:fctx_node_data', 'blob_raw_length',
286 'fctx_size', 'stream:fctx_node_data', 'blob_raw_length',
288 'node_history',
287 'node_history',
289 'revision', 'tree_items',
288 'revision', 'tree_items',
290 'ctx_list', 'ctx_branch', 'ctx_description',
289 'ctx_branch', 'ctx_description',
291 'bulk_request',
290 'bulk_request',
292 'assert_correct_path',
291 'assert_correct_path',
293 'is_path_valid_repository',
292 'is_path_valid_repository',
@@ -1,1 +1,1 b''
1 from pyramid.compat import configparser No newline at end of file
1 from pyramid.compat import configparser
@@ -20,9 +20,6 b''
20 Internal settings for vcs-lib
20 Internal settings for vcs-lib
21 """
21 """
22
22
23 # list of default encoding used in safe_str methods
24 DEFAULT_ENCODINGS = ['utf8']
25
26
23
27 # Compatibility version when creating SVN repositories. None means newest.
24 # Compatibility version when creating SVN repositories. None means newest.
28 # Other available options are: pre-1.4-compatible, pre-1.5-compatible,
25 # Other available options are: pre-1.4-compatible, pre-1.5-compatible,
@@ -102,10 +102,6 b' class NodeError(VCSError):'
102 pass
102 pass
103
103
104
104
105 class RemovedFileNodeError(NodeError):
106 pass
107
108
109 class NodeAlreadyExistsError(CommittingError):
105 class NodeAlreadyExistsError(CommittingError):
110 pass
106 pass
111
107
@@ -19,6 +19,7 b''
19 """
19 """
20 Module holding everything related to vcs nodes, with vcs2 architecture.
20 Module holding everything related to vcs nodes, with vcs2 architecture.
21 """
21 """
22
22 import functools
23 import functools
23 import os
24 import os
24 import stat
25 import stat
@@ -29,83 +30,25 b' from rhodecode.config.conf import LANGUA'
29 from rhodecode.lib.str_utils import safe_str, safe_bytes
30 from rhodecode.lib.str_utils import safe_str, safe_bytes
30 from rhodecode.lib.hash_utils import md5
31 from rhodecode.lib.hash_utils import md5
31 from rhodecode.lib.vcs import path as vcspath
32 from rhodecode.lib.vcs import path as vcspath
32 from rhodecode.lib.vcs.backends.base import EmptyCommit, FILEMODE_DEFAULT
33 from rhodecode.lib.vcs.backends.base import EmptyCommit
33 from rhodecode.lib.vcs.conf.mtypes import get_mimetypes_db
34 from rhodecode.lib.vcs.conf.mtypes import get_mimetypes_db
34 from rhodecode.lib.vcs.exceptions import NodeError, RemovedFileNodeError
35 from rhodecode.lib.vcs.exceptions import NodeError
35
36 from rhodecode.lib.vcs_common import NodeKind, FILEMODE_DEFAULT
36 LARGEFILE_PREFIX = '.hglf'
37
37
38
38 LARGEFILE_PREFIX = ".hglf"
39 class NodeKind:
40 SUBMODULE = -1
41 DIR = 1
42 FILE = 2
43 LARGEFILE = 3
44
39
45
40
46 class NodeState:
41 class NodeState:
47 ADDED = 'added'
42 ADDED = "added"
48 CHANGED = 'changed'
43 CHANGED = "changed"
49 NOT_CHANGED = 'not changed'
44 NOT_CHANGED = "not changed"
50 REMOVED = 'removed'
45 REMOVED = "removed"
51
52 #TODO: not sure if that should be bytes or str ?
53 # most probably bytes because content should be bytes and we check it
54 BIN_BYTE_MARKER = b'\0'
55
46
56
47
57 class NodeGeneratorBase(object):
48 # TODO: not sure if that should be bytes or str ?
58 """
49 # most probably bytes because content should be bytes and we check it
59 Base class for removed added and changed filenodes, it's a lazy generator
50 BIN_BYTE_MARKER = b"\0"
60 class that will create filenodes only on iteration or call
61
62 The len method doesn't need to create filenodes at all
63 """
64
65 def __init__(self, current_paths, cs):
66 self.cs = cs
67 self.current_paths = current_paths
68
69 def __call__(self):
70 return [n for n in self]
71
72 def __getitem__(self, key):
73 if isinstance(key, slice):
74 for p in self.current_paths[key.start:key.stop]:
75 yield self.cs.get_node(p)
76
77 def __len__(self):
78 return len(self.current_paths)
79
51
80 def __iter__(self):
81 for p in self.current_paths:
82 yield self.cs.get_node(p)
83
84
85 class AddedFileNodesGenerator(NodeGeneratorBase):
86 """
87 Class holding added files for current commit
88 """
89
90
91 class ChangedFileNodesGenerator(NodeGeneratorBase):
92 """
93 Class holding changed files for current commit
94 """
95
96
97 class RemovedFileNodesGenerator(NodeGeneratorBase):
98 """
99 Class holding removed files for current commit
100 """
101 def __iter__(self):
102 for p in self.current_paths:
103 yield RemovedFileNode(path=safe_bytes(p))
104
105 def __getitem__(self, key):
106 if isinstance(key, slice):
107 for p in self.current_paths[key.start:key.stop]:
108 yield RemovedFileNode(path=safe_bytes(p))
109
52
110
53
111 @functools.total_ordering
54 @functools.total_ordering
@@ -119,21 +62,22 b' class Node(object):'
119 only. Moreover, every single node is identified by the ``path`` attribute,
62 only. Moreover, every single node is identified by the ``path`` attribute,
120 so it cannot end with slash, too. Otherwise, path could lead to mistakes.
63 so it cannot end with slash, too. Otherwise, path could lead to mistakes.
121 """
64 """
65
122 # RTLO marker allows swapping text, and certain
66 # RTLO marker allows swapping text, and certain
123 # security attacks could be used with this
67 # security attacks could be used with this
124 RTLO_MARKER = "\u202E"
68 RTLO_MARKER = "\u202e"
125
69
126 commit = None
70 commit = None
127
71
128 def __init__(self, path: bytes, kind):
72 def __init__(self, path: bytes, kind):
129 self._validate_path(path) # can throw exception if path is invalid
73 self._validate_path(path) # can throw exception if path is invalid
130
74
131 self.bytes_path = path.rstrip(b'/') # store for __repr__
75 self.bytes_path: bytes = path.rstrip(b"/") # store for mixed encoding, and raw version
132 self.path = safe_str(self.bytes_path) # we store paths as str
76 self.str_path: str = safe_str(self.bytes_path) # we store paths as str
77 self.path: str = self.str_path
133
78
134 if self.bytes_path == b'' and kind != NodeKind.DIR:
79 if self.bytes_path == b"" and kind != NodeKind.DIR:
135 raise NodeError("Only DirNode and its subclasses may be "
80 raise NodeError("Only DirNode and its subclasses may be initialized with empty path")
136 "initialized with empty path")
137 self.kind = kind
81 self.kind = kind
138
82
139 if self.is_root() and not self.is_dir():
83 if self.is_root() and not self.is_dir():
@@ -142,7 +86,7 b' class Node(object):'
142 def __eq__(self, other):
86 def __eq__(self, other):
143 if type(self) is not type(other):
87 if type(self) is not type(other):
144 return False
88 return False
145 for attr in ['name', 'path', 'kind']:
89 for attr in ["name", "path", "kind"]:
146 if getattr(self, attr) != getattr(other, attr):
90 if getattr(self, attr) != getattr(other, attr):
147 return False
91 return False
148 if self.is_file():
92 if self.is_file():
@@ -166,22 +110,9 b' class Node(object):'
166 if self.path > other.path:
110 if self.path > other.path:
167 return False
111 return False
168
112
169 # def __cmp__(self, other):
170 # """
171 # Comparator using name of the node, needed for quick list sorting.
172 # """
173 #
174 # kind_cmp = cmp(self.kind, other.kind)
175 # if kind_cmp:
176 # if isinstance(self, SubModuleNode):
177 # # we make submodules equal to dirnode for "sorting" purposes
178 # return NodeKind.DIR
179 # return kind_cmp
180 # return cmp(self.name, other.name)
181
182 def __repr__(self):
113 def __repr__(self):
183 maybe_path = getattr(self, 'path', 'UNKNOWN_PATH')
114 maybe_path = getattr(self, "path", "UNKNOWN_PATH")
184 return f'<{self.__class__.__name__} {maybe_path!r}>'
115 return f"<{self.__class__.__name__} {maybe_path!r}>"
185
116
186 def __str__(self):
117 def __str__(self):
187 return self.name
118 return self.name
@@ -189,19 +120,21 b' class Node(object):'
189 def _validate_path(self, path: bytes):
120 def _validate_path(self, path: bytes):
190 self._assert_bytes(path)
121 self._assert_bytes(path)
191
122
192 if path.startswith(b'/'):
123 if path.startswith(b"/"):
193 raise NodeError(
124 raise NodeError(
194 f"Cannot initialize Node objects with slash at "
125 f"Cannot initialize Node objects with slash at "
195 f"the beginning as only relative paths are supported. "
126 f"the beginning as only relative paths are supported. "
196 f"Got {path}")
127 f"Got {path}"
128 )
197
129
198 def _assert_bytes(self, value):
130 @classmethod
131 def _assert_bytes(cls, value):
199 if not isinstance(value, bytes):
132 if not isinstance(value, bytes):
200 raise TypeError(f"Bytes required as input, got {type(value)} of {value}.")
133 raise TypeError(f"Bytes required as input, got {type(value)} of {value}.")
201
134
202 @LazyProperty
135 @LazyProperty
203 def parent(self):
136 def parent(self):
204 parent_path = self.get_parent_path()
137 parent_path: bytes = self.get_parent_path()
205 if parent_path:
138 if parent_path:
206 if self.commit:
139 if self.commit:
207 return self.commit.get_node(parent_path)
140 return self.commit.get_node(parent_path)
@@ -209,10 +142,6 b' class Node(object):'
209 return None
142 return None
210
143
211 @LazyProperty
144 @LazyProperty
212 def str_path(self) -> str:
213 return safe_str(self.path)
214
215 @LazyProperty
216 def has_rtlo(self):
145 def has_rtlo(self):
217 """Detects if a path has right-to-left-override marker"""
146 """Detects if a path has right-to-left-override marker"""
218 return self.RTLO_MARKER in self.str_path
147 return self.RTLO_MARKER in self.str_path
@@ -223,10 +152,10 b' class Node(object):'
223 Returns name of the directory from full path of this vcs node. Empty
152 Returns name of the directory from full path of this vcs node. Empty
224 string is returned if there's no directory in the path
153 string is returned if there's no directory in the path
225 """
154 """
226 _parts = self.path.rstrip('/').rsplit('/', 1)
155 _parts = self.path.rstrip("/").rsplit("/", 1)
227 if len(_parts) == 2:
156 if len(_parts) == 2:
228 return _parts[0]
157 return _parts[0]
229 return ''
158 return ""
230
159
231 @LazyProperty
160 @LazyProperty
232 def name(self):
161 def name(self):
@@ -234,7 +163,7 b' class Node(object):'
234 Returns name of the node so if its path
163 Returns name of the node so if its path
235 then only last part is returned.
164 then only last part is returned.
236 """
165 """
237 return self.path.rstrip('/').split('/')[-1]
166 return self.str_path.rstrip("/").split("/")[-1]
238
167
239 @property
168 @property
240 def kind(self):
169 def kind(self):
@@ -242,12 +171,12 b' class Node(object):'
242
171
243 @kind.setter
172 @kind.setter
244 def kind(self, kind):
173 def kind(self, kind):
245 if hasattr(self, '_kind'):
174 if hasattr(self, "_kind"):
246 raise NodeError("Cannot change node's kind")
175 raise NodeError("Cannot change node's kind")
247 else:
176 else:
248 self._kind = kind
177 self._kind = kind
249 # Post setter check (path's trailing slash)
178 # Post setter check (path's trailing slash)
250 if self.path.endswith('/'):
179 if self.str_path.endswith("/"):
251 raise NodeError("Node's path cannot end with slash")
180 raise NodeError("Node's path cannot end with slash")
252
181
253 def get_parent_path(self) -> bytes:
182 def get_parent_path(self) -> bytes:
@@ -255,8 +184,8 b' class Node(object):'
255 Returns node's parent path or empty string if node is root.
184 Returns node's parent path or empty string if node is root.
256 """
185 """
257 if self.is_root():
186 if self.is_root():
258 return b''
187 return b""
259 str_path = vcspath.dirname(self.path.rstrip('/')) + '/'
188 str_path = vcspath.dirname(self.bytes_path.rstrip(b"/")) + b"/"
260
189
261 return safe_bytes(str_path)
190 return safe_bytes(str_path)
262
191
@@ -278,7 +207,7 b' class Node(object):'
278 """
207 """
279 Returns ``True`` if node is a root node and ``False`` otherwise.
208 Returns ``True`` if node is a root node and ``False`` otherwise.
280 """
209 """
281 return self.kind == NodeKind.DIR and self.path == ''
210 return self.kind == NodeKind.DIR and self.path == ""
282
211
283 def is_submodule(self):
212 def is_submodule(self):
284 """
213 """
@@ -292,29 +221,13 b' class Node(object):'
292 Returns ``True`` if node's kind is ``NodeKind.LARGEFILE``, ``False``
221 Returns ``True`` if node's kind is ``NodeKind.LARGEFILE``, ``False``
293 otherwise
222 otherwise
294 """
223 """
295 return self.kind == NodeKind.LARGEFILE
224 return self.kind == NodeKind.LARGE_FILE
296
225
297 def is_link(self):
226 def is_link(self):
298 if self.commit:
227 if self.commit:
299 return self.commit.is_link(self.path)
228 return self.commit.is_link(self.bytes_path)
300 return False
229 return False
301
230
302 @LazyProperty
303 def added(self):
304 return self.state is NodeState.ADDED
305
306 @LazyProperty
307 def changed(self):
308 return self.state is NodeState.CHANGED
309
310 @LazyProperty
311 def not_changed(self):
312 return self.state is NodeState.NOT_CHANGED
313
314 @LazyProperty
315 def removed(self):
316 return self.state is NodeState.REMOVED
317
318
231
319 class FileNode(Node):
232 class FileNode(Node):
320 """
233 """
@@ -325,6 +238,7 b' class FileNode(Node):'
325 :attribute: commit: if given, first time content is accessed, callback
238 :attribute: commit: if given, first time content is accessed, callback
326 :attribute: mode: stat mode for a node. Default is `FILEMODE_DEFAULT`.
239 :attribute: mode: stat mode for a node. Default is `FILEMODE_DEFAULT`.
327 """
240 """
241
328 _filter_pre_load = []
242 _filter_pre_load = []
329
243
330 def __init__(self, path: bytes, content: bytes | None = None, commit=None, mode=None, pre_load=None):
244 def __init__(self, path: bytes, content: bytes | None = None, commit=None, mode=None, pre_load=None):
@@ -359,7 +273,7 b' class FileNode(Node):'
359 return self.content == other.content
273 return self.content == other.content
360
274
361 def __hash__(self):
275 def __hash__(self):
362 raw_id = getattr(self.commit, 'raw_id', '')
276 raw_id = getattr(self.commit, "raw_id", "")
363 return hash((self.path, raw_id))
277 return hash((self.path, raw_id))
364
278
365 def __lt__(self, other):
279 def __lt__(self, other):
@@ -369,33 +283,32 b' class FileNode(Node):'
369 return self.content < other.content
283 return self.content < other.content
370
284
371 def __repr__(self):
285 def __repr__(self):
372 short_id = getattr(self.commit, 'short_id', '')
286 short_id = getattr(self.commit, "short_id", "")
373 return f'<{self.__class__.__name__} path={self.path!r}, short_id={short_id}>'
287 return f"<{self.__class__.__name__} path={self.str_path!r}, short_id={short_id}>"
374
288
375 def _set_bulk_properties(self, pre_load):
289 def _set_bulk_properties(self, pre_load):
376 if not pre_load:
290 if not pre_load:
377 return
291 return
378 pre_load = [entry for entry in pre_load
292 pre_load = [entry for entry in pre_load if entry not in self._filter_pre_load]
379 if entry not in self._filter_pre_load]
380 if not pre_load:
293 if not pre_load:
381 return
294 return
382
295
383 remote = self.commit.get_remote()
296 remote = self.commit.get_remote()
384 result = remote.bulk_file_request(self.commit.raw_id, self.path, pre_load)
297 result = remote.bulk_file_request(self.commit.raw_id, self.bytes_path, pre_load)
385
298
386 for attr, value in result.items():
299 for attr, value in result.items():
387 if attr == "flags":
300 if attr == "flags":
388 self.__dict__['mode'] = safe_str(value)
301 self.__dict__["mode"] = safe_str(value)
389 elif attr == "size":
302 elif attr == "size":
390 self.__dict__['size'] = value
303 self.__dict__["size"] = value
391 elif attr == "data":
304 elif attr == "data":
392 self.__dict__['_content'] = value
305 self.__dict__["_content"] = value
393 elif attr == "is_binary":
306 elif attr == "is_binary":
394 self.__dict__['is_binary'] = value
307 self.__dict__["is_binary"] = value
395 elif attr == "md5":
308 elif attr == "md5":
396 self.__dict__['md5'] = value
309 self.__dict__["md5"] = value
397 else:
310 else:
398 raise ValueError(f'Unsupported attr in bulk_property: {attr}')
311 raise ValueError(f"Unsupported attr in bulk_property: {attr}")
399
312
400 @LazyProperty
313 @LazyProperty
401 def mode(self):
314 def mode(self):
@@ -404,7 +317,7 b' class FileNode(Node):'
404 use value given at initialization or `FILEMODE_DEFAULT` (default).
317 use value given at initialization or `FILEMODE_DEFAULT` (default).
405 """
318 """
406 if self.commit:
319 if self.commit:
407 mode = self.commit.get_file_mode(self.path)
320 mode = self.commit.get_file_mode(self.bytes_path)
408 else:
321 else:
409 mode = self._mode
322 mode = self._mode
410 return mode
323 return mode
@@ -416,7 +329,7 b' class FileNode(Node):'
416 """
329 """
417 if self.commit:
330 if self.commit:
418 if self._content is None:
331 if self._content is None:
419 self._content = self.commit.get_file_content(self.path)
332 self._content = self.commit.get_file_content(self.bytes_path)
420 content = self._content
333 content = self._content
421 else:
334 else:
422 content = self._content
335 content = self._content
@@ -427,7 +340,7 b' class FileNode(Node):'
427 Returns lazily content of the FileNode.
340 Returns lazily content of the FileNode.
428 """
341 """
429 if self.commit:
342 if self.commit:
430 content = self.commit.get_file_content(self.path)
343 content = self.commit.get_file_content(self.bytes_path)
431 else:
344 else:
432 content = self._content
345 content = self._content
433 return content
346 return content
@@ -438,7 +351,7 b' class FileNode(Node):'
438 vcsserver without loading it to memory.
351 vcsserver without loading it to memory.
439 """
352 """
440 if self.commit:
353 if self.commit:
441 return self.commit.get_file_content_streamed(self.path)
354 return self.commit.get_file_content_streamed(self.bytes_path)
442 raise NodeError("Cannot retrieve stream_bytes without related commit attribute")
355 raise NodeError("Cannot retrieve stream_bytes without related commit attribute")
443
356
444 def metadata_uncached(self):
357 def metadata_uncached(self):
@@ -462,7 +375,7 b' class FileNode(Node):'
462 """
375 """
463 content = self.raw_bytes
376 content = self.raw_bytes
464 if content and not isinstance(content, bytes):
377 if content and not isinstance(content, bytes):
465 raise ValueError(f'Content is of type {type(content)} instead of bytes')
378 raise ValueError(f"Content is of type {type(content)} instead of bytes")
466 return content
379 return content
467
380
468 @LazyProperty
381 @LazyProperty
@@ -472,27 +385,21 b' class FileNode(Node):'
472 @LazyProperty
385 @LazyProperty
473 def size(self):
386 def size(self):
474 if self.commit:
387 if self.commit:
475 return self.commit.get_file_size(self.path)
388 return self.commit.get_file_size(self.bytes_path)
476 raise NodeError(
389 raise NodeError("Cannot retrieve size of the file without related commit attribute")
477 "Cannot retrieve size of the file without related "
478 "commit attribute")
479
390
480 @LazyProperty
391 @LazyProperty
481 def message(self):
392 def message(self):
482 if self.commit:
393 if self.commit:
483 return self.last_commit.message
394 return self.last_commit.message
484 raise NodeError(
395 raise NodeError("Cannot retrieve message of the file without related " "commit attribute")
485 "Cannot retrieve message of the file without related "
486 "commit attribute")
487
396
488 @LazyProperty
397 @LazyProperty
489 def last_commit(self):
398 def last_commit(self):
490 if self.commit:
399 if self.commit:
491 pre_load = ["author", "date", "message", "parents"]
400 pre_load = ["author", "date", "message", "parents"]
492 return self.commit.get_path_commit(self.path, pre_load=pre_load)
401 return self.commit.get_path_commit(self.bytes_path, pre_load=pre_load)
493 raise NodeError(
402 raise NodeError("Cannot retrieve last commit of the file without related commit attribute")
494 "Cannot retrieve last commit of the file without "
495 "related commit attribute")
496
403
497 def get_mimetype(self):
404 def get_mimetype(self):
498 """
405 """
@@ -502,28 +409,27 b' class FileNode(Node):'
502 attribute to indicate that type should *NOT* be calculated).
409 attribute to indicate that type should *NOT* be calculated).
503 """
410 """
504
411
505 if hasattr(self, '_mimetype'):
412 if hasattr(self, "_mimetype"):
506 if (isinstance(self._mimetype, (tuple, list)) and
413 if isinstance(self._mimetype, (tuple, list)) and len(self._mimetype) == 2:
507 len(self._mimetype) == 2):
508 return self._mimetype
414 return self._mimetype
509 else:
415 else:
510 raise NodeError('given _mimetype attribute must be an 2 '
416 raise NodeError("given _mimetype attribute must be an 2 element list or tuple")
511 'element list or tuple')
512
417
513 db = get_mimetypes_db()
418 db = get_mimetypes_db()
514 mtype, encoding = db.guess_type(self.name)
419 mtype, encoding = db.guess_type(self.name)
515
420
516 if mtype is None:
421 if mtype is None:
517 if not self.is_largefile() and self.is_binary:
422 if not self.is_largefile() and self.is_binary:
518 mtype = 'application/octet-stream'
423 mtype = "application/octet-stream"
519 encoding = None
424 encoding = None
520 else:
425 else:
521 mtype = 'text/plain'
426 mtype = "text/plain"
522 encoding = None
427 encoding = None
523
428
524 # try with pygments
429 # try with pygments
525 try:
430 try:
526 from pygments.lexers import get_lexer_for_filename
431 from pygments.lexers import get_lexer_for_filename
432
527 mt = get_lexer_for_filename(self.name).mimetypes
433 mt = get_lexer_for_filename(self.name).mimetypes
528 except Exception:
434 except Exception:
529 mt = None
435 mt = None
@@ -544,18 +450,17 b' class FileNode(Node):'
544
450
545 @LazyProperty
451 @LazyProperty
546 def mimetype_main(self):
452 def mimetype_main(self):
547 return self.mimetype.split('/')[0]
453 return self.mimetype.split("/")[0]
548
454
549 @classmethod
455 @classmethod
550 def get_lexer(cls, filename, content=None):
456 def get_lexer(cls, filename, content=None):
551 from pygments import lexers
457 from pygments import lexers
552
458
553 extension = filename.split('.')[-1]
459 extension = filename.split(".")[-1]
554 lexer = None
460 lexer = None
555
461
556 try:
462 try:
557 lexer = lexers.guess_lexer_for_filename(
463 lexer = lexers.guess_lexer_for_filename(filename, content, stripnl=False)
558 filename, content, stripnl=False)
559 except lexers.ClassNotFound:
464 except lexers.ClassNotFound:
560 pass
465 pass
561
466
@@ -580,7 +485,7 b' class FileNode(Node):'
580 content, name and mimetype.
485 content, name and mimetype.
581 """
486 """
582 # TODO: this is more proper, but super heavy on investigating the type based on the content
487 # TODO: this is more proper, but super heavy on investigating the type based on the content
583 #self.get_lexer(self.name, self.content)
488 # self.get_lexer(self.name, self.content)
584
489
585 return self.get_lexer(self.name)
490 return self.get_lexer(self.name)
586
491
@@ -597,8 +502,8 b' class FileNode(Node):'
597 Returns a list of commit for this file in which the file was changed
502 Returns a list of commit for this file in which the file was changed
598 """
503 """
599 if self.commit is None:
504 if self.commit is None:
600 raise NodeError('Unable to get commit for this FileNode')
505 raise NodeError("Unable to get commit for this FileNode")
601 return self.commit.get_path_history(self.path)
506 return self.commit.get_path_history(self.bytes_path)
602
507
603 @LazyProperty
508 @LazyProperty
604 def annotate(self):
509 def annotate(self):
@@ -606,22 +511,9 b' class FileNode(Node):'
606 Returns a list of three element tuples with lineno, commit and line
511 Returns a list of three element tuples with lineno, commit and line
607 """
512 """
608 if self.commit is None:
513 if self.commit is None:
609 raise NodeError('Unable to get commit for this FileNode')
514 raise NodeError("Unable to get commit for this FileNode")
610 pre_load = ["author", "date", "message", "parents"]
515 pre_load = ["author", "date", "message", "parents"]
611 return self.commit.get_file_annotate(self.path, pre_load=pre_load)
516 return self.commit.get_file_annotate(self.bytes_path, pre_load=pre_load)
612
613 @LazyProperty
614 def state(self):
615 if not self.commit:
616 raise NodeError(
617 "Cannot check state of the node if it's not "
618 "linked with commit")
619 elif self.path in (node.path for node in self.commit.added):
620 return NodeState.ADDED
621 elif self.path in (node.path for node in self.commit.changed):
622 return NodeState.CHANGED
623 else:
624 return NodeState.NOT_CHANGED
625
517
626 @LazyProperty
518 @LazyProperty
627 def is_binary(self):
519 def is_binary(self):
@@ -629,7 +521,7 b' class FileNode(Node):'
629 Returns True if file has binary content.
521 Returns True if file has binary content.
630 """
522 """
631 if self.commit:
523 if self.commit:
632 return self.commit.is_node_binary(self.path)
524 return self.commit.is_node_binary(self.bytes_path)
633 else:
525 else:
634 raw_bytes = self._content
526 raw_bytes = self._content
635 return bool(raw_bytes and BIN_BYTE_MARKER in raw_bytes)
527 return bool(raw_bytes and BIN_BYTE_MARKER in raw_bytes)
@@ -641,7 +533,7 b' class FileNode(Node):'
641 """
533 """
642
534
643 if self.commit:
535 if self.commit:
644 return self.commit.node_md5_hash(self.path)
536 return self.commit.node_md5_hash(self.bytes_path)
645 else:
537 else:
646 raw_bytes = self._content
538 raw_bytes = self._content
647 # TODO: this sucks, we're computing md5 on potentially super big stream data...
539 # TODO: this sucks, we're computing md5 on potentially super big stream data...
@@ -650,7 +542,7 b' class FileNode(Node):'
650 @LazyProperty
542 @LazyProperty
651 def extension(self):
543 def extension(self):
652 """Returns filenode extension"""
544 """Returns filenode extension"""
653 return self.name.split('.')[-1]
545 return self.name.split(".")[-1]
654
546
655 @property
547 @property
656 def is_executable(self):
548 def is_executable(self):
@@ -667,15 +559,15 b' class FileNode(Node):'
667 LF store.
559 LF store.
668 """
560 """
669 if self.commit:
561 if self.commit:
670 return self.commit.get_largefile_node(self.path)
562 return self.commit.get_largefile_node(self.bytes_path)
671
563
672 def count_lines(self, content: str | bytes, count_empty=False):
564 def count_lines(self, content: str | bytes, count_empty=False):
673 if isinstance(content, str):
565 if isinstance(content, str):
674 newline_marker = '\n'
566 newline_marker = "\n"
675 elif isinstance(content, bytes):
567 elif isinstance(content, bytes):
676 newline_marker = b'\n'
568 newline_marker = b"\n"
677 else:
569 else:
678 raise ValueError('content must be bytes or str got {type(content)} instead')
570 raise ValueError("content must be bytes or str got {type(content)} instead")
679
571
680 if count_empty:
572 if count_empty:
681 all_lines = 0
573 all_lines = 0
@@ -704,33 +596,6 b' class FileNode(Node):'
704 return all_lines, empty_lines
596 return all_lines, empty_lines
705
597
706
598
707 class RemovedFileNode(FileNode):
708 """
709 Dummy FileNode class - trying to access any public attribute except path,
710 name, kind or state (or methods/attributes checking those two) would raise
711 RemovedFileNodeError.
712 """
713 ALLOWED_ATTRIBUTES = [
714 'name', 'path', 'state', 'is_root', 'is_file', 'is_dir', 'kind',
715 'added', 'changed', 'not_changed', 'removed', 'bytes_path'
716 ]
717
718 def __init__(self, path):
719 """
720 :param path: relative path to the node
721 """
722 super().__init__(path=path)
723
724 def __getattribute__(self, attr):
725 if attr.startswith('_') or attr in RemovedFileNode.ALLOWED_ATTRIBUTES:
726 return super().__getattribute__(attr)
727 raise RemovedFileNodeError(f"Cannot access attribute {attr} on RemovedFileNode. Not in allowed attributes")
728
729 @LazyProperty
730 def state(self):
731 return NodeState.REMOVED
732
733
734 class DirNode(Node):
599 class DirNode(Node):
735 """
600 """
736 DirNode stores list of files and directories within this node.
601 DirNode stores list of files and directories within this node.
@@ -752,7 +617,7 b' class DirNode(Node):'
752 super().__init__(path, NodeKind.DIR)
617 super().__init__(path, NodeKind.DIR)
753 self.commit = commit
618 self.commit = commit
754 self._nodes = nodes
619 self._nodes = nodes
755 self.default_pre_load = default_pre_load or ['is_binary', 'size']
620 self.default_pre_load = default_pre_load or ["is_binary", "size"]
756
621
757 def __iter__(self):
622 def __iter__(self):
758 yield from self.nodes
623 yield from self.nodes
@@ -782,10 +647,9 b' class DirNode(Node):'
782 @LazyProperty
647 @LazyProperty
783 def nodes(self):
648 def nodes(self):
784 if self.commit:
649 if self.commit:
785 nodes = self.commit.get_nodes(self.path, pre_load=self.default_pre_load)
650 nodes = self.commit.get_nodes(self.bytes_path, pre_load=self.default_pre_load)
786 else:
651 else:
787 nodes = self._nodes
652 nodes = self._nodes
788 self._nodes_dict = {node.path: node for node in nodes}
789 return sorted(nodes)
653 return sorted(nodes)
790
654
791 @LazyProperty
655 @LazyProperty
@@ -796,47 +660,6 b' class DirNode(Node):'
796 def dirs(self):
660 def dirs(self):
797 return sorted(node for node in self.nodes if node.is_dir())
661 return sorted(node for node in self.nodes if node.is_dir())
798
662
799 def get_node(self, path):
800 """
801 Returns node from within this particular ``DirNode``, so it is now
802 allowed to fetch, i.e. node located at 'docs/api/index.rst' from node
803 'docs'. In order to access deeper nodes one must fetch nodes between
804 them first - this would work::
805
806 docs = root.get_node('docs')
807 docs.get_node('api').get_node('index.rst')
808
809 :param: path - relative to the current node
810
811 .. note::
812 To access lazily (as in example above) node have to be initialized
813 with related commit object - without it node is out of
814 context and may know nothing about anything else than nearest
815 (located at same level) nodes.
816 """
817 try:
818 path = path.rstrip('/')
819 if path == '':
820 raise NodeError("Cannot retrieve node without path")
821 self.nodes # access nodes first in order to set _nodes_dict
822 paths = path.split('/')
823 if len(paths) == 1:
824 if not self.is_root():
825 path = '/'.join((self.path, paths[0]))
826 else:
827 path = paths[0]
828 return self._nodes_dict[path]
829 elif len(paths) > 1:
830 if self.commit is None:
831 raise NodeError("Cannot access deeper nodes without commit")
832 else:
833 path1, path2 = paths[0], '/'.join(paths[1:])
834 return self.get_node(path1).get_node(path2)
835 else:
836 raise KeyError
837 except KeyError:
838 raise NodeError(f"Node does not exist at {path}")
839
840 @LazyProperty
663 @LazyProperty
841 def state(self):
664 def state(self):
842 raise NodeError("Cannot access state of DirNode")
665 raise NodeError("Cannot access state of DirNode")
@@ -844,7 +667,7 b' class DirNode(Node):'
844 @LazyProperty
667 @LazyProperty
845 def size(self):
668 def size(self):
846 size = 0
669 size = 0
847 for root, dirs, files in self.commit.walk(self.path):
670 for root, dirs, files in self.commit.walk(self.bytes_path):
848 for f in files:
671 for f in files:
849 size += f.size
672 size += f.size
850
673
@@ -854,14 +677,12 b' class DirNode(Node):'
854 def last_commit(self):
677 def last_commit(self):
855 if self.commit:
678 if self.commit:
856 pre_load = ["author", "date", "message", "parents"]
679 pre_load = ["author", "date", "message", "parents"]
857 return self.commit.get_path_commit(self.path, pre_load=pre_load)
680 return self.commit.get_path_commit(self.bytes_path, pre_load=pre_load)
858 raise NodeError(
681 raise NodeError("Cannot retrieve last commit of the file without related commit attribute")
859 "Cannot retrieve last commit of the file without "
860 "related commit attribute")
861
682
862 def __repr__(self):
683 def __repr__(self):
863 short_id = getattr(self.commit, 'short_id', '')
684 short_id = getattr(self.commit, "short_id", "")
864 return f'<{self.__class__.__name__} {self.path!r} @ {short_id}>'
685 return f"<{self.__class__.__name__} path={self.str_path!r}, short_id={short_id}>"
865
686
866
687
867 class RootNode(DirNode):
688 class RootNode(DirNode):
@@ -870,21 +691,24 b' class RootNode(DirNode):'
870 """
691 """
871
692
872 def __init__(self, nodes=(), commit=None):
693 def __init__(self, nodes=(), commit=None):
873 super().__init__(path=b'', nodes=nodes, commit=commit)
694 super().__init__(path=b"", nodes=nodes, commit=commit)
874
695
875 def __repr__(self):
696 def __repr__(self):
876 return f'<{self.__class__.__name__}>'
697 short_id = getattr(self.commit, "short_id", "")
698 return f"<{self.__class__.__name__} path={self.str_path!r}, short_id={short_id}>"
877
699
878
700
879 class SubModuleNode(Node):
701 class SubModuleNode(Node):
880 """
702 """
881 represents a SubModule of Git or SubRepo of Mercurial
703 represents a SubModule of Git or SubRepo of Mercurial
882 """
704 """
705
883 is_binary = False
706 is_binary = False
884 size = 0
707 size = 0
885
708
886 def __init__(self, name, url=None, commit=None, alias=None):
709 def __init__(self, name, url=None, commit=None, alias=None):
887 self.path = name
710 self.path = name
711 self.str_path: str = safe_str(self.path) # we store paths as str
888 self.kind = NodeKind.SUBMODULE
712 self.kind = NodeKind.SUBMODULE
889 self.alias = alias
713 self.alias = alias
890
714
@@ -894,8 +718,8 b' class SubModuleNode(Node):'
894 self.url = url or self._extract_submodule_url()
718 self.url = url or self._extract_submodule_url()
895
719
896 def __repr__(self):
720 def __repr__(self):
897 short_id = getattr(self.commit, 'short_id', '')
721 short_id = getattr(self.commit, "short_id", "")
898 return f'<{self.__class__.__name__} {self.path!r} @ {short_id}>'
722 return f"<{self.__class__.__name__} {self.str_path!r} @ {short_id}>"
899
723
900 def _extract_submodule_url(self):
724 def _extract_submodule_url(self):
901 # TODO: find a way to parse gits submodule file and extract the
725 # TODO: find a way to parse gits submodule file and extract the
@@ -908,22 +732,22 b' class SubModuleNode(Node):'
908 Returns name of the node so if its path
732 Returns name of the node so if its path
909 then only last part is returned.
733 then only last part is returned.
910 """
734 """
911 org = safe_str(self.path.rstrip('/').split('/')[-1])
735 org = self.str_path.rstrip("/").split("/")[-1]
912 return f'{org} @ {self.commit.short_id}'
736 return f"{org} @ {self.commit.short_id}"
913
737
914
738
915 class LargeFileNode(FileNode):
739 class LargeFileNode(FileNode):
916
917 def __init__(self, path, url=None, commit=None, alias=None, org_path=None):
740 def __init__(self, path, url=None, commit=None, alias=None, org_path=None):
918 self._validate_path(path) # can throw exception if path is invalid
741 self._validate_path(path) # can throw exception if path is invalid
919 self.org_path = org_path # as stored in VCS as LF pointer
742 self.org_path = org_path # as stored in VCS as LF pointer
920
743
921 self.bytes_path = path.rstrip(b'/') # store for __repr__
744 self.bytes_path = path.rstrip(b"/") # store for __repr__
922 self.path = safe_str(self.bytes_path) # we store paths as str
745 self.str_path = safe_str(self.bytes_path)
746 self.path = self.str_path
923
747
924 self.kind = NodeKind.LARGEFILE
748 self.kind = NodeKind.LARGE_FILE
925 self.alias = alias
749 self.alias = alias
926 self._content = b''
750 self._content = b""
927
751
928 def _validate_path(self, path: bytes):
752 def _validate_path(self, path: bytes):
929 """
753 """
@@ -932,7 +756,7 b' class LargeFileNode(FileNode):'
932 self._assert_bytes(path)
756 self._assert_bytes(path)
933
757
934 def __repr__(self):
758 def __repr__(self):
935 return f'<{self.__class__.__name__} {self.org_path} -> {self.path!r}>'
759 return f"<{self.__class__.__name__} {self.org_path} -> {self.str_path!r}>"
936
760
937 @LazyProperty
761 @LazyProperty
938 def size(self):
762 def size(self):
@@ -940,7 +764,7 b' class LargeFileNode(FileNode):'
940
764
941 @LazyProperty
765 @LazyProperty
942 def raw_bytes(self):
766 def raw_bytes(self):
943 with open(self.path, 'rb') as f:
767 with open(self.path, "rb") as f:
944 content = f.read()
768 content = f.read()
945 return content
769 return content
946
770
@@ -952,7 +776,7 b' class LargeFileNode(FileNode):'
952 return self.org_path
776 return self.org_path
953
777
954 def stream_bytes(self):
778 def stream_bytes(self):
955 with open(self.path, 'rb') as stream:
779 with open(self.path, "rb") as stream:
956 while True:
780 while True:
957 data = stream.read(16 * 1024)
781 data = stream.read(16 * 1024)
958 if not data:
782 if not data:
@@ -103,7 +103,7 b' class GistModel(BaseModel):'
103 raise VCSError(f'Failed to load gist repository for {repo}')
103 raise VCSError(f'Failed to load gist repository for {repo}')
104
104
105 commit = vcs_repo.get_commit(commit_id=revision)
105 commit = vcs_repo.get_commit(commit_id=revision)
106 return commit, [n for n in commit.get_node('/')]
106 return commit, [n for n in commit.get_node(b'/')]
107
107
108 def create(self, description, owner, gist_mapping,
108 def create(self, description, owner, gist_mapping,
109 gist_type=Gist.GIST_PUBLIC, lifetime=-1, gist_id=None,
109 gist_type=Gist.GIST_PUBLIC, lifetime=-1, gist_id=None,
@@ -178,6 +178,7 b' def get_diff_info('
178 log.debug('Calculating authors of changed files')
178 log.debug('Calculating authors of changed files')
179 target_commit = source_repo.get_commit(ancestor_id)
179 target_commit = source_repo.get_commit(ancestor_id)
180
180
181 # TODO: change to operate in bytes..
181 for fname, lines in changed_lines.items():
182 for fname, lines in changed_lines.items():
182
183
183 try:
184 try:
@@ -2223,8 +2224,7 b' class MergeCheck(object):'
2223 )
2224 )
2224
2225
2225 @classmethod
2226 @classmethod
2226 def validate(cls, pull_request, auth_user, translator, fail_early=False,
2227 def validate(cls, pull_request, auth_user, translator, fail_early=False, force_shadow_repo_refresh=False):
2227 force_shadow_repo_refresh=False):
2228 _ = translator
2228 _ = translator
2229 merge_check = cls()
2229 merge_check = cls()
2230
2230
@@ -2285,12 +2285,10 b' class MergeCheck(object):'
2285 # left over TODOs
2285 # left over TODOs
2286 todos = CommentsModel().get_pull_request_unresolved_todos(pull_request)
2286 todos = CommentsModel().get_pull_request_unresolved_todos(pull_request)
2287 if todos:
2287 if todos:
2288 log.debug("MergeCheck: cannot merge, {} "
2288 log.debug("MergeCheck: cannot merge, %s unresolved TODOs left.", len(todos))
2289 "unresolved TODOs left.".format(len(todos)))
2290
2289
2291 if len(todos) == 1:
2290 if len(todos) == 1:
2292 msg = _('Cannot merge, {} TODO still not resolved.').format(
2291 msg = _('Cannot merge, {} TODO still not resolved.').format(len(todos))
2293 len(todos))
2294 else:
2292 else:
2295 msg = _('Cannot merge, {} TODOs still not resolved.').format(
2293 msg = _('Cannot merge, {} TODOs still not resolved.').format(
2296 len(todos))
2294 len(todos))
@@ -33,6 +33,7 b' from rhodecode.lib.auth import HasUserGr'
33 from rhodecode.lib.caching_query import FromCache
33 from rhodecode.lib.caching_query import FromCache
34 from rhodecode.lib.exceptions import AttachedForksError, AttachedPullRequestsError, AttachedArtifactsError
34 from rhodecode.lib.exceptions import AttachedForksError, AttachedPullRequestsError, AttachedArtifactsError
35 from rhodecode.lib import hooks_base
35 from rhodecode.lib import hooks_base
36 from rhodecode.lib.str_utils import safe_bytes
36 from rhodecode.lib.user_log_filter import user_log_filter
37 from rhodecode.lib.user_log_filter import user_log_filter
37 from rhodecode.lib.utils import make_db_config
38 from rhodecode.lib.utils import make_db_config
38 from rhodecode.lib.utils2 import (
39 from rhodecode.lib.utils2 import (
@@ -1109,47 +1110,47 b' class ReadmeFinder:'
1109 different.
1110 different.
1110 """
1111 """
1111
1112
1112 readme_re = re.compile(r'^readme(\.[^\.]+)?$', re.IGNORECASE)
1113 readme_re = re.compile(br'^readme(\.[^.]+)?$', re.IGNORECASE)
1113 path_re = re.compile(r'^docs?', re.IGNORECASE)
1114 path_re = re.compile(br'^docs?', re.IGNORECASE)
1114
1115
1115 default_priorities = {
1116 default_priorities = {
1116 None: 0,
1117 None: 0,
1117 '.rst': 1,
1118 b'.rst': 1,
1118 '.md': 1,
1119 b'.md': 1,
1119 '.rest': 2,
1120 b'.rest': 2,
1120 '.mkdn': 2,
1121 b'.mkdn': 2,
1121 '.text': 2,
1122 b'.text': 2,
1122 '.txt': 3,
1123 b'.txt': 3,
1123 '.mdown': 3,
1124 b'.mdown': 3,
1124 '.markdown': 4,
1125 b'.markdown': 4,
1125 }
1126 }
1126
1127
1127 path_priority = {
1128 path_priority = {
1128 'doc': 0,
1129 b'doc': 0,
1129 'docs': 1,
1130 b'docs': 1,
1130 }
1131 }
1131
1132
1132 FALLBACK_PRIORITY = 99
1133 FALLBACK_PRIORITY = 99
1133
1134
1134 RENDERER_TO_EXTENSION = {
1135 RENDERER_TO_EXTENSION = {
1135 'rst': ['.rst', '.rest'],
1136 'rst': [b'.rst', b'.rest'],
1136 'markdown': ['.md', 'mkdn', '.mdown', '.markdown'],
1137 'markdown': [b'.md', b'mkdn', b'.mdown', b'.markdown'],
1137 }
1138 }
1138
1139
1139 def __init__(self, default_renderer=None):
1140 def __init__(self, default_renderer=None):
1140 self._default_renderer = default_renderer
1141 self._default_renderer = default_renderer
1141 self._renderer_extensions = self.RENDERER_TO_EXTENSION.get(
1142 self._renderer_extensions = self.RENDERER_TO_EXTENSION.get(default_renderer, [])
1142 default_renderer, [])
1143
1143
1144 def search(self, commit, path='/'):
1144 def search(self, commit, path=b'/'):
1145 """
1145 """
1146 Find a readme in the given `commit`.
1146 Find a readme in the given `commit`.
1147 """
1147 """
1148 # firstly, check the PATH type if it is actually a DIR
1148 # firstly, check the PATH type if it is actually a DIR
1149 if commit.get_node(path).kind != NodeKind.DIR:
1149 bytes_path = safe_bytes(path)
1150 if commit.get_node(bytes_path).kind != NodeKind.DIR:
1150 return None
1151 return None
1151
1152
1152 nodes = commit.get_nodes(path)
1153 nodes = commit.get_nodes(bytes_path)
1153 matches = self._match_readmes(nodes)
1154 matches = self._match_readmes(nodes)
1154 matches = self._sort_according_to_priority(matches)
1155 matches = self._sort_according_to_priority(matches)
1155 if matches:
1156 if matches:
@@ -1157,8 +1158,8 b' class ReadmeFinder:'
1157
1158
1158 paths = self._match_paths(nodes)
1159 paths = self._match_paths(nodes)
1159 paths = self._sort_paths_according_to_priority(paths)
1160 paths = self._sort_paths_according_to_priority(paths)
1160 for path in paths:
1161 for bytes_path in paths:
1161 match = self.search(commit, path=path)
1162 match = self.search(commit, path=bytes_path)
1162 if match:
1163 if match:
1163 return match
1164 return match
1164
1165
@@ -1168,7 +1169,7 b' class ReadmeFinder:'
1168 for node in nodes:
1169 for node in nodes:
1169 if not node.is_file():
1170 if not node.is_file():
1170 continue
1171 continue
1171 path = node.path.rsplit('/', 1)[-1]
1172 path = node.bytes_path.rsplit(b'/', 1)[-1]
1172 match = self.readme_re.match(path)
1173 match = self.readme_re.match(path)
1173 if match:
1174 if match:
1174 extension = match.group(1)
1175 extension = match.group(1)
@@ -1178,28 +1179,26 b' class ReadmeFinder:'
1178 for node in nodes:
1179 for node in nodes:
1179 if not node.is_dir():
1180 if not node.is_dir():
1180 continue
1181 continue
1181 match = self.path_re.match(node.path)
1182 match = self.path_re.match(node.bytes_path)
1182 if match:
1183 if match:
1183 yield node.path
1184 yield node.bytes_path
1184
1185
1185 def _priority(self, extension):
1186 def _priority(self, extension):
1186 renderer_priority = (
1187 renderer_priority = 0 if extension in self._renderer_extensions else 1
1187 0 if extension in self._renderer_extensions else 1)
1188 extension_priority = self.default_priorities.get(extension, self.FALLBACK_PRIORITY)
1188 extension_priority = self.default_priorities.get(
1189 return renderer_priority, extension_priority
1189 extension, self.FALLBACK_PRIORITY)
1190 return (renderer_priority, extension_priority)
1191
1190
1192 def _sort_according_to_priority(self, matches):
1191 def _sort_according_to_priority(self, matches):
1193
1192
1194 def priority_and_path(match):
1193 def priority_and_path(match):
1195 return (match.priority, match.path)
1194 return match.priority, match.path
1196
1195
1197 return sorted(matches, key=priority_and_path)
1196 return sorted(matches, key=priority_and_path)
1198
1197
1199 def _sort_paths_according_to_priority(self, paths):
1198 def _sort_paths_according_to_priority(self, paths):
1200
1199
1201 def priority_and_path(path):
1200 def priority_and_path(path):
1202 return (self.path_priority.get(path, self.FALLBACK_PRIORITY), path)
1201 return self.path_priority.get(path, self.FALLBACK_PRIORITY), path
1203
1202
1204 return sorted(paths, key=priority_and_path)
1203 return sorted(paths, key=priority_and_path)
1205
1204
@@ -543,7 +543,7 b' class ScmModel(BaseModel):'
543 root_path = root_path.lstrip('/')
543 root_path = root_path.lstrip('/')
544
544
545 # get RootNode, inject pre-load options before walking
545 # get RootNode, inject pre-load options before walking
546 top_node = commit.get_node(root_path)
546 top_node = commit.get_node(safe_bytes(root_path))
547 extended_info_pre_load = []
547 extended_info_pre_load = []
548 if extended_info:
548 if extended_info:
549 extended_info_pre_load += ['md5']
549 extended_info_pre_load += ['md5']
@@ -614,12 +614,13 b' class ScmModel(BaseModel):'
614
614
615 _files = list()
615 _files = list()
616 _dirs = list()
616 _dirs = list()
617 bytes_path = safe_bytes(root_path)
617 try:
618 try:
618 _repo = self._get_repo(repo_name)
619 _repo = self._get_repo(repo_name)
619 commit = _repo.scm_instance().get_commit(commit_id=commit_id)
620 commit = _repo.scm_instance().get_commit(commit_id=commit_id)
620 root_path = root_path.lstrip('/')
621 root_path = bytes_path.lstrip(b'/')
621
622
622 top_node = commit.get_node(root_path)
623 top_node = commit.get_node(safe_bytes(root_path))
623 top_node.default_pre_load = []
624 top_node.default_pre_load = []
624
625
625 for __, dirs, files in commit.walk(top_node):
626 for __, dirs, files in commit.walk(top_node):
@@ -736,7 +737,7 b' class ScmModel(BaseModel):'
736 _repo = self._get_repo(repo_name)
737 _repo = self._get_repo(repo_name)
737 commit = _repo.scm_instance().get_commit(commit_id=commit_id)
738 commit = _repo.scm_instance().get_commit(commit_id=commit_id)
738 root_path = root_path.lstrip('/')
739 root_path = root_path.lstrip('/')
739 top_node = commit.get_node(root_path)
740 top_node = commit.get_node(safe_bytes(root_path))
740 top_node.default_pre_load = []
741 top_node.default_pre_load = []
741
742
742 for __, dirs, files in commit.walk(top_node):
743 for __, dirs, files in commit.walk(top_node):
@@ -774,7 +775,7 b' class ScmModel(BaseModel):'
774 only for git
775 only for git
775 :param trigger_push_hook: trigger push hooks
776 :param trigger_push_hook: trigger push hooks
776
777
777 :returns: new committed commit
778 :returns: new commit
778 """
779 """
779 user, scm_instance, message, commiter, author, imc = self.initialize_inmemory_vars(
780 user, scm_instance, message, commiter, author, imc = self.initialize_inmemory_vars(
780 user, repo, message, author)
781 user, repo, message, author)
@@ -36,8 +36,8 b' connection_available = pytest.mark.skipi'
36
36
37
37
38 import requests
38 import requests
39 from urllib3.util.retry import Retry
39 from requests.adapters import HTTPAdapter
40 from requests.adapters import HTTPAdapter
40 from requests.packages.urllib3.util.retry import Retry
41
41
42
42
43 def requests_retry_session(
43 def requests_retry_session(
@@ -92,8 +92,8 b' class TestRepoModel(object):'
92 is not None)
92 is not None)
93
93
94 @pytest.mark.parametrize("filename, expected", [
94 @pytest.mark.parametrize("filename, expected", [
95 ("README", True),
95 (b"README", True),
96 ("README.rst", False),
96 (b"README.rst", False),
97 ])
97 ])
98 def test_filenode_is_link(self, vcsbackend, filename, expected):
98 def test_filenode_is_link(self, vcsbackend, filename, expected):
99 repo = vcsbackend.repo
99 repo = vcsbackend.repo
@@ -28,7 +28,7 b' import pytest'
28
28
29 import rhodecode
29 import rhodecode
30 from rhodecode.lib.archive_cache import get_archival_config
30 from rhodecode.lib.archive_cache import get_archival_config
31 from rhodecode.lib.str_utils import ascii_bytes
31 from rhodecode.lib.str_utils import ascii_bytes, safe_bytes, safe_str
32 from rhodecode.lib.vcs.backends import base
32 from rhodecode.lib.vcs.backends import base
33 from rhodecode.lib.vcs.exceptions import ImproperArchiveTypeError, VCSError
33 from rhodecode.lib.vcs.exceptions import ImproperArchiveTypeError, VCSError
34 from rhodecode.lib.vcs.nodes import FileNode
34 from rhodecode.lib.vcs.nodes import FileNode
@@ -80,8 +80,8 b' class TestArchives(BackendTestMixin):'
80 out_file.close()
80 out_file.close()
81
81
82 for x in range(5):
82 for x in range(5):
83 node_path = "%d/file_%d.txt" % (x, x)
83 node_path = b"%d/file_%d.txt" % (x, x)
84 with open(os.path.join(out_dir, "repo/" + node_path), "rb") as f:
84 with open(os.path.join(safe_bytes(str(out_dir)), b"repo/" + node_path), "rb") as f:
85 file_content = f.read()
85 file_content = f.read()
86 assert file_content == self.tip.get_node(node_path).content
86 assert file_content == self.tip.get_node(node_path).content
87
87
@@ -120,8 +120,9 b' class TestArchives(BackendTestMixin):'
120 zip_file = zipfile.ZipFile(str(archive_lnk))
120 zip_file = zipfile.ZipFile(str(archive_lnk))
121
121
122 for x in range(5):
122 for x in range(5):
123 node_path = "%d/file_%d.txt" % (x, x)
123 node_path = b"%d/file_%d.txt" % (x, x)
124 data = zip_file.read(f"repo/{node_path}")
124 # NOTE: zipfile operates only on strings inside the archive
125 data = zip_file.read(safe_str(b"repo/%s" % node_path))
125
126
126 decompressed = io.BytesIO()
127 decompressed = io.BytesIO()
127 decompressed.write(data)
128 decompressed.write(data)
@@ -143,8 +144,9 b' class TestArchives(BackendTestMixin):'
143 assert b"commit_id:%b" % raw_id in metafile
144 assert b"commit_id:%b" % raw_id in metafile
144
145
145 for x in range(5):
146 for x in range(5):
146 node_path = "%d/file_%d.txt" % (x, x)
147 node_path = b"%d/file_%d.txt" % (x, x)
147 data = zip_file.read(f"repo/{node_path}")
148 # NOTE: zipfile operates only on strings inside the archive
149 data = zip_file.read(safe_str(b"repo/%s" % node_path))
148 decompressed = io.BytesIO()
150 decompressed = io.BytesIO()
149 decompressed.write(data)
151 decompressed.write(data)
150 assert decompressed.getvalue() == self.tip.get_node(node_path).content
152 assert decompressed.getvalue() == self.tip.get_node(node_path).content
@@ -22,21 +22,17 b' import time'
22 import pytest
22 import pytest
23
23
24 from rhodecode.lib.str_utils import safe_bytes
24 from rhodecode.lib.str_utils import safe_bytes
25 from rhodecode.lib.vcs.backends.base import CollectionGenerator, FILEMODE_DEFAULT, EmptyCommit
25 from rhodecode.lib.vcs.backends.base import CollectionGenerator, EmptyCommit
26 from rhodecode.lib.vcs.exceptions import (
26 from rhodecode.lib.vcs.exceptions import (
27 BranchDoesNotExistError,
27 BranchDoesNotExistError,
28 CommitDoesNotExistError,
28 CommitDoesNotExistError,
29 RepositoryError,
29 RepositoryError,
30 EmptyRepositoryError,
30 EmptyRepositoryError,
31 )
31 )
32 from rhodecode.lib.vcs.nodes import (
32 from rhodecode.lib.vcs.nodes import FileNode
33 FileNode,
34 AddedFileNodesGenerator,
35 ChangedFileNodesGenerator,
36 RemovedFileNodesGenerator,
37 )
38 from rhodecode.tests import get_new_dir
33 from rhodecode.tests import get_new_dir
39 from rhodecode.tests.vcs.conftest import BackendTestMixin
34 from rhodecode.tests.vcs.conftest import BackendTestMixin
35 from rhodecode.lib.vcs_common import NodeKind, FILEMODE_EXECUTABLE, FILEMODE_DEFAULT, FILEMODE_LINK
40
36
41
37
42 class TestBaseChangeset(object):
38 class TestBaseChangeset(object):
@@ -70,7 +66,7 b' class TestCommitsInNonEmptyRepo(BackendT'
70 }
66 }
71
67
72 def test_walk_returns_empty_list_in_case_of_file(self):
68 def test_walk_returns_empty_list_in_case_of_file(self):
73 result = list(self.tip.walk("file_0.txt"))
69 result = list(self.tip.walk(b"file_0.txt"))
74 assert result == []
70 assert result == []
75
71
76 @pytest.mark.backends("git", "hg")
72 @pytest.mark.backends("git", "hg")
@@ -319,7 +315,7 b' class TestCommits(BackendTestMixin):'
319
315
320 def test_get_path_commit(self):
316 def test_get_path_commit(self):
321 commit = self.repo.get_commit()
317 commit = self.repo.get_commit()
322 commit.get_path_commit("file_4.txt")
318 commit.get_path_commit(b"file_4.txt")
323 assert commit.message == "Commit 4"
319 assert commit.message == "Commit 4"
324
320
325 def test_get_filenodes_generator(self):
321 def test_get_filenodes_generator(self):
@@ -500,8 +496,8 b' class TestCommits(BackendTestMixin):'
500 @pytest.mark.parametrize(
496 @pytest.mark.parametrize(
501 "filename, expected",
497 "filename, expected",
502 [
498 [
503 ("README.rst", False),
499 (b"README.rst", False),
504 ("README", True),
500 (b"README", True),
505 ],
501 ],
506 )
502 )
507 def test_commit_is_link(vcsbackend, filename, expected):
503 def test_commit_is_link(vcsbackend, filename, expected):
@@ -543,49 +539,41 b' class TestCommitsChanges(BackendTestMixi'
543
539
544 def test_initial_commit(self, local_dt_to_utc):
540 def test_initial_commit(self, local_dt_to_utc):
545 commit = self.repo.get_commit(commit_idx=0)
541 commit = self.repo.get_commit(commit_idx=0)
546 assert set(commit.added) == {
542 assert sorted(commit.added_paths) == sorted([b"foo/bar", b"foo/ba\xc5\x82", b"foobar", b"qwe"])
547 commit.get_node("foo/bar"),
543 assert commit.changed_paths == []
548 commit.get_node("foo/baΕ‚"),
544 assert commit.removed_paths == []
549 commit.get_node("foobar"),
545 assert sorted(commit.affected_files) == sorted([b"foo/bar", b"foo/ba\xc5\x82", b"foobar", b"qwe"])
550 commit.get_node("qwe"),
551 }
552 assert set(commit.changed) == set()
553 assert set(commit.removed) == set()
554 assert set(commit.affected_files) == {"foo/bar", "foo/baΕ‚", "foobar", "qwe"}
555 assert commit.date == local_dt_to_utc(datetime.datetime(2010, 1, 1, 20, 0))
546 assert commit.date == local_dt_to_utc(datetime.datetime(2010, 1, 1, 20, 0))
556
547
557 def test_head_added(self):
548 def test_head_added(self):
558 commit = self.repo.get_commit()
549 commit = self.repo.get_commit()
559 assert isinstance(commit.added, AddedFileNodesGenerator)
550
560 assert set(commit.added) == {commit.get_node("fallout")}
551 assert commit.added_paths == [b"fallout"]
561 assert isinstance(commit.changed, ChangedFileNodesGenerator)
552 assert commit.changed_paths == [b"foo/bar", b"foobar"]
562 assert set(commit.changed) == {commit.get_node("foo/bar"), commit.get_node("foobar")}
553 assert commit.removed_paths == [b"qwe"]
563 assert isinstance(commit.removed, RemovedFileNodesGenerator)
564 assert len(commit.removed) == 1
565 assert list(commit.removed)[0].path == "qwe"
566
554
567 def test_get_filemode(self):
555 def test_get_filemode(self):
568 commit = self.repo.get_commit()
556 commit = self.repo.get_commit()
569 assert FILEMODE_DEFAULT == commit.get_file_mode("foo/bar")
557 assert FILEMODE_DEFAULT == commit.get_file_mode(b"foo/bar")
570
558
571 def test_get_filemode_non_ascii(self):
559 def test_get_filemode_non_ascii(self):
572 commit = self.repo.get_commit()
560 commit = self.repo.get_commit()
573 assert FILEMODE_DEFAULT == commit.get_file_mode("foo/baΕ‚")
561 assert FILEMODE_DEFAULT == commit.get_file_mode(b"foo/ba\xc5\x82")
574 assert FILEMODE_DEFAULT == commit.get_file_mode("foo/baΕ‚")
562 assert FILEMODE_DEFAULT == commit.get_file_mode(b"foo/ba\xc5\x82")
575
563
576 def test_get_path_history(self):
564 def test_get_path_history(self):
577 commit = self.repo.get_commit()
565 commit = self.repo.get_commit()
578 history = commit.get_path_history("foo/bar")
566 history = commit.get_path_history(b"foo/bar")
579 assert len(history) == 2
567 assert len(history) == 2
580
568
581 def test_get_path_history_with_limit(self):
569 def test_get_path_history_with_limit(self):
582 commit = self.repo.get_commit()
570 commit = self.repo.get_commit()
583 history = commit.get_path_history("foo/bar", limit=1)
571 history = commit.get_path_history(b"foo/bar", limit=1)
584 assert len(history) == 1
572 assert len(history) == 1
585
573
586 def test_get_path_history_first_commit(self):
574 def test_get_path_history_first_commit(self):
587 commit = self.repo[0]
575 commit = self.repo[0]
588 history = commit.get_path_history("foo/bar")
576 history = commit.get_path_history(b"foo/bar")
589 assert len(history) == 1
577 assert len(history) == 1
590
578
591
579
@@ -28,7 +28,7 b' from rhodecode.lib.utils import make_db_'
28 from rhodecode.lib.vcs.backends.base import Reference
28 from rhodecode.lib.vcs.backends.base import Reference
29 from rhodecode.lib.vcs.backends.git import GitRepository, GitCommit, discover_git_version
29 from rhodecode.lib.vcs.backends.git import GitRepository, GitCommit, discover_git_version
30 from rhodecode.lib.vcs.exceptions import RepositoryError, VCSError, NodeDoesNotExistError
30 from rhodecode.lib.vcs.exceptions import RepositoryError, VCSError, NodeDoesNotExistError
31 from rhodecode.lib.vcs.nodes import NodeKind, FileNode, DirNode, NodeState, SubModuleNode
31 from rhodecode.lib.vcs.nodes import NodeKind, FileNode, DirNode, NodeState, SubModuleNode, RootNode
32 from rhodecode.tests import TEST_GIT_REPO, TEST_GIT_REPO_CLONE, get_new_dir
32 from rhodecode.tests import TEST_GIT_REPO, TEST_GIT_REPO_CLONE, get_new_dir
33 from rhodecode.tests.vcs.conftest import BackendTestMixin
33 from rhodecode.tests.vcs.conftest import BackendTestMixin
34
34
@@ -219,23 +219,38 b' class TestGitRepository(object):'
219 assert init_commit.message == "initial import\n"
219 assert init_commit.message == "initial import\n"
220 assert init_author == "Marcin Kuzminski <marcin@python-blog.com>"
220 assert init_author == "Marcin Kuzminski <marcin@python-blog.com>"
221 assert init_author == init_commit.committer
221 assert init_author == init_commit.committer
222 for path in ("vcs/__init__.py", "vcs/backends/BaseRepository.py", "vcs/backends/__init__.py"):
222 assert sorted(init_commit.added_paths) == sorted(
223 [
224 b"vcs/__init__.py",
225 b"vcs/backends/BaseRepository.py",
226 b"vcs/backends/__init__.py",
227 ]
228 )
229 assert sorted(init_commit.affected_files) == sorted(
230 [
231 b"vcs/__init__.py",
232 b"vcs/backends/BaseRepository.py",
233 b"vcs/backends/__init__.py",
234 ]
235 )
236
237 for path in (b"vcs/__init__.py", b"vcs/backends/BaseRepository.py", b"vcs/backends/__init__.py"):
223 assert isinstance(init_commit.get_node(path), FileNode)
238 assert isinstance(init_commit.get_node(path), FileNode)
224 for path in ("", "vcs", "vcs/backends"):
239 for path in (b"", b"vcs", b"vcs/backends"):
225 assert isinstance(init_commit.get_node(path), DirNode)
240 assert isinstance(init_commit.get_node(path), DirNode)
226
241
227 with pytest.raises(NodeDoesNotExistError):
242 with pytest.raises(NodeDoesNotExistError):
228 init_commit.get_node(path="foobar")
243 init_commit.get_node(path=b"foobar")
229
244
230 node = init_commit.get_node("vcs/")
245 node = init_commit.get_node(b"vcs/")
231 assert hasattr(node, "kind")
246 assert hasattr(node, "kind")
232 assert node.kind == NodeKind.DIR
247 assert node.kind == NodeKind.DIR
233
248
234 node = init_commit.get_node("vcs")
249 node = init_commit.get_node(b"vcs")
235 assert hasattr(node, "kind")
250 assert hasattr(node, "kind")
236 assert node.kind == NodeKind.DIR
251 assert node.kind == NodeKind.DIR
237
252
238 node = init_commit.get_node("vcs/__init__.py")
253 node = init_commit.get_node(b"vcs/__init__.py")
239 assert hasattr(node, "kind")
254 assert hasattr(node, "kind")
240 assert node.kind == NodeKind.FILE
255 assert node.kind == NodeKind.FILE
241
256
@@ -257,7 +272,7 b' Introduction'
257 TODO: To be written...
272 TODO: To be written...
258
273
259 """
274 """
260 node = commit10.get_node("README.rst")
275 node = commit10.get_node(b"README.rst")
261 assert node.kind == NodeKind.FILE
276 assert node.kind == NodeKind.FILE
262 assert node.str_content == README
277 assert node.str_content == README
263
278
@@ -615,7 +630,7 b' class TestGitCommit(object):'
615
630
616 def test_root_node(self):
631 def test_root_node(self):
617 tip = self.repo.get_commit()
632 tip = self.repo.get_commit()
618 assert tip.root is tip.get_node("")
633 assert tip.root is tip.get_node(b"")
619
634
620 def test_lazy_fetch(self):
635 def test_lazy_fetch(self):
621 """
636 """
@@ -633,29 +648,29 b' class TestGitCommit(object):'
633 # accessing root.nodes updates commit.nodes
648 # accessing root.nodes updates commit.nodes
634 assert len(commit.nodes) == 9
649 assert len(commit.nodes) == 9
635
650
636 docs = root.get_node("docs")
651 docs = commit.get_node(b"docs")
637 # we haven't yet accessed anything new as docs dir was already cached
652 # we haven't yet accessed anything new as docs dir was already cached
638 assert len(commit.nodes) == 9
653 assert len(commit.nodes) == 9
639 assert len(docs.nodes) == 8
654 assert len(docs.nodes) == 8
640 # accessing docs.nodes updates commit.nodes
655 # accessing docs.nodes updates commit.nodes
641 assert len(commit.nodes) == 17
656 assert len(commit.nodes) == 17
642
657
643 assert docs is commit.get_node("docs")
658 assert docs is commit.get_node(b"docs")
644 assert docs is root.nodes[0]
659 assert docs is root.nodes[0]
645 assert docs is root.dirs[0]
660 assert docs is root.dirs[0]
646 assert docs is commit.get_node("docs")
661 assert docs is commit.get_node(b"docs")
647
662
648 def test_nodes_with_commit(self):
663 def test_nodes_with_commit(self):
649 commit_id = "2a13f185e4525f9d4b59882791a2d397b90d5ddc"
664 commit_id = "2a13f185e4525f9d4b59882791a2d397b90d5ddc"
650 commit = self.repo.get_commit(commit_id)
665 commit = self.repo.get_commit(commit_id)
651 root = commit.root
666 root = commit.root
652 docs = root.get_node("docs")
667 assert isinstance(root, RootNode)
653 assert docs is commit.get_node("docs")
668 docs = commit.get_node(b"docs")
654 api = docs.get_node("api")
669 assert docs is commit.get_node(b"docs")
655 assert api is commit.get_node("docs/api")
670 api = commit.get_node(b"docs/api")
656 index = api.get_node("index.rst")
671 assert api is commit.get_node(b"docs/api")
657 assert index is commit.get_node("docs/api/index.rst")
672 index = commit.get_node(b"docs/api/index.rst")
658 assert index is commit.get_node("docs").get_node("api").get_node("index.rst")
673 assert index is commit.get_node(b"docs/api/index.rst")
659
674
660 def test_branch_and_tags(self):
675 def test_branch_and_tags(self):
661 """
676 """
@@ -682,12 +697,12 b' class TestGitCommit(object):'
682
697
683 def test_file_size(self):
698 def test_file_size(self):
684 to_check = (
699 to_check = (
685 ("c1214f7e79e02fc37156ff215cd71275450cffc3", "vcs/backends/BaseRepository.py", 502),
700 ("c1214f7e79e02fc37156ff215cd71275450cffc3", b"vcs/backends/BaseRepository.py", 502),
686 ("d7e0d30fbcae12c90680eb095a4f5f02505ce501", "vcs/backends/hg.py", 854),
701 ("d7e0d30fbcae12c90680eb095a4f5f02505ce501", b"vcs/backends/hg.py", 854),
687 ("6e125e7c890379446e98980d8ed60fba87d0f6d1", "setup.py", 1068),
702 ("6e125e7c890379446e98980d8ed60fba87d0f6d1", b"setup.py", 1068),
688 ("d955cd312c17b02143c04fa1099a352b04368118", "vcs/backends/base.py", 2921),
703 ("d955cd312c17b02143c04fa1099a352b04368118", b"vcs/backends/base.py", 2921),
689 ("ca1eb7957a54bce53b12d1a51b13452f95bc7c7e", "vcs/backends/base.py", 3936),
704 ("ca1eb7957a54bce53b12d1a51b13452f95bc7c7e", b"vcs/backends/base.py", 3936),
690 ("f50f42baeed5af6518ef4b0cb2f1423f3851a941", "vcs/backends/base.py", 6189),
705 ("f50f42baeed5af6518ef4b0cb2f1423f3851a941", b"vcs/backends/base.py", 6189),
691 )
706 )
692 for commit_id, path, size in to_check:
707 for commit_id, path, size in to_check:
693 node = self.repo.get_commit(commit_id).get_node(path)
708 node = self.repo.get_commit(commit_id).get_node(path)
@@ -695,17 +710,17 b' class TestGitCommit(object):'
695 assert node.size == size
710 assert node.size == size
696
711
697 def test_file_history_from_commits(self):
712 def test_file_history_from_commits(self):
698 node = self.repo[10].get_node("setup.py")
713 node = self.repo[10].get_node(b"setup.py")
699 commit_ids = [commit.raw_id for commit in node.history]
714 commit_ids = [commit.raw_id for commit in node.history]
700 assert ["ff7ca51e58c505fec0dd2491de52c622bb7a806b"] == commit_ids
715 assert ["ff7ca51e58c505fec0dd2491de52c622bb7a806b"] == commit_ids
701
716
702 node = self.repo[20].get_node("setup.py")
717 node = self.repo[20].get_node(b"setup.py")
703 node_ids = [commit.raw_id for commit in node.history]
718 node_ids = [commit.raw_id for commit in node.history]
704 assert ["191caa5b2c81ed17c0794bf7bb9958f4dcb0b87e", "ff7ca51e58c505fec0dd2491de52c622bb7a806b"] == node_ids
719 assert ["191caa5b2c81ed17c0794bf7bb9958f4dcb0b87e", "ff7ca51e58c505fec0dd2491de52c622bb7a806b"] == node_ids
705
720
706 # special case we check history from commit that has this particular
721 # special case we check history from commit that has this particular
707 # file changed this means we check if it's included as well
722 # file changed this means we check if it's included as well
708 node = self.repo.get_commit("191caa5b2c81ed17c0794bf7bb9958f4dcb0b87e").get_node("setup.py")
723 node = self.repo.get_commit("191caa5b2c81ed17c0794bf7bb9958f4dcb0b87e").get_node(b"setup.py")
709 node_ids = [commit.raw_id for commit in node.history]
724 node_ids = [commit.raw_id for commit in node.history]
710 assert ["191caa5b2c81ed17c0794bf7bb9958f4dcb0b87e", "ff7ca51e58c505fec0dd2491de52c622bb7a806b"] == node_ids
725 assert ["191caa5b2c81ed17c0794bf7bb9958f4dcb0b87e", "ff7ca51e58c505fec0dd2491de52c622bb7a806b"] == node_ids
711
726
@@ -713,7 +728,7 b' class TestGitCommit(object):'
713 # we can only check if those commits are present in the history
728 # we can only check if those commits are present in the history
714 # as we cannot update this test every time file is changed
729 # as we cannot update this test every time file is changed
715 files = {
730 files = {
716 "setup.py": [
731 b"setup.py": [
717 "54386793436c938cff89326944d4c2702340037d",
732 "54386793436c938cff89326944d4c2702340037d",
718 "51d254f0ecf5df2ce50c0b115741f4cf13985dab",
733 "51d254f0ecf5df2ce50c0b115741f4cf13985dab",
719 "998ed409c795fec2012b1c0ca054d99888b22090",
734 "998ed409c795fec2012b1c0ca054d99888b22090",
@@ -724,7 +739,7 b' class TestGitCommit(object):'
724 "191caa5b2c81ed17c0794bf7bb9958f4dcb0b87e",
739 "191caa5b2c81ed17c0794bf7bb9958f4dcb0b87e",
725 "ff7ca51e58c505fec0dd2491de52c622bb7a806b",
740 "ff7ca51e58c505fec0dd2491de52c622bb7a806b",
726 ],
741 ],
727 "vcs/nodes.py": [
742 b"vcs/nodes.py": [
728 "33fa3223355104431402a888fa77a4e9956feb3e",
743 "33fa3223355104431402a888fa77a4e9956feb3e",
729 "fa014c12c26d10ba682fadb78f2a11c24c8118e1",
744 "fa014c12c26d10ba682fadb78f2a11c24c8118e1",
730 "e686b958768ee96af8029fe19c6050b1a8dd3b2b",
745 "e686b958768ee96af8029fe19c6050b1a8dd3b2b",
@@ -757,7 +772,7 b' class TestGitCommit(object):'
757 "dd80b0f6cf5052f17cc738c2951c4f2070200d7f",
772 "dd80b0f6cf5052f17cc738c2951c4f2070200d7f",
758 "ff7ca51e58c505fec0dd2491de52c622bb7a806b",
773 "ff7ca51e58c505fec0dd2491de52c622bb7a806b",
759 ],
774 ],
760 "vcs/backends/git.py": [
775 b"vcs/backends/git.py": [
761 "4cf116ad5a457530381135e2f4c453e68a1b0105",
776 "4cf116ad5a457530381135e2f4c453e68a1b0105",
762 "9a751d84d8e9408e736329767387f41b36935153",
777 "9a751d84d8e9408e736329767387f41b36935153",
763 "cb681fb539c3faaedbcdf5ca71ca413425c18f01",
778 "cb681fb539c3faaedbcdf5ca71ca413425c18f01",
@@ -778,7 +793,7 b' class TestGitCommit(object):'
778
793
779 def test_file_annotate(self):
794 def test_file_annotate(self):
780 files = {
795 files = {
781 "vcs/backends/__init__.py": {
796 b"vcs/backends/__init__.py": {
782 "c1214f7e79e02fc37156ff215cd71275450cffc3": {
797 "c1214f7e79e02fc37156ff215cd71275450cffc3": {
783 "lines_no": 1,
798 "lines_no": 1,
784 "commits": [
799 "commits": [
@@ -870,39 +885,31 b' class TestGitCommit(object):'
870 """
885 """
871 Tests state of FileNodes.
886 Tests state of FileNodes.
872 """
887 """
873 node = self.repo.get_commit("e6ea6d16e2f26250124a1f4b4fe37a912f9d86a0").get_node("vcs/utils/diffs.py")
888 commit = self.repo.get_commit("e6ea6d16e2f26250124a1f4b4fe37a912f9d86a0")
874 assert node.state, NodeState.ADDED
889 node = commit.get_node(b"vcs/utils/diffs.py")
875 assert node.added
890 assert node.bytes_path in commit.added_paths
876 assert not node.changed
877 assert not node.not_changed
878 assert not node.removed
879
891
880 node = self.repo.get_commit("33fa3223355104431402a888fa77a4e9956feb3e").get_node(".hgignore")
892 commit = self.repo.get_commit("33fa3223355104431402a888fa77a4e9956feb3e")
881 assert node.state, NodeState.CHANGED
893 node = commit.get_node(b".hgignore")
882 assert not node.added
894 assert node.bytes_path in commit.changed_paths
883 assert node.changed
884 assert not node.not_changed
885 assert not node.removed
886
895
887 node = self.repo.get_commit("e29b67bd158580fc90fc5e9111240b90e6e86064").get_node("setup.py")
896 commit = self.repo.get_commit("e29b67bd158580fc90fc5e9111240b90e6e86064")
888 assert node.state, NodeState.NOT_CHANGED
897 node = commit.get_node(b"setup.py")
889 assert not node.added
898 assert node.bytes_path not in commit.affected_files
890 assert not node.changed
891 assert node.not_changed
892 assert not node.removed
893
899
894 # If node has REMOVED state then trying to fetch it would raise
900 # If node has REMOVED state then trying to fetch it would raise
895 # CommitError exception
901 # CommitError exception
896 commit = self.repo.get_commit("fa6600f6848800641328adbf7811fd2372c02ab2")
902 commit = self.repo.get_commit("fa6600f6848800641328adbf7811fd2372c02ab2")
897 path = "vcs/backends/BaseRepository.py"
903 path = b"vcs/backends/BaseRepository.py"
898 with pytest.raises(NodeDoesNotExistError):
904 with pytest.raises(NodeDoesNotExistError):
899 commit.get_node(path)
905 commit.get_node(path)
906
900 # but it would be one of ``removed`` (commit's attribute)
907 # but it would be one of ``removed`` (commit's attribute)
901 assert path in [rf.path for rf in commit.removed]
908 assert path in [rf for rf in commit.removed_paths]
902
909
903 commit = self.repo.get_commit("54386793436c938cff89326944d4c2702340037d")
910 commit = self.repo.get_commit("54386793436c938cff89326944d4c2702340037d")
904 changed = ["setup.py", "tests/test_nodes.py", "vcs/backends/hg.py", "vcs/nodes.py"]
911 changed = [b"setup.py", b"tests/test_nodes.py", b"vcs/backends/hg.py", b"vcs/nodes.py"]
905 assert set(changed) == set([f.path for f in commit.changed])
912 assert set(changed) == set([f for f in commit.changed_paths])
906
913
907 def test_unicode_branch_refs(self):
914 def test_unicode_branch_refs(self):
908 unicode_branches = {
915 unicode_branches = {
@@ -936,14 +943,14 b' class TestGitCommit(object):'
936
943
937 def test_repo_files_content_types(self):
944 def test_repo_files_content_types(self):
938 commit = self.repo.get_commit()
945 commit = self.repo.get_commit()
939 for node in commit.get_node("/"):
946 for node in commit.get_node(b"/"):
940 if node.is_file():
947 if node.is_file():
941 assert type(node.content) == bytes
948 assert type(node.content) == bytes
942 assert type(node.str_content) == str
949 assert type(node.str_content) == str
943
950
944 def test_wrong_path(self):
951 def test_wrong_path(self):
945 # There is 'setup.py' in the root dir but not there:
952 # There is 'setup.py' in the root dir but not there:
946 path = "foo/bar/setup.py"
953 path = b"foo/bar/setup.py"
947 tip = self.repo.get_commit()
954 tip = self.repo.get_commit()
948 with pytest.raises(VCSError):
955 with pytest.raises(VCSError):
949 tip.get_node(path)
956 tip.get_node(path)
@@ -981,8 +988,7 b' class TestLargeFileRepo(object):'
981 repo = backend_git.create_test_repo("largefiles", conf)
988 repo = backend_git.create_test_repo("largefiles", conf)
982
989
983 tip = repo.scm_instance().get_commit()
990 tip = repo.scm_instance().get_commit()
984 node = tip.get_node("1MB.zip")
991 node = tip.get_node(b"1MB.zip")
985
986
992
987 # extract stored LF node into the origin cache
993 # extract stored LF node into the origin cache
988 repo_lfs_store: str = os.path.join(repo.repo_path, repo.repo_name, "lfs_store")
994 repo_lfs_store: str = os.path.join(repo.repo_path, repo.repo_name, "lfs_store")
@@ -1002,7 +1008,7 b' class TestLargeFileRepo(object):'
1002
1008
1003 assert lf_node.is_largefile() is True
1009 assert lf_node.is_largefile() is True
1004 assert lf_node.size == 1024000
1010 assert lf_node.size == 1024000
1005 assert lf_node.name == "1MB.zip"
1011 assert lf_node.name == b"1MB.zip"
1006
1012
1007
1013
1008 @pytest.mark.usefixtures("vcs_repository_support")
1014 @pytest.mark.usefixtures("vcs_repository_support")
@@ -1032,14 +1038,11 b' class TestGitSpecificWithRepo(BackendTes'
1032
1038
1033 def test_paths_slow_traversing(self):
1039 def test_paths_slow_traversing(self):
1034 commit = self.repo.get_commit()
1040 commit = self.repo.get_commit()
1035 assert (
1041 assert commit.get_node(b"foobar/static/js/admin/base.js").content == b"base"
1036 commit.get_node("foobar").get_node("static").get_node("js").get_node("admin").get_node("base.js").content
1037 == b"base"
1038 )
1039
1042
1040 def test_paths_fast_traversing(self):
1043 def test_paths_fast_traversing(self):
1041 commit = self.repo.get_commit()
1044 commit = self.repo.get_commit()
1042 assert commit.get_node("foobar/static/js/admin/base.js").content == b"base"
1045 assert commit.get_node(b"foobar/static/js/admin/base.js").content == b"base"
1043
1046
1044 def test_get_diff_runs_git_command_with_hashes(self):
1047 def test_get_diff_runs_git_command_with_hashes(self):
1045 comm1 = self.repo[0]
1048 comm1 = self.repo[0]
@@ -1110,12 +1113,15 b' class TestGitRegression(BackendTestMixin'
1110 @pytest.mark.parametrize(
1113 @pytest.mark.parametrize(
1111 "path, expected_paths",
1114 "path, expected_paths",
1112 [
1115 [
1113 ("bot", ["bot/build", "bot/templates", "bot/__init__.py"]),
1116 (b"bot", ["bot/build", "bot/templates", "bot/__init__.py"]),
1114 ("bot/build", ["bot/build/migrations", "bot/build/static", "bot/build/templates"]),
1117 (b"bot/build", ["bot/build/migrations", "bot/build/static", "bot/build/templates"]),
1115 ("bot/build/static", ["bot/build/static/templates"]),
1118 (b"bot/build/static", ["bot/build/static/templates"]),
1116 ("bot/build/static/templates", ["bot/build/static/templates/f.html", "bot/build/static/templates/f1.html"]),
1119 (
1117 ("bot/build/templates", ["bot/build/templates/err.html", "bot/build/templates/err2.html"]),
1120 b"bot/build/static/templates",
1118 ("bot/templates/", ["bot/templates/404.html", "bot/templates/500.html"]),
1121 ["bot/build/static/templates/f.html", "bot/build/static/templates/f1.html"],
1122 ),
1123 (b"bot/build/templates", ["bot/build/templates/err.html", "bot/build/templates/err2.html"]),
1124 (b"bot/templates/", ["bot/templates/404.html", "bot/templates/500.html"]),
1119 ],
1125 ],
1120 )
1126 )
1121 def test_similar_paths(self, path, expected_paths):
1127 def test_similar_paths(self, path, expected_paths):
@@ -1146,8 +1152,8 b' class TestGetSubmoduleUrl(object):'
1146 node.str_content = (
1152 node.str_content = (
1147 '[submodule "subrepo1"]\n' "\tpath = subrepo1\n" "\turl = https://code.rhodecode.com/dulwich\n"
1153 '[submodule "subrepo1"]\n' "\tpath = subrepo1\n" "\turl = https://code.rhodecode.com/dulwich\n"
1148 )
1154 )
1149 result = commit._get_submodule_url("subrepo1")
1155 result = commit._get_submodule_url(b"subrepo1")
1150 get_node_mock.assert_called_once_with(".gitmodules")
1156 get_node_mock.assert_called_once_with(b".gitmodules")
1151 assert result == "https://code.rhodecode.com/dulwich"
1157 assert result == "https://code.rhodecode.com/dulwich"
1152
1158
1153 def test_complex_submodule_path(self):
1159 def test_complex_submodule_path(self):
@@ -1160,14 +1166,14 b' class TestGetSubmoduleUrl(object):'
1160 "\tpath = complex/subrepo/path\n"
1166 "\tpath = complex/subrepo/path\n"
1161 "\turl = https://code.rhodecode.com/dulwich\n"
1167 "\turl = https://code.rhodecode.com/dulwich\n"
1162 )
1168 )
1163 result = commit._get_submodule_url("complex/subrepo/path")
1169 result = commit._get_submodule_url(b"complex/subrepo/path")
1164 get_node_mock.assert_called_once_with(".gitmodules")
1170 get_node_mock.assert_called_once_with(b".gitmodules")
1165 assert result == "https://code.rhodecode.com/dulwich"
1171 assert result == "https://code.rhodecode.com/dulwich"
1166
1172
1167 def test_submodules_file_not_found(self):
1173 def test_submodules_file_not_found(self):
1168 commit = GitCommit(repository=mock.Mock(), raw_id="abcdef12", idx=1)
1174 commit = GitCommit(repository=mock.Mock(), raw_id="abcdef12", idx=1)
1169 with mock.patch.object(commit, "get_node", side_effect=NodeDoesNotExistError):
1175 with mock.patch.object(commit, "get_node", side_effect=NodeDoesNotExistError):
1170 result = commit._get_submodule_url("complex/subrepo/path")
1176 result = commit._get_submodule_url(b"complex/subrepo/path")
1171 assert result is None
1177 assert result is None
1172
1178
1173 def test_path_not_found(self):
1179 def test_path_not_found(self):
@@ -1178,8 +1184,8 b' class TestGetSubmoduleUrl(object):'
1178 node.str_content = (
1184 node.str_content = (
1179 '[submodule "subrepo1"]\n' "\tpath = subrepo1\n" "\turl = https://code.rhodecode.com/dulwich\n"
1185 '[submodule "subrepo1"]\n' "\tpath = subrepo1\n" "\turl = https://code.rhodecode.com/dulwich\n"
1180 )
1186 )
1181 result = commit._get_submodule_url("subrepo2")
1187 result = commit._get_submodule_url(b"subrepo2")
1182 get_node_mock.assert_called_once_with(".gitmodules")
1188 get_node_mock.assert_called_once_with(b".gitmodules")
1183 assert result is None
1189 assert result is None
1184
1190
1185 def test_returns_cached_values(self):
1191 def test_returns_cached_values(self):
@@ -1191,44 +1197,43 b' class TestGetSubmoduleUrl(object):'
1191 '[submodule "subrepo1"]\n' "\tpath = subrepo1\n" "\turl = https://code.rhodecode.com/dulwich\n"
1197 '[submodule "subrepo1"]\n' "\tpath = subrepo1\n" "\turl = https://code.rhodecode.com/dulwich\n"
1192 )
1198 )
1193 for _ in range(3):
1199 for _ in range(3):
1194 commit._get_submodule_url("subrepo1")
1200 commit._get_submodule_url(b"subrepo1")
1195 get_node_mock.assert_called_once_with(".gitmodules")
1201 get_node_mock.assert_called_once_with(b".gitmodules")
1196
1202
1197 def test_get_node_returns_a_link(self):
1203 def test_get_node_returns_a_link(self):
1198 repository = mock.Mock()
1204 repository = mock.Mock()
1199 repository.alias = "git"
1205 repository.alias = "git"
1200 commit = GitCommit(repository=repository, raw_id="abcdef12", idx=1)
1206 commit = GitCommit(repository=repository, raw_id="abcdef12", idx=1)
1201 submodule_url = "https://code.rhodecode.com/dulwich"
1207 submodule_url = "https://code.rhodecode.com/dulwich"
1202 get_id_patch = mock.patch.object(commit, "_get_tree_id_for_path", return_value=(1, "link"))
1208 get_id_patch = mock.patch.object(commit, "_get_path_tree_id_and_type", return_value=(1, NodeKind.SUBMODULE))
1203 get_submodule_patch = mock.patch.object(commit, "_get_submodule_url", return_value=submodule_url)
1209 get_submodule_patch = mock.patch.object(commit, "_get_submodule_url", return_value=submodule_url)
1204
1210
1205 with get_id_patch, get_submodule_patch as submodule_mock:
1211 with get_id_patch, get_submodule_patch as submodule_mock:
1206 node = commit.get_node("/abcde")
1212 node = commit.get_node(b"/abcde")
1207
1213
1208 submodule_mock.assert_called_once_with("/abcde")
1214 submodule_mock.assert_called_once_with(b"/abcde")
1209 assert type(node) == SubModuleNode
1215 assert type(node) == SubModuleNode
1210 assert node.url == submodule_url
1216 assert node.url == submodule_url
1211
1217
1212 def test_get_nodes_returns_links(self):
1218 def test_get_nodes_returns_links(self):
1213 repository = mock.MagicMock()
1219 repository = mock.MagicMock()
1214 repository.alias = "git"
1220 repository.alias = "git"
1215 repository._remote.tree_items.return_value = [("subrepo", "stat", 1, "link")]
1221 repository._remote.tree_items.return_value = [(b"subrepo", "stat", 1, NodeKind.SUBMODULE)]
1216 commit = GitCommit(repository=repository, raw_id="abcdef12", idx=1)
1222 commit = GitCommit(repository=repository, raw_id="abcdef12", idx=1)
1217 submodule_url = "https://code.rhodecode.com/dulwich"
1223 submodule_url = "https://code.rhodecode.com/dulwich"
1218 get_id_patch = mock.patch.object(commit, "_get_tree_id_for_path", return_value=(1, "tree"))
1224 get_id_patch = mock.patch.object(commit, "_get_path_tree_id_and_type", return_value=(1, NodeKind.DIR))
1219 get_submodule_patch = mock.patch.object(commit, "_get_submodule_url", return_value=submodule_url)
1225 get_submodule_patch = mock.patch.object(commit, "_get_submodule_url", return_value=submodule_url)
1220
1226
1221 with get_id_patch, get_submodule_patch as submodule_mock:
1227 with get_id_patch, get_submodule_patch as submodule_mock:
1222 nodes = commit.get_nodes("/abcde")
1228 nodes = commit.get_nodes(b"/abcde")
1223
1229
1224 submodule_mock.assert_called_once_with("/abcde/subrepo")
1230 submodule_mock.assert_called_once_with(b"/abcde/subrepo")
1225 assert len(nodes) == 1
1231 assert len(nodes) == 1
1226 assert type(nodes[0]) == SubModuleNode
1232 assert type(nodes[0]) == SubModuleNode
1227 assert nodes[0].url == submodule_url
1233 assert nodes[0].url == submodule_url
1228
1234
1229
1235
1230 class TestGetShadowInstance(object):
1236 class TestGetShadowInstance(object):
1231
1232 @pytest.fixture()
1237 @pytest.fixture()
1233 def repo(self, vcsbackend_git):
1238 def repo(self, vcsbackend_git):
1234 _git_repo = vcsbackend_git.repo
1239 _git_repo = vcsbackend_git.repo
@@ -27,7 +27,7 b' from rhodecode.lib.vcs import backends'
27 from rhodecode.lib.vcs.backends.base import Reference, MergeResponse, MergeFailureReason
27 from rhodecode.lib.vcs.backends.base import Reference, MergeResponse, MergeFailureReason
28 from rhodecode.lib.vcs.backends.hg import MercurialRepository, MercurialCommit
28 from rhodecode.lib.vcs.backends.hg import MercurialRepository, MercurialCommit
29 from rhodecode.lib.vcs.exceptions import RepositoryError, VCSError, NodeDoesNotExistError, CommitDoesNotExistError
29 from rhodecode.lib.vcs.exceptions import RepositoryError, VCSError, NodeDoesNotExistError, CommitDoesNotExistError
30 from rhodecode.lib.vcs.nodes import FileNode, NodeKind, NodeState
30 from rhodecode.lib.vcs.nodes import FileNode, NodeKind, DirNode, RootNode
31 from rhodecode.tests import TEST_HG_REPO, TEST_HG_REPO_CLONE, repo_id_generator
31 from rhodecode.tests import TEST_HG_REPO, TEST_HG_REPO_CLONE, repo_id_generator
32
32
33
33
@@ -41,22 +41,17 b' def repo_path_generator():'
41 i = 0
41 i = 0
42 while True:
42 while True:
43 i += 1
43 i += 1
44 yield "%s-%d" % (TEST_HG_REPO_CLONE, i)
44 yield f"{TEST_HG_REPO_CLONE}-{i:d}"
45
45
46
46
47 REPO_PATH_GENERATOR = repo_path_generator()
47 REPO_PATH_GENERATOR = repo_path_generator()
48
48
49
49
50 @pytest.fixture(scope="class", autouse=True)
50 class TestMercurialRepository:
51 def repo(request, baseapp):
52 repo = MercurialRepository(TEST_HG_REPO)
53 if request.cls:
54 request.cls.repo = repo
55 return repo
56
57
58 class TestMercurialRepository(object):
59 # pylint: disable=protected-access
51 # pylint: disable=protected-access
52 @pytest.fixture(autouse=True)
53 def prepare(self):
54 self.repo = MercurialRepository(TEST_HG_REPO)
60
55
61 def get_clone_repo(self):
56 def get_clone_repo(self):
62 """
57 """
@@ -100,9 +95,8 b' class TestMercurialRepository(object):'
100
95
101 def test_repo_clone(self):
96 def test_repo_clone(self):
102 if os.path.exists(TEST_HG_REPO_CLONE):
97 if os.path.exists(TEST_HG_REPO_CLONE):
103 self.fail(
98 pytest.fail(
104 "Cannot test mercurial clone repo as location %s already "
99 f"Cannot test mercurial clone repo as location {TEST_HG_REPO_CLONE} already exists. You should manually remove it first."
105 "exists. You should manually remove it first." % TEST_HG_REPO_CLONE
106 )
100 )
107
101
108 repo = MercurialRepository(TEST_HG_REPO)
102 repo = MercurialRepository(TEST_HG_REPO)
@@ -217,8 +211,8 b' class TestMercurialRepository(object):'
217 assert "git" in self.repo._get_branches(closed=True)
211 assert "git" in self.repo._get_branches(closed=True)
218 assert "web" in self.repo._get_branches(closed=True)
212 assert "web" in self.repo._get_branches(closed=True)
219
213
220 for name, id in self.repo.branches.items():
214 for name, commit_id in self.repo.branches.items():
221 assert isinstance(self.repo.get_commit(id), MercurialCommit)
215 assert isinstance(self.repo.get_commit(commit_id), MercurialCommit)
222
216
223 def test_tip_in_tags(self):
217 def test_tip_in_tags(self):
224 # tip is always a tag
218 # tip is always a tag
@@ -235,29 +229,38 b' class TestMercurialRepository(object):'
235 assert init_commit.message == "initial import"
229 assert init_commit.message == "initial import"
236 assert init_author == "Marcin Kuzminski <marcin@python-blog.com>"
230 assert init_author == "Marcin Kuzminski <marcin@python-blog.com>"
237 assert init_author == init_commit.committer
231 assert init_author == init_commit.committer
238 assert sorted(init_commit._file_paths) == sorted(
232 assert sorted(init_commit.added_paths) == sorted(
239 [
233 [
240 "vcs/__init__.py",
234 b"vcs/__init__.py",
241 "vcs/backends/BaseRepository.py",
235 b"vcs/backends/BaseRepository.py",
242 "vcs/backends/__init__.py",
236 b"vcs/backends/__init__.py",
243 ]
237 ]
244 )
238 )
245 assert sorted(init_commit._dir_paths) == sorted(["", "vcs", "vcs/backends"])
239 assert sorted(init_commit.affected_files) == sorted(
240 [
241 b"vcs/__init__.py",
242 b"vcs/backends/BaseRepository.py",
243 b"vcs/backends/__init__.py",
244 ]
245 )
246
246
247 assert init_commit._dir_paths + init_commit._file_paths == init_commit._paths
247 for path in (b"vcs/__init__.py", b"vcs/backends/BaseRepository.py", b"vcs/backends/__init__.py"):
248 assert isinstance(init_commit.get_node(path), FileNode)
249 for path in (b"", b"vcs", b"vcs/backends"):
250 assert isinstance(init_commit.get_node(path), DirNode)
248
251
249 with pytest.raises(NodeDoesNotExistError):
252 with pytest.raises(NodeDoesNotExistError):
250 init_commit.get_node(path="foobar")
253 init_commit.get_node(path=b"foobar")
251
254
252 node = init_commit.get_node("vcs/")
255 node = init_commit.get_node(b"vcs/")
253 assert hasattr(node, "kind")
256 assert hasattr(node, "kind")
254 assert node.kind == NodeKind.DIR
257 assert node.kind == NodeKind.DIR
255
258
256 node = init_commit.get_node("vcs")
259 node = init_commit.get_node(b"vcs")
257 assert hasattr(node, "kind")
260 assert hasattr(node, "kind")
258 assert node.kind == NodeKind.DIR
261 assert node.kind == NodeKind.DIR
259
262
260 node = init_commit.get_node("vcs/__init__.py")
263 node = init_commit.get_node(b"vcs/__init__.py")
261 assert hasattr(node, "kind")
264 assert hasattr(node, "kind")
262 assert node.kind == NodeKind.FILE
265 assert node.kind == NodeKind.FILE
263
266
@@ -279,7 +282,7 b' class TestMercurialRepository(object):'
279
282
280 def test_commit10(self):
283 def test_commit10(self):
281 commit10 = self.repo.get_commit(commit_idx=10)
284 commit10 = self.repo.get_commit(commit_idx=10)
282 README = """===
285 readme = """===
283 VCS
286 VCS
284 ===
287 ===
285
288
@@ -291,9 +294,9 b' Introduction'
291 TODO: To be written...
294 TODO: To be written...
292
295
293 """
296 """
294 node = commit10.get_node("README.rst")
297 node = commit10.get_node(b"README.rst")
295 assert node.kind == NodeKind.FILE
298 assert node.kind == NodeKind.FILE
296 assert node.str_content == README
299 assert node.str_content == readme
297
300
298 def test_local_clone(self):
301 def test_local_clone(self):
299 clone_path = next(REPO_PATH_GENERATOR)
302 clone_path = next(REPO_PATH_GENERATOR)
@@ -370,7 +373,7 b' TODO: To be written...'
370 assert target_repo.branches["default"] == commit_id
373 assert target_repo.branches["default"] == commit_id
371
374
372 def test_local_pull_from_same_repo(self):
375 def test_local_pull_from_same_repo(self):
373 reference = Reference("branch", "default", None)
376 reference = Reference("branch", "default", "")
374 with pytest.raises(ValueError):
377 with pytest.raises(ValueError):
375 self.repo._local_pull(self.repo.path, reference)
378 self.repo._local_pull(self.repo.path, reference)
376
379
@@ -503,7 +506,7 b' TODO: To be written...'
503 # Check we are not left in an intermediate merge state
506 # Check we are not left in an intermediate merge state
504 assert not os.path.exists(os.path.join(target_repo.path, ".hg", "merge", "state"))
507 assert not os.path.exists(os.path.join(target_repo.path, ".hg", "merge", "state"))
505
508
506 def test_local_merge_of_two_branches_of_the_same_repo(self, backend_hg):
509 def test_local_merge_of_two_branches_of_the_same_repo(self, backend_hg, vcs_repo):
507 commits = [
510 commits = [
508 {"message": "a"},
511 {"message": "a"},
509 {"message": "b", "branch": "b"},
512 {"message": "b", "branch": "b"},
@@ -639,7 +642,7 b' TODO: To be written...'
639
642
640 # add an extra head to the target repo
643 # add an extra head to the target repo
641 imc = target_repo.in_memory_commit
644 imc = target_repo.in_memory_commit
642 imc.add(FileNode(b"file_x", content="foo"))
645 imc.add(FileNode(b"file_x", content=b"foo"))
643 commits = list(target_repo.get_commits())
646 commits = list(target_repo.get_commits())
644 imc.commit(
647 imc.commit(
645 message="Automatic commit from repo merge test",
648 message="Automatic commit from repo merge test",
@@ -728,8 +731,7 b' TODO: To be written...'
728 assert len(target_repo.commit_ids) == 2 + 2
731 assert len(target_repo.commit_ids) == 2 + 2
729
732
730
733
731 class TestGetShadowInstance(object):
734 class TestGetShadowInstance:
732
733 @pytest.fixture()
735 @pytest.fixture()
734 def repo(self, vcsbackend_hg):
736 def repo(self, vcsbackend_hg):
735 _hg_repo = vcsbackend_hg.repo
737 _hg_repo = vcsbackend_hg.repo
@@ -742,17 +744,21 b' class TestGetShadowInstance(object):'
742 assert shadow.config.serialize() == repo.config.serialize()
744 assert shadow.config.serialize() == repo.config.serialize()
743
745
744 def test_disables_hooks_section(self, repo):
746 def test_disables_hooks_section(self, repo):
745 repo.config.set('hooks', 'foo', 'val')
747 repo.config.set("hooks", "foo", "val")
746 shadow = repo.get_shadow_instance(repo.path)
748 shadow = repo.get_shadow_instance(repo.path)
747 assert not shadow.config.items('hooks')
749 assert not shadow.config.items("hooks")
748
750
749 def test_allows_to_keep_hooks(self, repo):
751 def test_allows_to_keep_hooks(self, repo):
750 repo.config.set('hooks', 'foo', 'val')
752 repo.config.set("hooks", "foo", "val")
751 shadow = repo.get_shadow_instance(repo.path, enable_hooks=True)
753 shadow = repo.get_shadow_instance(repo.path, enable_hooks=True)
752 assert shadow.config.items('hooks')
754 assert shadow.config.items("hooks")
753
755
754
756
755 class TestMercurialCommit(object):
757 class TestMercurialCommit:
758 @pytest.fixture(autouse=True)
759 def prepare(self):
760 self.repo = MercurialRepository(TEST_HG_REPO)
761
756 def _test_equality(self, commit):
762 def _test_equality(self, commit):
757 idx = commit.idx
763 idx = commit.idx
758 assert commit == self.repo.get_commit(commit_idx=idx)
764 assert commit == self.repo.get_commit(commit_idx=idx)
@@ -772,7 +778,7 b' class TestMercurialCommit(object):'
772
778
773 def test_root_node(self):
779 def test_root_node(self):
774 tip = self.repo.get_commit("tip")
780 tip = self.repo.get_commit("tip")
775 assert tip.root is tip.get_node("")
781 assert tip.root is tip.get_node(b"")
776
782
777 def test_lazy_fetch(self):
783 def test_lazy_fetch(self):
778 """
784 """
@@ -788,28 +794,28 b' class TestMercurialCommit(object):'
788 # accessing root.nodes updates commit.nodes
794 # accessing root.nodes updates commit.nodes
789 assert len(commit.nodes) == 9
795 assert len(commit.nodes) == 9
790
796
791 docs = root.get_node("docs")
797 docs = commit.get_node(b"docs")
792 # we haven't yet accessed anything new as docs dir was already cached
798 # we haven't yet accessed anything new as docs dir was already cached
793 assert len(commit.nodes) == 9
799 assert len(commit.nodes) == 9
794 assert len(docs.nodes) == 8
800 assert len(docs.nodes) == 8
795 # accessing docs.nodes updates commit.nodes
801 # accessing docs.nodes updates commit.nodes
796 assert len(commit.nodes) == 17
802 assert len(commit.nodes) == 17
797
803
798 assert docs is commit.get_node("docs")
804 assert docs is commit.get_node(b"docs")
799 assert docs is root.nodes[0]
805 assert docs is root.nodes[0]
800 assert docs is root.dirs[0]
806 assert docs is root.dirs[0]
801 assert docs is commit.get_node("docs")
807 assert docs is commit.get_node(b"docs")
802
808
803 def test_nodes_with_commit(self):
809 def test_nodes_with_commit(self):
804 commit = self.repo.get_commit(commit_idx=45)
810 commit = self.repo.get_commit(commit_idx=45)
805 root = commit.root
811 root = commit.root
806 docs = root.get_node("docs")
812 assert isinstance(root, RootNode)
807 assert docs is commit.get_node("docs")
813 docs = commit.get_node(b"docs")
808 api = docs.get_node("api")
814 assert docs is commit.get_node(b"docs")
809 assert api is commit.get_node("docs/api")
815 api = commit.get_node(b"docs/api")
810 index = api.get_node("index.rst")
816 assert api is commit.get_node(b"docs/api")
811 assert index is commit.get_node("docs/api/index.rst")
817 index = commit.get_node(b"docs/api/index.rst")
812 assert index is commit.get_node("docs").get_node("api").get_node("index.rst")
818 assert index is commit.get_node(b"docs/api/index.rst")
813
819
814 def test_branch_and_tags(self):
820 def test_branch_and_tags(self):
815 commit0 = self.repo.get_commit(commit_idx=0)
821 commit0 = self.repo.get_commit(commit_idx=0)
@@ -837,28 +843,28 b' class TestMercurialCommit(object):'
837
843
838 def test_file_size(self):
844 def test_file_size(self):
839 to_check = (
845 to_check = (
840 (10, "setup.py", 1068),
846 (10, b"setup.py", 1068),
841 (20, "setup.py", 1106),
847 (20, b"setup.py", 1106),
842 (60, "setup.py", 1074),
848 (60, b"setup.py", 1074),
843 (10, "vcs/backends/base.py", 2921),
849 (10, b"vcs/backends/base.py", 2921),
844 (20, "vcs/backends/base.py", 3936),
850 (20, b"vcs/backends/base.py", 3936),
845 (60, "vcs/backends/base.py", 6189),
851 (60, b"vcs/backends/base.py", 6189),
846 )
852 )
847 for idx, path, size in to_check:
853 for idx, path, size in to_check:
848 self._test_file_size(idx, path, size)
854 self._test_file_size(idx, path, size)
849
855
850 def test_file_history_from_commits(self):
856 def test_file_history_from_commits(self):
851 node = self.repo[10].get_node("setup.py")
857 node = self.repo[10].get_node(b"setup.py")
852 commit_ids = [commit.raw_id for commit in node.history]
858 commit_ids = [commit.raw_id for commit in node.history]
853 assert ["3803844fdbd3b711175fc3da9bdacfcd6d29a6fb"] == commit_ids
859 assert ["3803844fdbd3b711175fc3da9bdacfcd6d29a6fb"] == commit_ids
854
860
855 node = self.repo[20].get_node("setup.py")
861 node = self.repo[20].get_node(b"setup.py")
856 node_ids = [commit.raw_id for commit in node.history]
862 node_ids = [commit.raw_id for commit in node.history]
857 assert ["eada5a770da98ab0dd7325e29d00e0714f228d09", "3803844fdbd3b711175fc3da9bdacfcd6d29a6fb"] == node_ids
863 assert ["eada5a770da98ab0dd7325e29d00e0714f228d09", "3803844fdbd3b711175fc3da9bdacfcd6d29a6fb"] == node_ids
858
864
859 # special case we check history from commit that has this particular
865 # special case we check history from commit that has this particular
860 # file changed this means we check if it's included as well
866 # file changed this means we check if it's included as well
861 node = self.repo.get_commit("eada5a770da98ab0dd7325e29d00e0714f228d09").get_node("setup.py")
867 node = self.repo.get_commit("eada5a770da98ab0dd7325e29d00e0714f228d09").get_node(b"setup.py")
862 node_ids = [commit.raw_id for commit in node.history]
868 node_ids = [commit.raw_id for commit in node.history]
863 assert ["eada5a770da98ab0dd7325e29d00e0714f228d09", "3803844fdbd3b711175fc3da9bdacfcd6d29a6fb"] == node_ids
869 assert ["eada5a770da98ab0dd7325e29d00e0714f228d09", "3803844fdbd3b711175fc3da9bdacfcd6d29a6fb"] == node_ids
864
870
@@ -866,9 +872,9 b' class TestMercurialCommit(object):'
866 # we can only check if those commits are present in the history
872 # we can only check if those commits are present in the history
867 # as we cannot update this test every time file is changed
873 # as we cannot update this test every time file is changed
868 files = {
874 files = {
869 "setup.py": [7, 18, 45, 46, 47, 69, 77],
875 b"setup.py": [7, 18, 45, 46, 47, 69, 77],
870 "vcs/nodes.py": [7, 8, 24, 26, 30, 45, 47, 49, 56, 57, 58, 59, 60, 61, 73, 76],
876 b"vcs/nodes.py": [7, 8, 24, 26, 30, 45, 47, 49, 56, 57, 58, 59, 60, 61, 73, 76],
871 "vcs/backends/hg.py": [
877 b"vcs/backends/hg.py": [
872 4,
878 4,
873 5,
879 5,
874 6,
880 6,
@@ -927,7 +933,7 b' class TestMercurialCommit(object):'
927
933
928 def test_file_annotate(self):
934 def test_file_annotate(self):
929 files = {
935 files = {
930 "vcs/backends/__init__.py": {
936 b"vcs/backends/__init__.py": {
931 89: {
937 89: {
932 "lines_no": 31,
938 "lines_no": 31,
933 "commits": [
939 "commits": [
@@ -1002,7 +1008,7 b' class TestMercurialCommit(object):'
1002 ],
1008 ],
1003 },
1009 },
1004 },
1010 },
1005 "vcs/exceptions.py": {
1011 b"vcs/exceptions.py": {
1006 89: {
1012 89: {
1007 "lines_no": 18,
1013 "lines_no": 18,
1008 "commits": [16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 17, 16, 16, 18, 18, 18],
1014 "commits": [16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 17, 16, 16, 18, 18, 18],
@@ -1016,25 +1022,25 b' class TestMercurialCommit(object):'
1016 "commits": [16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 17, 16, 16, 18, 18, 18],
1022 "commits": [16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 17, 16, 16, 18, 18, 18],
1017 },
1023 },
1018 },
1024 },
1019 "MANIFEST.in": {
1025 b"MANIFEST.in": {
1020 89: {"lines_no": 5, "commits": [7, 7, 7, 71, 71]},
1026 89: {"lines_no": 5, "commits": [7, 7, 7, 71, 71]},
1021 20: {"lines_no": 3, "commits": [7, 7, 7]},
1027 20: {"lines_no": 3, "commits": [7, 7, 7]},
1022 55: {"lines_no": 3, "commits": [7, 7, 7]},
1028 55: {"lines_no": 3, "commits": [7, 7, 7]},
1023 },
1029 },
1024 }
1030 }
1025
1031
1026 for fname, commit_dict in files.items():
1032 for file_name, commit_dict in files.items():
1027 for idx, __ in commit_dict.items():
1033 for idx, __ in commit_dict.items():
1028 commit = self.repo.get_commit(commit_idx=idx)
1034 commit = self.repo.get_commit(commit_idx=idx)
1029 l1_1 = [x[1] for x in commit.get_file_annotate(fname)]
1035 l1_1 = [x[1] for x in commit.get_file_annotate(file_name)]
1030 l1_2 = [x[2]().raw_id for x in commit.get_file_annotate(fname)]
1036 l1_2 = [x[2]().raw_id for x in commit.get_file_annotate(file_name)]
1031 assert l1_1 == l1_2
1037 assert l1_1 == l1_2
1032 l1 = l1_2 = [x[2]().idx for x in commit.get_file_annotate(fname)]
1038 l1 = l1_2 = [x[2]().idx for x in commit.get_file_annotate(file_name)]
1033 l2 = files[fname][idx]["commits"]
1039 l2 = files[file_name][idx]["commits"]
1034 assert l1 == l2, (
1040 assert l1 == l2, (
1035 "The lists of commit for %s@commit_id%s"
1041 "The lists of commit for %s@commit_id%s"
1036 "from annotation list should match each other,"
1042 "from annotation list should match each other,"
1037 "got \n%s \nvs \n%s " % (fname, idx, l1, l2)
1043 "got \n%s \nvs \n%s " % (file_name, idx, l1, l2)
1038 )
1044 )
1039
1045
1040 def test_commit_state(self):
1046 def test_commit_state(self):
@@ -1047,55 +1053,51 b' class TestMercurialCommit(object):'
1047 # changed: 13
1053 # changed: 13
1048 # added: 20
1054 # added: 20
1049 # removed: 1
1055 # removed: 1
1050 changed = set(
1056 changed = {
1051 [
1057 b".hgignore",
1052 ".hgignore",
1058 b"README.rst",
1053 "README.rst",
1059 b"docs/conf.py",
1054 "docs/conf.py",
1060 b"docs/index.rst",
1055 "docs/index.rst",
1061 b"setup.py",
1056 "setup.py",
1062 b"tests/test_hg.py",
1057 "tests/test_hg.py",
1063 b"tests/test_nodes.py",
1058 "tests/test_nodes.py",
1064 b"vcs/__init__.py",
1059 "vcs/__init__.py",
1065 b"vcs/backends/__init__.py",
1060 "vcs/backends/__init__.py",
1066 b"vcs/backends/base.py",
1061 "vcs/backends/base.py",
1067 b"vcs/backends/hg.py",
1062 "vcs/backends/hg.py",
1068 b"vcs/nodes.py",
1063 "vcs/nodes.py",
1069 b"vcs/utils/__init__.py",
1064 "vcs/utils/__init__.py",
1070 }
1065 ]
1066 )
1067
1071
1068 added = set(
1072 added = {
1069 [
1073 b"docs/api/backends/hg.rst",
1070 "docs/api/backends/hg.rst",
1074 b"docs/api/backends/index.rst",
1071 "docs/api/backends/index.rst",
1075 b"docs/api/index.rst",
1072 "docs/api/index.rst",
1076 b"docs/api/nodes.rst",
1073 "docs/api/nodes.rst",
1077 b"docs/api/web/index.rst",
1074 "docs/api/web/index.rst",
1078 b"docs/api/web/simplevcs.rst",
1075 "docs/api/web/simplevcs.rst",
1079 b"docs/installation.rst",
1076 "docs/installation.rst",
1080 b"docs/quickstart.rst",
1077 "docs/quickstart.rst",
1081 b"setup.cfg",
1078 "setup.cfg",
1082 b"vcs/utils/baseui_config.py",
1079 "vcs/utils/baseui_config.py",
1083 b"vcs/utils/web.py",
1080 "vcs/utils/web.py",
1084 b"vcs/web/__init__.py",
1081 "vcs/web/__init__.py",
1085 b"vcs/web/exceptions.py",
1082 "vcs/web/exceptions.py",
1086 b"vcs/web/simplevcs/__init__.py",
1083 "vcs/web/simplevcs/__init__.py",
1087 b"vcs/web/simplevcs/exceptions.py",
1084 "vcs/web/simplevcs/exceptions.py",
1088 b"vcs/web/simplevcs/middleware.py",
1085 "vcs/web/simplevcs/middleware.py",
1089 b"vcs/web/simplevcs/models.py",
1086 "vcs/web/simplevcs/models.py",
1090 b"vcs/web/simplevcs/settings.py",
1087 "vcs/web/simplevcs/settings.py",
1091 b"vcs/web/simplevcs/utils.py",
1088 "vcs/web/simplevcs/utils.py",
1092 b"vcs/web/simplevcs/views.py",
1089 "vcs/web/simplevcs/views.py",
1093 }
1090 ]
1091 )
1092
1094
1093 removed = set(["docs/api.rst"])
1095 removed = {b"docs/api.rst"}
1094
1096
1095 commit64 = self.repo.get_commit("46ad32a4f974")
1097 commit64 = self.repo.get_commit("46ad32a4f974")
1096 assert set((node.path for node in commit64.added)) == added
1098 assert set((node for node in commit64.added_paths)) == added
1097 assert set((node.path for node in commit64.changed)) == changed
1099 assert set((node for node in commit64.changed_paths)) == changed
1098 assert set((node.path for node in commit64.removed)) == removed
1100 assert set((node for node in commit64.removed_paths)) == removed
1099
1101
1100 # commit_id b090f22d27d6:
1102 # commit_id b090f22d27d6:
1101 # hg st --rev b090f22d27d6
1103 # hg st --rev b090f22d27d6
@@ -1103,9 +1105,9 b' class TestMercurialCommit(object):'
1103 # added: 20
1105 # added: 20
1104 # removed: 1
1106 # removed: 1
1105 commit88 = self.repo.get_commit("b090f22d27d6")
1107 commit88 = self.repo.get_commit("b090f22d27d6")
1106 assert set((node.path for node in commit88.added)) == set()
1108 assert set((node for node in commit88.added_paths)) == set()
1107 assert set((node.path for node in commit88.changed)) == set([".hgignore"])
1109 assert set((node for node in commit88.changed_paths)) == {b".hgignore"}
1108 assert set((node.path for node in commit88.removed)) == set()
1110 assert set((node for node in commit88.removed_paths)) == set()
1109
1111
1110 #
1112 #
1111 # 85:
1113 # 85:
@@ -1114,55 +1116,40 b' class TestMercurialCommit(object):'
1114 # changed: 4 ['vcs/web/simplevcs/models.py', ...]
1116 # changed: 4 ['vcs/web/simplevcs/models.py', ...]
1115 # removed: 1 ['vcs/utils/web.py']
1117 # removed: 1 ['vcs/utils/web.py']
1116 commit85 = self.repo.get_commit(commit_idx=85)
1118 commit85 = self.repo.get_commit(commit_idx=85)
1117 assert set((node.path for node in commit85.added)) == set(
1119 assert set((node for node in commit85.added_paths)) == {b"vcs/utils/diffs.py", b"vcs/web/simplevcs/views/diffs.py"}
1118 ["vcs/utils/diffs.py", "vcs/web/simplevcs/views/diffs.py"]
1120 assert set((node for node in commit85.changed_paths)) == {
1119 )
1121 b"vcs/web/simplevcs/models.py",
1120 assert set((node.path for node in commit85.changed)) == set(
1122 b"vcs/web/simplevcs/utils.py",
1121 [
1123 b"vcs/web/simplevcs/views/__init__.py",
1122 "vcs/web/simplevcs/models.py",
1124 b"vcs/web/simplevcs/views/repository.py",
1123 "vcs/web/simplevcs/utils.py",
1125 }
1124 "vcs/web/simplevcs/views/__init__.py",
1126 assert set((node for node in commit85.removed_paths)) == {b"vcs/utils/web.py"}
1125 "vcs/web/simplevcs/views/repository.py",
1126 ]
1127 )
1128 assert set((node.path for node in commit85.removed)) == set(["vcs/utils/web.py"])
1129
1127
1130 def test_files_state(self):
1128 def test_files_state(self):
1131 """
1129 """
1132 Tests state of FileNodes.
1130 Tests state of FileNodes.
1133 """
1131 """
1134 commit = self.repo.get_commit(commit_idx=85)
1132 commit = self.repo.get_commit(commit_idx=85)
1135 node = commit.get_node("vcs/utils/diffs.py")
1133 node = commit.get_node(b"vcs/utils/diffs.py")
1136 assert node.state, NodeState.ADDED
1134 assert node.bytes_path in commit.added_paths
1137 assert node.added
1138 assert not node.changed
1139 assert not node.not_changed
1140 assert not node.removed
1141
1135
1142 commit = self.repo.get_commit(commit_idx=88)
1136 commit = self.repo.get_commit(commit_idx=88)
1143 node = commit.get_node(".hgignore")
1137 node = commit.get_node(b".hgignore")
1144 assert node.state, NodeState.CHANGED
1138 assert node.bytes_path in commit.changed_paths
1145 assert not node.added
1146 assert node.changed
1147 assert not node.not_changed
1148 assert not node.removed
1149
1139
1150 commit = self.repo.get_commit(commit_idx=85)
1140 commit = self.repo.get_commit(commit_idx=85)
1151 node = commit.get_node("setup.py")
1141 node = commit.get_node(b"setup.py")
1152 assert node.state, NodeState.NOT_CHANGED
1142 assert node.bytes_path not in commit.affected_files
1153 assert not node.added
1154 assert not node.changed
1155 assert node.not_changed
1156 assert not node.removed
1157
1143
1158 # If node has REMOVED state then trying to fetch it would raise
1144 # If node has REMOVED state then trying to fetch it would raise
1159 # CommitError exception
1145 # CommitError exception
1160 commit = self.repo.get_commit(commit_idx=2)
1146 commit = self.repo.get_commit(commit_idx=2)
1161 path = "vcs/backends/BaseRepository.py"
1147 path = b"vcs/backends/BaseRepository.py"
1162 with pytest.raises(NodeDoesNotExistError):
1148 with pytest.raises(NodeDoesNotExistError):
1163 commit.get_node(path)
1149 commit.get_node(path)
1150
1164 # but it would be one of ``removed`` (commit's attribute)
1151 # but it would be one of ``removed`` (commit's attribute)
1165 assert path in [rf.path for rf in commit.removed]
1152 assert path in [rf for rf in commit.removed_paths]
1166
1153
1167 def test_commit_message_is_unicode(self):
1154 def test_commit_message_is_unicode(self):
1168 for cm in self.repo:
1155 for cm in self.repo:
@@ -1174,14 +1161,14 b' class TestMercurialCommit(object):'
1174
1161
1175 def test_repo_files_content_type(self):
1162 def test_repo_files_content_type(self):
1176 test_commit = self.repo.get_commit(commit_idx=100)
1163 test_commit = self.repo.get_commit(commit_idx=100)
1177 for node in test_commit.get_node("/"):
1164 for node in test_commit.get_node(b"/"):
1178 if node.is_file():
1165 if node.is_file():
1179 assert type(node.content) == bytes
1166 assert type(node.content) == bytes
1180 assert type(node.str_content) == str
1167 assert type(node.str_content) == str
1181
1168
1182 def test_wrong_path(self):
1169 def test_wrong_path(self):
1183 # There is 'setup.py' in the root dir but not there:
1170 # There is 'setup.py' in the root dir but not there:
1184 path = "foo/bar/setup.py"
1171 path = b"foo/bar/setup.py"
1185 with pytest.raises(VCSError):
1172 with pytest.raises(VCSError):
1186 self.repo.get_commit().get_node(path)
1173 self.repo.get_commit().get_node(path)
1187
1174
@@ -1196,23 +1183,27 b' class TestMercurialCommit(object):'
1196 assert "marcink" == self.repo.get_commit("84478366594b").author_name
1183 assert "marcink" == self.repo.get_commit("84478366594b").author_name
1197
1184
1198
1185
1199 class TestLargeFileRepo(object):
1186 class TestLargeFileRepo:
1200 def test_large_file(self, backend_hg):
1187 def test_large_file(self, backend_hg):
1201 conf = make_db_config()
1188 conf = make_db_config()
1202 hg_largefiles_store = conf.get("largefiles", "usercache")
1189 hg_largefiles_store = conf.get("largefiles", "usercache")
1203 repo = backend_hg.create_test_repo("largefiles", conf)
1190 repo = backend_hg.create_test_repo("largefiles", conf)
1204
1191
1205 tip = repo.scm_instance().get_commit()
1192 tip = repo.scm_instance().get_commit()
1206 node = tip.get_node(".hglf/thisfileislarge")
1193 node = tip.get_node(b".hglf/thisfileislarge")
1207
1194
1208 lf_node = node.get_largefile_node()
1195 lf_node = node.get_largefile_node()
1209
1196
1210 assert lf_node.is_largefile() is True
1197 assert lf_node.is_largefile() is True
1211 assert lf_node.size == 1024000
1198 assert lf_node.size == 1024000
1212 assert lf_node.name == ".hglf/thisfileislarge"
1199 assert lf_node.name == b".hglf/thisfileislarge"
1213
1200
1214
1201
1215 class TestGetBranchName(object):
1202 class TestGetBranchName:
1203 @pytest.fixture(autouse=True)
1204 def prepare(self):
1205 self.repo = MercurialRepository(TEST_HG_REPO)
1206
1216 def test_returns_ref_name_when_type_is_branch(self):
1207 def test_returns_ref_name_when_type_is_branch(self):
1217 ref = self._create_ref("branch", "fake-name")
1208 ref = self._create_ref("branch", "fake-name")
1218 result = self.repo._get_branch_name(ref)
1209 result = self.repo._get_branch_name(ref)
@@ -1235,7 +1226,11 b' class TestGetBranchName(object):'
1235 return ref
1226 return ref
1236
1227
1237
1228
1238 class TestIsTheSameBranch(object):
1229 class TestIsTheSameBranch:
1230 @pytest.fixture(autouse=True)
1231 def prepare(self):
1232 self.repo = MercurialRepository(TEST_HG_REPO)
1233
1239 def test_returns_true_when_branches_are_equal(self):
1234 def test_returns_true_when_branches_are_equal(self):
1240 source_ref = mock.Mock(name="source-ref")
1235 source_ref = mock.Mock(name="source-ref")
1241 target_ref = mock.Mock(name="target-ref")
1236 target_ref = mock.Mock(name="target-ref")
@@ -128,8 +128,8 b' class TestInMemoryCommit(BackendTestMixi'
128 ]
128 ]
129 self.imc.add(*to_add)
129 self.imc.add(*to_add)
130 commit = self.imc.commit("Initial", "joe doe <joe.doe@example.com>")
130 commit = self.imc.commit("Initial", "joe doe <joe.doe@example.com>")
131 assert isinstance(commit.get_node("foo"), DirNode)
131 assert isinstance(commit.get_node(b"foo"), DirNode)
132 assert isinstance(commit.get_node("foo/bar"), DirNode)
132 assert isinstance(commit.get_node(b"foo/bar"), DirNode)
133 self.assert_nodes_in_commit(commit, to_add)
133 self.assert_nodes_in_commit(commit, to_add)
134
134
135 # commit some more files again
135 # commit some more files again
@@ -244,7 +244,7 b' class TestInMemoryCommit(BackendTestMixi'
244
244
245 tip = self.repo.get_commit()
245 tip = self.repo.get_commit()
246 node = nodes[0]
246 node = nodes[0]
247 assert node.content == tip.get_node(node.path).content
247 assert node.content == tip.get_node(node.bytes_path).content
248 self.imc.remove(node)
248 self.imc.remove(node)
249 self.imc.commit(message=f"Removed {node.path}", author="Some Name <foo@bar.com>")
249 self.imc.commit(message=f"Removed {node.path}", author="Some Name <foo@bar.com>")
250
250
@@ -252,7 +252,7 b' class TestInMemoryCommit(BackendTestMixi'
252 assert tip != newtip
252 assert tip != newtip
253 assert tip.id != newtip.id
253 assert tip.id != newtip.id
254 with pytest.raises(NodeDoesNotExistError):
254 with pytest.raises(NodeDoesNotExistError):
255 newtip.get_node(node.path)
255 newtip.get_node(node.bytes_path)
256
256
257 def test_remove_last_file_from_directory(self):
257 def test_remove_last_file_from_directory(self):
258 node = FileNode(b"omg/qwe/foo/bar", content=b"foobar")
258 node = FileNode(b"omg/qwe/foo/bar", content=b"foobar")
@@ -262,7 +262,7 b' class TestInMemoryCommit(BackendTestMixi'
262 self.imc.remove(node)
262 self.imc.remove(node)
263 tip = self.imc.commit("removed", "joe doe <joe@doe.com>")
263 tip = self.imc.commit("removed", "joe doe <joe@doe.com>")
264 with pytest.raises(NodeDoesNotExistError):
264 with pytest.raises(NodeDoesNotExistError):
265 tip.get_node("omg/qwe/foo/bar")
265 tip.get_node(b"omg/qwe/foo/bar")
266
266
267 def test_remove_raise_node_does_not_exist(self, nodes):
267 def test_remove_raise_node_does_not_exist(self, nodes):
268 self.imc.remove(nodes[0])
268 self.imc.remove(nodes[0])
@@ -338,5 +338,5 b' class TestInMemoryCommit(BackendTestMixi'
338
338
339 def assert_nodes_in_commit(self, commit, nodes):
339 def assert_nodes_in_commit(self, commit, nodes):
340 for node in nodes:
340 for node in nodes:
341 assert commit.get_node(node.path).path == node.path
341 assert commit.get_node(node.bytes_path).path == node.path
342 assert commit.get_node(node.path).content == node.content
342 assert commit.get_node(node.bytes_path).content == node.content
@@ -155,30 +155,6 b' class TestNodeBasics:'
155 with pytest.raises(NodeError):
155 with pytest.raises(NodeError):
156 node.content # noqa
156 node.content # noqa
157
157
158 def test_dir_node_iter(self):
159 nodes = [
160 DirNode(b"docs"),
161 DirNode(b"tests"),
162 FileNode(b"bar"),
163 FileNode(b"foo"),
164 FileNode(b"readme.txt"),
165 FileNode(b"setup.py"),
166 ]
167 dirnode = DirNode(b"", nodes=nodes)
168 for node in dirnode:
169 assert node == dirnode.get_node(node.path)
170
171 def test_node_state(self):
172 """
173 Without link to commit nodes should raise NodeError.
174 """
175 node = FileNode(b"anything")
176 with pytest.raises(NodeError):
177 node.state # noqa
178 node = DirNode(b"anything")
179 with pytest.raises(NodeError):
180 node.state # noqa
181
182 def test_file_node_stat(self):
158 def test_file_node_stat(self):
183 node = FileNode(b"foobar", b"empty... almost")
159 node = FileNode(b"foobar", b"empty... almost")
184 mode = node.mode # default should be 0100644
160 mode = node.mode # default should be 0100644
@@ -272,5 +248,5 b' class TestNodesCommits(BackendTestMixin)'
272 last_commit = repo.get_commit()
248 last_commit = repo.get_commit()
273
249
274 for x in range(3):
250 for x in range(3):
275 node = last_commit.get_node(f"file_{x}.txt")
251 node = last_commit.get_node(b"file_%d.txt" % x)
276 assert node.last_commit == repo[x]
252 assert node.last_commit == repo[x]
@@ -128,7 +128,7 b' def test_read_full_file_tree(head):'
128
128
129
129
130 def test_topnode_files_attribute(head):
130 def test_topnode_files_attribute(head):
131 topnode = head.get_node("")
131 topnode = head.get_node(b"")
132 topnode.files
132 topnode.files
133
133
134
134
@@ -173,23 +173,23 b' class TestSVNCommit(object):'
173 self.repo = repo
173 self.repo = repo
174
174
175 def test_file_history_from_commits(self):
175 def test_file_history_from_commits(self):
176 node = self.repo[10].get_node("setup.py")
176 node = self.repo[10].get_node(b"setup.py")
177 commit_ids = [commit.raw_id for commit in node.history]
177 commit_ids = [commit.raw_id for commit in node.history]
178 assert ["8"] == commit_ids
178 assert ["8"] == commit_ids
179
179
180 node = self.repo[20].get_node("setup.py")
180 node = self.repo[20].get_node(b"setup.py")
181 node_ids = [commit.raw_id for commit in node.history]
181 node_ids = [commit.raw_id for commit in node.history]
182 assert ["18", "8"] == node_ids
182 assert ["18", "8"] == node_ids
183
183
184 # special case we check history from commit that has this particular
184 # special case we check history from commit that has this particular
185 # file changed this means we check if it's included as well
185 # file changed this means we check if it's included as well
186 node = self.repo.get_commit("18").get_node("setup.py")
186 node = self.repo.get_commit("18").get_node(b"setup.py")
187 node_ids = [commit.raw_id for commit in node.history]
187 node_ids = [commit.raw_id for commit in node.history]
188 assert ["18", "8"] == node_ids
188 assert ["18", "8"] == node_ids
189
189
190 def test_repo_files_content_type(self):
190 def test_repo_files_content_type(self):
191 test_commit = self.repo.get_commit(commit_idx=100)
191 test_commit = self.repo.get_commit(commit_idx=100)
192 for node in test_commit.get_node("/"):
192 for node in test_commit.get_node(b"/"):
193 if node.is_file():
193 if node.is_file():
194 assert type(node.content) == bytes
194 assert type(node.content) == bytes
195 assert type(node.str_content) == str
195 assert type(node.str_content) == str
@@ -30,11 +30,11 b' class TestTags(BackendTestMixin):'
30 def test_new_tag(self):
30 def test_new_tag(self):
31 tip = self.repo.get_commit()
31 tip = self.repo.get_commit()
32 tagsize = len(self.repo.tags)
32 tagsize = len(self.repo.tags)
33 tag = self.repo.tag("last-commit", "joe", tip.raw_id)
33 tag_commit = self.repo.tag("last-commit", "joe", tip.raw_id)
34
34
35 assert len(self.repo.tags) == tagsize + 1
35 assert len(self.repo.tags) == tagsize + 1
36 for top, __, __ in tip.walk():
36 for top, __, __ in tip.walk():
37 assert top == tag.get_node(top.path)
37 assert top == tag_commit.get_node(top.bytes_path)
38
38
39 def test_tag_already_exist(self):
39 def test_tag_already_exist(self):
40 tip = self.repo.get_commit()
40 tip = self.repo.get_commit()
@@ -65,7 +65,7 b' class TestVCSOperationsOnUsingBadClient('
65 # push fails repo is locked by other user !
65 # push fails repo is locked by other user !
66 push_url = rcstack.repo_clone_url(HG_REPO)
66 push_url = rcstack.repo_clone_url(HG_REPO)
67 stdout, stderr = _add_files_and_push("hg", tmpdir.strpath, clone_url=push_url)
67 stdout, stderr = _add_files_and_push("hg", tmpdir.strpath, clone_url=push_url)
68 msg = "Your hg client (ver=mercurial/proto-1.0 (Mercurial 6.7.4)) is forbidden by security rules"
68 msg = "Your hg client (version=mercurial/proto-1.0 (Mercurial 6.7.4)) is forbidden by security rules"
69 assert msg in stderr
69 assert msg in stderr
70
70
71 def test_push_with_bad_client_repo_by_other_user_git(self, rcstack, tmpdir):
71 def test_push_with_bad_client_repo_by_other_user_git(self, rcstack, tmpdir):
@@ -81,7 +81,7 b' class TestVCSOperationsOnUsingBadClient('
81 push_url = rcstack.repo_clone_url(GIT_REPO)
81 push_url = rcstack.repo_clone_url(GIT_REPO)
82 stdout, stderr = _add_files_and_push("git", tmpdir.strpath, clone_url=push_url)
82 stdout, stderr = _add_files_and_push("git", tmpdir.strpath, clone_url=push_url)
83
83
84 err = "Your git client (ver=git/2.45.2) is forbidden by security rules"
84 err = "Your git client (version=git/2.45.2) is forbidden by security rules"
85 assert err in stderr
85 assert err in stderr
86
86
87 @pytest.mark.xfail(reason="Lack of proper SVN support of cloning")
87 @pytest.mark.xfail(reason="Lack of proper SVN support of cloning")
@@ -124,7 +124,6 b' class TestVCSOperationsSVN(object):'
124
124
125 assert 'not found' in stderr
125 assert 'not found' in stderr
126
126
127 @pytest.mark.xfail(reason='Lack of proper SVN support of cloning')
128 def test_clone_existing_path_svn_not_in_database(
127 def test_clone_existing_path_svn_not_in_database(
129 self, rcstack, tmpdir, fs_repo_only):
128 self, rcstack, tmpdir, fs_repo_only):
130 db_name = fs_repo_only('not-in-db-git', repo_type='git')
129 db_name = fs_repo_only('not-in-db-git', repo_type='git')
@@ -136,7 +135,6 b' class TestVCSOperationsSVN(object):'
136 f'svn checkout {flags} {auth}', clone_url, tmpdir.strpath)
135 f'svn checkout {flags} {auth}', clone_url, tmpdir.strpath)
137 assert 'not found' in stderr
136 assert 'not found' in stderr
138
137
139 @pytest.mark.xfail(reason='Lack of proper SVN support of cloning')
140 def test_clone_existing_path_svn_not_in_database_different_scm(
138 def test_clone_existing_path_svn_not_in_database_different_scm(
141 self, rcstack, tmpdir, fs_repo_only):
139 self, rcstack, tmpdir, fs_repo_only):
142 db_name = fs_repo_only('not-in-db-hg', repo_type='hg')
140 db_name = fs_repo_only('not-in-db-hg', repo_type='hg')
@@ -149,7 +147,6 b' class TestVCSOperationsSVN(object):'
149 f'svn checkout {flags} {auth}', clone_url, tmpdir.strpath)
147 f'svn checkout {flags} {auth}', clone_url, tmpdir.strpath)
150 assert 'not found' in stderr
148 assert 'not found' in stderr
151
149
152 @pytest.mark.xfail(reason='Lack of proper SVN support of cloning')
153 def test_clone_non_existing_store_path_svn(self, rcstack, tmpdir, user_util):
150 def test_clone_non_existing_store_path_svn(self, rcstack, tmpdir, user_util):
154 repo = user_util.create_repo(repo_type='git')
151 repo = user_util.create_repo(repo_type='git')
155 clone_url = rcstack.repo_clone_url(repo.repo_name)
152 clone_url = rcstack.repo_clone_url(repo.repo_name)
General Comments 0
You need to be logged in to leave comments. Login now