##// END OF EJS Templates
docs; fixed docstring
super-admin -
r1169:14edc919 default
parent child Browse files
Show More
@@ -1,941 +1,940 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2023 RhodeCode GmbH
2 # Copyright (C) 2014-2023 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18
18
19 import os
19 import os
20 import subprocess
20 import subprocess
21 from urllib.error import URLError
21 from urllib.error import URLError
22 import urllib.parse
22 import urllib.parse
23 import logging
23 import logging
24 import posixpath as vcspath
24 import posixpath as vcspath
25 import io
25 import io
26 import urllib.request
26 import urllib.request
27 import urllib.parse
27 import urllib.parse
28 import urllib.error
28 import urllib.error
29 import traceback
29 import traceback
30
30
31
31
32 import svn.client # noqa
32 import svn.client # noqa
33 import svn.core # noqa
33 import svn.core # noqa
34 import svn.delta # noqa
34 import svn.delta # noqa
35 import svn.diff # noqa
35 import svn.diff # noqa
36 import svn.fs # noqa
36 import svn.fs # noqa
37 import svn.repos # noqa
37 import svn.repos # noqa
38
38
39 from vcsserver import svn_diff, exceptions, subprocessio, settings
39 from vcsserver import svn_diff, exceptions, subprocessio, settings
40 from vcsserver.base import (
40 from vcsserver.base import (
41 RepoFactory,
41 RepoFactory,
42 raise_from_original,
42 raise_from_original,
43 ArchiveNode,
43 ArchiveNode,
44 store_archive_in_cache,
44 store_archive_in_cache,
45 BytesEnvelope,
45 BytesEnvelope,
46 BinaryEnvelope,
46 BinaryEnvelope,
47 )
47 )
48 from vcsserver.exceptions import NoContentException
48 from vcsserver.exceptions import NoContentException
49 from vcsserver.str_utils import safe_str, safe_bytes
49 from vcsserver.str_utils import safe_str, safe_bytes
50 from vcsserver.type_utils import assert_bytes
50 from vcsserver.type_utils import assert_bytes
51 from vcsserver.vcs_base import RemoteBase
51 from vcsserver.vcs_base import RemoteBase
52 from vcsserver.lib.svnremoterepo import svnremoterepo
52 from vcsserver.lib.svnremoterepo import svnremoterepo
53
53
54 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
55
55
56
56
57 svn_compatible_versions_map = {
57 svn_compatible_versions_map = {
58 'pre-1.4-compatible': '1.3',
58 'pre-1.4-compatible': '1.3',
59 'pre-1.5-compatible': '1.4',
59 'pre-1.5-compatible': '1.4',
60 'pre-1.6-compatible': '1.5',
60 'pre-1.6-compatible': '1.5',
61 'pre-1.8-compatible': '1.7',
61 'pre-1.8-compatible': '1.7',
62 'pre-1.9-compatible': '1.8',
62 'pre-1.9-compatible': '1.8',
63 }
63 }
64
64
65 current_compatible_version = '1.14'
65 current_compatible_version = '1.14'
66
66
67
67
68 def reraise_safe_exceptions(func):
68 def reraise_safe_exceptions(func):
69 """Decorator for converting svn exceptions to something neutral."""
69 """Decorator for converting svn exceptions to something neutral."""
70 def wrapper(*args, **kwargs):
70 def wrapper(*args, **kwargs):
71 try:
71 try:
72 return func(*args, **kwargs)
72 return func(*args, **kwargs)
73 except Exception as e:
73 except Exception as e:
74 if not hasattr(e, '_vcs_kind'):
74 if not hasattr(e, '_vcs_kind'):
75 log.exception("Unhandled exception in svn remote call")
75 log.exception("Unhandled exception in svn remote call")
76 raise_from_original(exceptions.UnhandledException(e), e)
76 raise_from_original(exceptions.UnhandledException(e), e)
77 raise
77 raise
78 return wrapper
78 return wrapper
79
79
80
80
81 class SubversionFactory(RepoFactory):
81 class SubversionFactory(RepoFactory):
82 repo_type = 'svn'
82 repo_type = 'svn'
83
83
84 def _create_repo(self, wire, create, compatible_version):
84 def _create_repo(self, wire, create, compatible_version):
85 path = svn.core.svn_path_canonicalize(wire['path'])
85 path = svn.core.svn_path_canonicalize(wire['path'])
86 if create:
86 if create:
87 fs_config = {'compatible-version': current_compatible_version}
87 fs_config = {'compatible-version': current_compatible_version}
88 if compatible_version:
88 if compatible_version:
89
89
90 compatible_version_string = \
90 compatible_version_string = \
91 svn_compatible_versions_map.get(compatible_version) \
91 svn_compatible_versions_map.get(compatible_version) \
92 or compatible_version
92 or compatible_version
93 fs_config['compatible-version'] = compatible_version_string
93 fs_config['compatible-version'] = compatible_version_string
94
94
95 log.debug('Create SVN repo with config `%s`', fs_config)
95 log.debug('Create SVN repo with config `%s`', fs_config)
96 repo = svn.repos.create(path, "", "", None, fs_config)
96 repo = svn.repos.create(path, "", "", None, fs_config)
97 else:
97 else:
98 repo = svn.repos.open(path)
98 repo = svn.repos.open(path)
99
99
100 log.debug('repository created: got SVN object: %s', repo)
100 log.debug('repository created: got SVN object: %s', repo)
101 return repo
101 return repo
102
102
103 def repo(self, wire, create=False, compatible_version=None):
103 def repo(self, wire, create=False, compatible_version=None):
104 """
104 """
105 Get a repository instance for the given path.
105 Get a repository instance for the given path.
106 """
106 """
107 return self._create_repo(wire, create, compatible_version)
107 return self._create_repo(wire, create, compatible_version)
108
108
109
109
110 NODE_TYPE_MAPPING = {
110 NODE_TYPE_MAPPING = {
111 svn.core.svn_node_file: 'file',
111 svn.core.svn_node_file: 'file',
112 svn.core.svn_node_dir: 'dir',
112 svn.core.svn_node_dir: 'dir',
113 }
113 }
114
114
115
115
116 class SvnRemote(RemoteBase):
116 class SvnRemote(RemoteBase):
117
117
118 def __init__(self, factory, hg_factory=None):
118 def __init__(self, factory, hg_factory=None):
119 self._factory = factory
119 self._factory = factory
120
120
121 self._bulk_methods = {
121 self._bulk_methods = {
122 # NOT supported in SVN ATM...
122 # NOT supported in SVN ATM...
123 }
123 }
124 self._bulk_file_methods = {
124 self._bulk_file_methods = {
125 "size": self.get_file_size,
125 "size": self.get_file_size,
126 "data": self.get_file_content,
126 "data": self.get_file_content,
127 "flags": self.get_node_type,
127 "flags": self.get_node_type,
128 "is_binary": self.is_binary,
128 "is_binary": self.is_binary,
129 "md5": self.md5_hash
129 "md5": self.md5_hash
130 }
130 }
131
131
132 @reraise_safe_exceptions
132 @reraise_safe_exceptions
133 def bulk_file_request(self, wire, commit_id, path, pre_load):
133 def bulk_file_request(self, wire, commit_id, path, pre_load):
134 cache_on, context_uid, repo_id = self._cache_on(wire)
134 cache_on, context_uid, repo_id = self._cache_on(wire)
135 region = self._region(wire)
135 region = self._region(wire)
136
136
137 # since we use unified API, we need to cast from str to in for SVN
137 # since we use unified API, we need to cast from str to in for SVN
138 commit_id = int(commit_id)
138 commit_id = int(commit_id)
139
139
140 @region.conditional_cache_on_arguments(condition=cache_on)
140 @region.conditional_cache_on_arguments(condition=cache_on)
141 def _bulk_file_request(_repo_id, _commit_id, _path, _pre_load):
141 def _bulk_file_request(_repo_id, _commit_id, _path, _pre_load):
142 result = {}
142 result = {}
143 for attr in pre_load:
143 for attr in pre_load:
144 try:
144 try:
145 method = self._bulk_file_methods[attr]
145 method = self._bulk_file_methods[attr]
146 wire.update({'cache': False}) # disable cache for bulk calls so we don't double cache
146 wire.update({'cache': False}) # disable cache for bulk calls so we don't double cache
147 result[attr] = method(wire, _commit_id, _path)
147 result[attr] = method(wire, _commit_id, _path)
148 except KeyError as e:
148 except KeyError as e:
149 raise exceptions.VcsException(e)(f'Unknown bulk attribute: "{attr}"')
149 raise exceptions.VcsException(e)(f'Unknown bulk attribute: "{attr}"')
150 return result
150 return result
151
151
152 return BinaryEnvelope(_bulk_file_request(repo_id, commit_id, path, sorted(pre_load)))
152 return BinaryEnvelope(_bulk_file_request(repo_id, commit_id, path, sorted(pre_load)))
153
153
154 @reraise_safe_exceptions
154 @reraise_safe_exceptions
155 def discover_svn_version(self):
155 def discover_svn_version(self):
156 try:
156 try:
157 import svn.core
157 import svn.core
158 svn_ver = svn.core.SVN_VERSION
158 svn_ver = svn.core.SVN_VERSION
159 except ImportError:
159 except ImportError:
160 svn_ver = None
160 svn_ver = None
161 return safe_str(svn_ver)
161 return safe_str(svn_ver)
162
162
163 @reraise_safe_exceptions
163 @reraise_safe_exceptions
164 def is_empty(self, wire):
164 def is_empty(self, wire):
165 try:
165 try:
166 return self.lookup(wire, -1) == 0
166 return self.lookup(wire, -1) == 0
167 except Exception:
167 except Exception:
168 log.exception("failed to read object_store")
168 log.exception("failed to read object_store")
169 return False
169 return False
170
170
171 def check_url(self, url, config):
171 def check_url(self, url, config):
172
172
173 # uuid function gets only valid UUID from proper repo, else
173 # uuid function gets only valid UUID from proper repo, else
174 # throws exception
174 # throws exception
175 username, password, src_url = self.get_url_and_credentials(url)
175 username, password, src_url = self.get_url_and_credentials(url)
176 try:
176 try:
177 svnremoterepo(safe_bytes(username), safe_bytes(password), safe_bytes(src_url)).svn().uuid
177 svnremoterepo(safe_bytes(username), safe_bytes(password), safe_bytes(src_url)).svn().uuid
178 except Exception:
178 except Exception:
179 tb = traceback.format_exc()
179 tb = traceback.format_exc()
180 log.debug("Invalid Subversion url: `%s`, tb: %s", url, tb)
180 log.debug("Invalid Subversion url: `%s`, tb: %s", url, tb)
181 raise URLError(f'"{url}" is not a valid Subversion source url.')
181 raise URLError(f'"{url}" is not a valid Subversion source url.')
182 return True
182 return True
183
183
184 def is_path_valid_repository(self, wire, path):
184 def is_path_valid_repository(self, wire, path):
185
186 # NOTE(marcink): short circuit the check for SVN repo
185 # NOTE(marcink): short circuit the check for SVN repo
187 # the repos.open might be expensive to check, but we have one cheap
186 # the repos.open might be expensive to check, but we have one cheap
188 # pre condition that we can use, to check for 'format' file
187 # pre-condition that we can use, to check for 'format' file
189 if not os.path.isfile(os.path.join(path, 'format')):
188 if not os.path.isfile(os.path.join(path, 'format')):
190 return False
189 return False
191
190
192 try:
191 try:
193 svn.repos.open(path)
192 svn.repos.open(path)
194 except svn.core.SubversionException:
193 except svn.core.SubversionException:
195 tb = traceback.format_exc()
194 tb = traceback.format_exc()
196 log.debug("Invalid Subversion path `%s`, tb: %s", path, tb)
195 log.debug("Invalid Subversion path `%s`, tb: %s", path, tb)
197 return False
196 return False
198 return True
197 return True
199
198
200 @reraise_safe_exceptions
199 @reraise_safe_exceptions
201 def verify(self, wire,):
200 def verify(self, wire,):
202 repo_path = wire['path']
201 repo_path = wire['path']
203 if not self.is_path_valid_repository(wire, repo_path):
202 if not self.is_path_valid_repository(wire, repo_path):
204 raise Exception(
203 raise Exception(
205 f"Path {repo_path} is not a valid Subversion repository.")
204 f"Path {repo_path} is not a valid Subversion repository.")
206
205
207 cmd = ['svnadmin', 'info', repo_path]
206 cmd = ['svnadmin', 'info', repo_path]
208 stdout, stderr = subprocessio.run_command(cmd)
207 stdout, stderr = subprocessio.run_command(cmd)
209 return stdout
208 return stdout
210
209
211 @reraise_safe_exceptions
210 @reraise_safe_exceptions
212 def lookup(self, wire, revision):
211 def lookup(self, wire, revision):
213 if revision not in [-1, None, 'HEAD']:
212 if revision not in [-1, None, 'HEAD']:
214 raise NotImplementedError
213 raise NotImplementedError
215 repo = self._factory.repo(wire)
214 repo = self._factory.repo(wire)
216 fs_ptr = svn.repos.fs(repo)
215 fs_ptr = svn.repos.fs(repo)
217 head = svn.fs.youngest_rev(fs_ptr)
216 head = svn.fs.youngest_rev(fs_ptr)
218 return head
217 return head
219
218
220 @reraise_safe_exceptions
219 @reraise_safe_exceptions
221 def lookup_interval(self, wire, start_ts, end_ts):
220 def lookup_interval(self, wire, start_ts, end_ts):
222 repo = self._factory.repo(wire)
221 repo = self._factory.repo(wire)
223 fsobj = svn.repos.fs(repo)
222 fsobj = svn.repos.fs(repo)
224 start_rev = None
223 start_rev = None
225 end_rev = None
224 end_rev = None
226 if start_ts:
225 if start_ts:
227 start_ts_svn = apr_time_t(start_ts)
226 start_ts_svn = apr_time_t(start_ts)
228 start_rev = svn.repos.dated_revision(repo, start_ts_svn) + 1
227 start_rev = svn.repos.dated_revision(repo, start_ts_svn) + 1
229 else:
228 else:
230 start_rev = 1
229 start_rev = 1
231 if end_ts:
230 if end_ts:
232 end_ts_svn = apr_time_t(end_ts)
231 end_ts_svn = apr_time_t(end_ts)
233 end_rev = svn.repos.dated_revision(repo, end_ts_svn)
232 end_rev = svn.repos.dated_revision(repo, end_ts_svn)
234 else:
233 else:
235 end_rev = svn.fs.youngest_rev(fsobj)
234 end_rev = svn.fs.youngest_rev(fsobj)
236 return start_rev, end_rev
235 return start_rev, end_rev
237
236
238 @reraise_safe_exceptions
237 @reraise_safe_exceptions
239 def revision_properties(self, wire, revision):
238 def revision_properties(self, wire, revision):
240
239
241 cache_on, context_uid, repo_id = self._cache_on(wire)
240 cache_on, context_uid, repo_id = self._cache_on(wire)
242 region = self._region(wire)
241 region = self._region(wire)
243
242
244 @region.conditional_cache_on_arguments(condition=cache_on)
243 @region.conditional_cache_on_arguments(condition=cache_on)
245 def _revision_properties(_repo_id, _revision):
244 def _revision_properties(_repo_id, _revision):
246 repo = self._factory.repo(wire)
245 repo = self._factory.repo(wire)
247 fs_ptr = svn.repos.fs(repo)
246 fs_ptr = svn.repos.fs(repo)
248 return svn.fs.revision_proplist(fs_ptr, revision)
247 return svn.fs.revision_proplist(fs_ptr, revision)
249 return _revision_properties(repo_id, revision)
248 return _revision_properties(repo_id, revision)
250
249
251 def revision_changes(self, wire, revision):
250 def revision_changes(self, wire, revision):
252
251
253 repo = self._factory.repo(wire)
252 repo = self._factory.repo(wire)
254 fsobj = svn.repos.fs(repo)
253 fsobj = svn.repos.fs(repo)
255 rev_root = svn.fs.revision_root(fsobj, revision)
254 rev_root = svn.fs.revision_root(fsobj, revision)
256
255
257 editor = svn.repos.ChangeCollector(fsobj, rev_root)
256 editor = svn.repos.ChangeCollector(fsobj, rev_root)
258 editor_ptr, editor_baton = svn.delta.make_editor(editor)
257 editor_ptr, editor_baton = svn.delta.make_editor(editor)
259 base_dir = ""
258 base_dir = ""
260 send_deltas = False
259 send_deltas = False
261 svn.repos.replay2(
260 svn.repos.replay2(
262 rev_root, base_dir, svn.core.SVN_INVALID_REVNUM, send_deltas,
261 rev_root, base_dir, svn.core.SVN_INVALID_REVNUM, send_deltas,
263 editor_ptr, editor_baton, None)
262 editor_ptr, editor_baton, None)
264
263
265 added = []
264 added = []
266 changed = []
265 changed = []
267 removed = []
266 removed = []
268
267
269 # TODO: CHANGE_ACTION_REPLACE: Figure out where it belongs
268 # TODO: CHANGE_ACTION_REPLACE: Figure out where it belongs
270 for path, change in editor.changes.items():
269 for path, change in editor.changes.items():
271 # TODO: Decide what to do with directory nodes. Subversion can add
270 # TODO: Decide what to do with directory nodes. Subversion can add
272 # empty directories.
271 # empty directories.
273
272
274 if change.item_kind == svn.core.svn_node_dir:
273 if change.item_kind == svn.core.svn_node_dir:
275 continue
274 continue
276 if change.action in [svn.repos.CHANGE_ACTION_ADD]:
275 if change.action in [svn.repos.CHANGE_ACTION_ADD]:
277 added.append(path)
276 added.append(path)
278 elif change.action in [svn.repos.CHANGE_ACTION_MODIFY,
277 elif change.action in [svn.repos.CHANGE_ACTION_MODIFY,
279 svn.repos.CHANGE_ACTION_REPLACE]:
278 svn.repos.CHANGE_ACTION_REPLACE]:
280 changed.append(path)
279 changed.append(path)
281 elif change.action in [svn.repos.CHANGE_ACTION_DELETE]:
280 elif change.action in [svn.repos.CHANGE_ACTION_DELETE]:
282 removed.append(path)
281 removed.append(path)
283 else:
282 else:
284 raise NotImplementedError(
283 raise NotImplementedError(
285 "Action {} not supported on path {}".format(
284 "Action {} not supported on path {}".format(
286 change.action, path))
285 change.action, path))
287
286
288 changes = {
287 changes = {
289 'added': added,
288 'added': added,
290 'changed': changed,
289 'changed': changed,
291 'removed': removed,
290 'removed': removed,
292 }
291 }
293 return changes
292 return changes
294
293
295 @reraise_safe_exceptions
294 @reraise_safe_exceptions
296 def node_history(self, wire, path, revision, limit):
295 def node_history(self, wire, path, revision, limit):
297 cache_on, context_uid, repo_id = self._cache_on(wire)
296 cache_on, context_uid, repo_id = self._cache_on(wire)
298 region = self._region(wire)
297 region = self._region(wire)
299
298
300 @region.conditional_cache_on_arguments(condition=cache_on)
299 @region.conditional_cache_on_arguments(condition=cache_on)
301 def _assert_correct_path(_context_uid, _repo_id, _path, _revision, _limit):
300 def _assert_correct_path(_context_uid, _repo_id, _path, _revision, _limit):
302 cross_copies = False
301 cross_copies = False
303 repo = self._factory.repo(wire)
302 repo = self._factory.repo(wire)
304 fsobj = svn.repos.fs(repo)
303 fsobj = svn.repos.fs(repo)
305 rev_root = svn.fs.revision_root(fsobj, revision)
304 rev_root = svn.fs.revision_root(fsobj, revision)
306
305
307 history_revisions = []
306 history_revisions = []
308 history = svn.fs.node_history(rev_root, path)
307 history = svn.fs.node_history(rev_root, path)
309 history = svn.fs.history_prev(history, cross_copies)
308 history = svn.fs.history_prev(history, cross_copies)
310 while history:
309 while history:
311 __, node_revision = svn.fs.history_location(history)
310 __, node_revision = svn.fs.history_location(history)
312 history_revisions.append(node_revision)
311 history_revisions.append(node_revision)
313 if limit and len(history_revisions) >= limit:
312 if limit and len(history_revisions) >= limit:
314 break
313 break
315 history = svn.fs.history_prev(history, cross_copies)
314 history = svn.fs.history_prev(history, cross_copies)
316 return history_revisions
315 return history_revisions
317 return _assert_correct_path(context_uid, repo_id, path, revision, limit)
316 return _assert_correct_path(context_uid, repo_id, path, revision, limit)
318
317
319 @reraise_safe_exceptions
318 @reraise_safe_exceptions
320 def node_properties(self, wire, path, revision):
319 def node_properties(self, wire, path, revision):
321 cache_on, context_uid, repo_id = self._cache_on(wire)
320 cache_on, context_uid, repo_id = self._cache_on(wire)
322 region = self._region(wire)
321 region = self._region(wire)
323
322
324 @region.conditional_cache_on_arguments(condition=cache_on)
323 @region.conditional_cache_on_arguments(condition=cache_on)
325 def _node_properties(_repo_id, _path, _revision):
324 def _node_properties(_repo_id, _path, _revision):
326 repo = self._factory.repo(wire)
325 repo = self._factory.repo(wire)
327 fsobj = svn.repos.fs(repo)
326 fsobj = svn.repos.fs(repo)
328 rev_root = svn.fs.revision_root(fsobj, revision)
327 rev_root = svn.fs.revision_root(fsobj, revision)
329 return svn.fs.node_proplist(rev_root, path)
328 return svn.fs.node_proplist(rev_root, path)
330 return _node_properties(repo_id, path, revision)
329 return _node_properties(repo_id, path, revision)
331
330
332 def file_annotate(self, wire, path, revision):
331 def file_annotate(self, wire, path, revision):
333 abs_path = 'file://' + urllib.request.pathname2url(
332 abs_path = 'file://' + urllib.request.pathname2url(
334 vcspath.join(wire['path'], path))
333 vcspath.join(wire['path'], path))
335 file_uri = svn.core.svn_path_canonicalize(abs_path)
334 file_uri = svn.core.svn_path_canonicalize(abs_path)
336
335
337 start_rev = svn_opt_revision_value_t(0)
336 start_rev = svn_opt_revision_value_t(0)
338 peg_rev = svn_opt_revision_value_t(revision)
337 peg_rev = svn_opt_revision_value_t(revision)
339 end_rev = peg_rev
338 end_rev = peg_rev
340
339
341 annotations = []
340 annotations = []
342
341
343 def receiver(line_no, revision, author, date, line, pool):
342 def receiver(line_no, revision, author, date, line, pool):
344 annotations.append((line_no, revision, line))
343 annotations.append((line_no, revision, line))
345
344
346 # TODO: Cannot use blame5, missing typemap function in the swig code
345 # TODO: Cannot use blame5, missing typemap function in the swig code
347 try:
346 try:
348 svn.client.blame2(
347 svn.client.blame2(
349 file_uri, peg_rev, start_rev, end_rev,
348 file_uri, peg_rev, start_rev, end_rev,
350 receiver, svn.client.create_context())
349 receiver, svn.client.create_context())
351 except svn.core.SubversionException as exc:
350 except svn.core.SubversionException as exc:
352 log.exception("Error during blame operation.")
351 log.exception("Error during blame operation.")
353 raise Exception(
352 raise Exception(
354 f"Blame not supported or file does not exist at path {path}. "
353 f"Blame not supported or file does not exist at path {path}. "
355 f"Error {exc}.")
354 f"Error {exc}.")
356
355
357 return BinaryEnvelope(annotations)
356 return BinaryEnvelope(annotations)
358
357
359 @reraise_safe_exceptions
358 @reraise_safe_exceptions
360 def get_node_type(self, wire, revision=None, path=''):
359 def get_node_type(self, wire, revision=None, path=''):
361
360
362 cache_on, context_uid, repo_id = self._cache_on(wire)
361 cache_on, context_uid, repo_id = self._cache_on(wire)
363 region = self._region(wire)
362 region = self._region(wire)
364
363
365 @region.conditional_cache_on_arguments(condition=cache_on)
364 @region.conditional_cache_on_arguments(condition=cache_on)
366 def _get_node_type(_repo_id, _revision, _path):
365 def _get_node_type(_repo_id, _revision, _path):
367 repo = self._factory.repo(wire)
366 repo = self._factory.repo(wire)
368 fs_ptr = svn.repos.fs(repo)
367 fs_ptr = svn.repos.fs(repo)
369 if _revision is None:
368 if _revision is None:
370 _revision = svn.fs.youngest_rev(fs_ptr)
369 _revision = svn.fs.youngest_rev(fs_ptr)
371 root = svn.fs.revision_root(fs_ptr, _revision)
370 root = svn.fs.revision_root(fs_ptr, _revision)
372 node = svn.fs.check_path(root, path)
371 node = svn.fs.check_path(root, path)
373 return NODE_TYPE_MAPPING.get(node, None)
372 return NODE_TYPE_MAPPING.get(node, None)
374 return _get_node_type(repo_id, revision, path)
373 return _get_node_type(repo_id, revision, path)
375
374
376 @reraise_safe_exceptions
375 @reraise_safe_exceptions
377 def get_nodes(self, wire, revision=None, path=''):
376 def get_nodes(self, wire, revision=None, path=''):
378
377
379 cache_on, context_uid, repo_id = self._cache_on(wire)
378 cache_on, context_uid, repo_id = self._cache_on(wire)
380 region = self._region(wire)
379 region = self._region(wire)
381
380
382 @region.conditional_cache_on_arguments(condition=cache_on)
381 @region.conditional_cache_on_arguments(condition=cache_on)
383 def _get_nodes(_repo_id, _path, _revision):
382 def _get_nodes(_repo_id, _path, _revision):
384 repo = self._factory.repo(wire)
383 repo = self._factory.repo(wire)
385 fsobj = svn.repos.fs(repo)
384 fsobj = svn.repos.fs(repo)
386 if _revision is None:
385 if _revision is None:
387 _revision = svn.fs.youngest_rev(fsobj)
386 _revision = svn.fs.youngest_rev(fsobj)
388 root = svn.fs.revision_root(fsobj, _revision)
387 root = svn.fs.revision_root(fsobj, _revision)
389 entries = svn.fs.dir_entries(root, path)
388 entries = svn.fs.dir_entries(root, path)
390 result = []
389 result = []
391 for entry_path, entry_info in entries.items():
390 for entry_path, entry_info in entries.items():
392 result.append(
391 result.append(
393 (entry_path, NODE_TYPE_MAPPING.get(entry_info.kind, None)))
392 (entry_path, NODE_TYPE_MAPPING.get(entry_info.kind, None)))
394 return result
393 return result
395 return _get_nodes(repo_id, path, revision)
394 return _get_nodes(repo_id, path, revision)
396
395
397 @reraise_safe_exceptions
396 @reraise_safe_exceptions
398 def get_file_content(self, wire, rev=None, path=''):
397 def get_file_content(self, wire, rev=None, path=''):
399 repo = self._factory.repo(wire)
398 repo = self._factory.repo(wire)
400 fsobj = svn.repos.fs(repo)
399 fsobj = svn.repos.fs(repo)
401
400
402 if rev is None:
401 if rev is None:
403 rev = svn.fs.youngest_rev(fsobj)
402 rev = svn.fs.youngest_rev(fsobj)
404
403
405 root = svn.fs.revision_root(fsobj, rev)
404 root = svn.fs.revision_root(fsobj, rev)
406 content = svn.core.Stream(svn.fs.file_contents(root, path))
405 content = svn.core.Stream(svn.fs.file_contents(root, path))
407 return BytesEnvelope(content.read())
406 return BytesEnvelope(content.read())
408
407
409 @reraise_safe_exceptions
408 @reraise_safe_exceptions
410 def get_file_size(self, wire, revision=None, path=''):
409 def get_file_size(self, wire, revision=None, path=''):
411
410
412 cache_on, context_uid, repo_id = self._cache_on(wire)
411 cache_on, context_uid, repo_id = self._cache_on(wire)
413 region = self._region(wire)
412 region = self._region(wire)
414
413
415 @region.conditional_cache_on_arguments(condition=cache_on)
414 @region.conditional_cache_on_arguments(condition=cache_on)
416 def _get_file_size(_repo_id, _revision, _path):
415 def _get_file_size(_repo_id, _revision, _path):
417 repo = self._factory.repo(wire)
416 repo = self._factory.repo(wire)
418 fsobj = svn.repos.fs(repo)
417 fsobj = svn.repos.fs(repo)
419 if _revision is None:
418 if _revision is None:
420 _revision = svn.fs.youngest_revision(fsobj)
419 _revision = svn.fs.youngest_revision(fsobj)
421 root = svn.fs.revision_root(fsobj, _revision)
420 root = svn.fs.revision_root(fsobj, _revision)
422 size = svn.fs.file_length(root, path)
421 size = svn.fs.file_length(root, path)
423 return size
422 return size
424 return _get_file_size(repo_id, revision, path)
423 return _get_file_size(repo_id, revision, path)
425
424
426 def create_repository(self, wire, compatible_version=None):
425 def create_repository(self, wire, compatible_version=None):
427 log.info('Creating Subversion repository in path "%s"', wire['path'])
426 log.info('Creating Subversion repository in path "%s"', wire['path'])
428 self._factory.repo(wire, create=True,
427 self._factory.repo(wire, create=True,
429 compatible_version=compatible_version)
428 compatible_version=compatible_version)
430
429
431 def get_url_and_credentials(self, src_url) -> tuple[str, str, str]:
430 def get_url_and_credentials(self, src_url) -> tuple[str, str, str]:
432 obj = urllib.parse.urlparse(src_url)
431 obj = urllib.parse.urlparse(src_url)
433 username = obj.username or ''
432 username = obj.username or ''
434 password = obj.password or ''
433 password = obj.password or ''
435 return username, password, src_url
434 return username, password, src_url
436
435
437 def import_remote_repository(self, wire, src_url):
436 def import_remote_repository(self, wire, src_url):
438 repo_path = wire['path']
437 repo_path = wire['path']
439 if not self.is_path_valid_repository(wire, repo_path):
438 if not self.is_path_valid_repository(wire, repo_path):
440 raise Exception(
439 raise Exception(
441 f"Path {repo_path} is not a valid Subversion repository.")
440 f"Path {repo_path} is not a valid Subversion repository.")
442
441
443 username, password, src_url = self.get_url_and_credentials(src_url)
442 username, password, src_url = self.get_url_and_credentials(src_url)
444 rdump_cmd = ['svnrdump', 'dump', '--non-interactive',
443 rdump_cmd = ['svnrdump', 'dump', '--non-interactive',
445 '--trust-server-cert-failures=unknown-ca']
444 '--trust-server-cert-failures=unknown-ca']
446 if username and password:
445 if username and password:
447 rdump_cmd += ['--username', username, '--password', password]
446 rdump_cmd += ['--username', username, '--password', password]
448 rdump_cmd += [src_url]
447 rdump_cmd += [src_url]
449
448
450 rdump = subprocess.Popen(
449 rdump = subprocess.Popen(
451 rdump_cmd,
450 rdump_cmd,
452 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
451 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
453 load = subprocess.Popen(
452 load = subprocess.Popen(
454 ['svnadmin', 'load', repo_path], stdin=rdump.stdout)
453 ['svnadmin', 'load', repo_path], stdin=rdump.stdout)
455
454
456 # TODO: johbo: This can be a very long operation, might be better
455 # TODO: johbo: This can be a very long operation, might be better
457 # to track some kind of status and provide an api to check if the
456 # to track some kind of status and provide an api to check if the
458 # import is done.
457 # import is done.
459 rdump.wait()
458 rdump.wait()
460 load.wait()
459 load.wait()
461
460
462 log.debug('Return process ended with code: %s', rdump.returncode)
461 log.debug('Return process ended with code: %s', rdump.returncode)
463 if rdump.returncode != 0:
462 if rdump.returncode != 0:
464 errors = rdump.stderr.read()
463 errors = rdump.stderr.read()
465 log.error('svnrdump dump failed: statuscode %s: message: %s', rdump.returncode, errors)
464 log.error('svnrdump dump failed: statuscode %s: message: %s', rdump.returncode, errors)
466
465
467 reason = 'UNKNOWN'
466 reason = 'UNKNOWN'
468 if b'svnrdump: E230001:' in errors:
467 if b'svnrdump: E230001:' in errors:
469 reason = 'INVALID_CERTIFICATE'
468 reason = 'INVALID_CERTIFICATE'
470
469
471 if reason == 'UNKNOWN':
470 if reason == 'UNKNOWN':
472 reason = f'UNKNOWN:{safe_str(errors)}'
471 reason = f'UNKNOWN:{safe_str(errors)}'
473
472
474 raise Exception(
473 raise Exception(
475 'Failed to dump the remote repository from {}. Reason:{}'.format(
474 'Failed to dump the remote repository from {}. Reason:{}'.format(
476 src_url, reason))
475 src_url, reason))
477 if load.returncode != 0:
476 if load.returncode != 0:
478 raise Exception(
477 raise Exception(
479 f'Failed to load the dump of remote repository from {src_url}.')
478 f'Failed to load the dump of remote repository from {src_url}.')
480
479
481 def commit(self, wire, message, author, timestamp, updated, removed):
480 def commit(self, wire, message, author, timestamp, updated, removed):
482
481
483 message = safe_bytes(message)
482 message = safe_bytes(message)
484 author = safe_bytes(author)
483 author = safe_bytes(author)
485
484
486 repo = self._factory.repo(wire)
485 repo = self._factory.repo(wire)
487 fsobj = svn.repos.fs(repo)
486 fsobj = svn.repos.fs(repo)
488
487
489 rev = svn.fs.youngest_rev(fsobj)
488 rev = svn.fs.youngest_rev(fsobj)
490 txn = svn.repos.fs_begin_txn_for_commit(repo, rev, author, message)
489 txn = svn.repos.fs_begin_txn_for_commit(repo, rev, author, message)
491 txn_root = svn.fs.txn_root(txn)
490 txn_root = svn.fs.txn_root(txn)
492
491
493 for node in updated:
492 for node in updated:
494 TxnNodeProcessor(node, txn_root).update()
493 TxnNodeProcessor(node, txn_root).update()
495 for node in removed:
494 for node in removed:
496 TxnNodeProcessor(node, txn_root).remove()
495 TxnNodeProcessor(node, txn_root).remove()
497
496
498 commit_id = svn.repos.fs_commit_txn(repo, txn)
497 commit_id = svn.repos.fs_commit_txn(repo, txn)
499
498
500 if timestamp:
499 if timestamp:
501 apr_time = apr_time_t(timestamp)
500 apr_time = apr_time_t(timestamp)
502 ts_formatted = svn.core.svn_time_to_cstring(apr_time)
501 ts_formatted = svn.core.svn_time_to_cstring(apr_time)
503 svn.fs.change_rev_prop(fsobj, commit_id, 'svn:date', ts_formatted)
502 svn.fs.change_rev_prop(fsobj, commit_id, 'svn:date', ts_formatted)
504
503
505 log.debug('Committed revision "%s" to "%s".', commit_id, wire['path'])
504 log.debug('Committed revision "%s" to "%s".', commit_id, wire['path'])
506 return commit_id
505 return commit_id
507
506
508 @reraise_safe_exceptions
507 @reraise_safe_exceptions
509 def diff(self, wire, rev1, rev2, path1=None, path2=None,
508 def diff(self, wire, rev1, rev2, path1=None, path2=None,
510 ignore_whitespace=False, context=3):
509 ignore_whitespace=False, context=3):
511
510
512 wire.update(cache=False)
511 wire.update(cache=False)
513 repo = self._factory.repo(wire)
512 repo = self._factory.repo(wire)
514 diff_creator = SvnDiffer(
513 diff_creator = SvnDiffer(
515 repo, rev1, path1, rev2, path2, ignore_whitespace, context)
514 repo, rev1, path1, rev2, path2, ignore_whitespace, context)
516 try:
515 try:
517 return BytesEnvelope(diff_creator.generate_diff())
516 return BytesEnvelope(diff_creator.generate_diff())
518 except svn.core.SubversionException as e:
517 except svn.core.SubversionException as e:
519 log.exception(
518 log.exception(
520 "Error during diff operation operation. "
519 "Error during diff operation operation. "
521 "Path might not exist %s, %s", path1, path2)
520 "Path might not exist %s, %s", path1, path2)
522 return BytesEnvelope(b'')
521 return BytesEnvelope(b'')
523
522
524 @reraise_safe_exceptions
523 @reraise_safe_exceptions
525 def is_large_file(self, wire, path):
524 def is_large_file(self, wire, path):
526 return False
525 return False
527
526
528 @reraise_safe_exceptions
527 @reraise_safe_exceptions
529 def is_binary(self, wire, rev, path):
528 def is_binary(self, wire, rev, path):
530 cache_on, context_uid, repo_id = self._cache_on(wire)
529 cache_on, context_uid, repo_id = self._cache_on(wire)
531 region = self._region(wire)
530 region = self._region(wire)
532
531
533 @region.conditional_cache_on_arguments(condition=cache_on)
532 @region.conditional_cache_on_arguments(condition=cache_on)
534 def _is_binary(_repo_id, _rev, _path):
533 def _is_binary(_repo_id, _rev, _path):
535 raw_bytes = self.get_file_content(wire, rev, path)
534 raw_bytes = self.get_file_content(wire, rev, path)
536 if not raw_bytes:
535 if not raw_bytes:
537 return False
536 return False
538 return b'\0' in raw_bytes
537 return b'\0' in raw_bytes
539
538
540 return _is_binary(repo_id, rev, path)
539 return _is_binary(repo_id, rev, path)
541
540
542 @reraise_safe_exceptions
541 @reraise_safe_exceptions
543 def md5_hash(self, wire, rev, path):
542 def md5_hash(self, wire, rev, path):
544 cache_on, context_uid, repo_id = self._cache_on(wire)
543 cache_on, context_uid, repo_id = self._cache_on(wire)
545 region = self._region(wire)
544 region = self._region(wire)
546
545
547 @region.conditional_cache_on_arguments(condition=cache_on)
546 @region.conditional_cache_on_arguments(condition=cache_on)
548 def _md5_hash(_repo_id, _rev, _path):
547 def _md5_hash(_repo_id, _rev, _path):
549 return ''
548 return ''
550
549
551 return _md5_hash(repo_id, rev, path)
550 return _md5_hash(repo_id, rev, path)
552
551
553 @reraise_safe_exceptions
552 @reraise_safe_exceptions
554 def run_svn_command(self, wire, cmd, **opts):
553 def run_svn_command(self, wire, cmd, **opts):
555 path = wire.get('path', None)
554 path = wire.get('path', None)
556
555
557 if path and os.path.isdir(path):
556 if path and os.path.isdir(path):
558 opts['cwd'] = path
557 opts['cwd'] = path
559
558
560 safe_call = opts.pop('_safe', False)
559 safe_call = opts.pop('_safe', False)
561
560
562 svnenv = os.environ.copy()
561 svnenv = os.environ.copy()
563 svnenv.update(opts.pop('extra_env', {}))
562 svnenv.update(opts.pop('extra_env', {}))
564
563
565 _opts = {'env': svnenv, 'shell': False}
564 _opts = {'env': svnenv, 'shell': False}
566
565
567 try:
566 try:
568 _opts.update(opts)
567 _opts.update(opts)
569 proc = subprocessio.SubprocessIOChunker(cmd, **_opts)
568 proc = subprocessio.SubprocessIOChunker(cmd, **_opts)
570
569
571 return b''.join(proc), b''.join(proc.stderr)
570 return b''.join(proc), b''.join(proc.stderr)
572 except OSError as err:
571 except OSError as err:
573 if safe_call:
572 if safe_call:
574 return '', safe_str(err).strip()
573 return '', safe_str(err).strip()
575 else:
574 else:
576 cmd = ' '.join(map(safe_str, cmd)) # human friendly CMD
575 cmd = ' '.join(map(safe_str, cmd)) # human friendly CMD
577 tb_err = ("Couldn't run svn command (%s).\n"
576 tb_err = ("Couldn't run svn command (%s).\n"
578 "Original error was:%s\n"
577 "Original error was:%s\n"
579 "Call options:%s\n"
578 "Call options:%s\n"
580 % (cmd, err, _opts))
579 % (cmd, err, _opts))
581 log.exception(tb_err)
580 log.exception(tb_err)
582 raise exceptions.VcsException()(tb_err)
581 raise exceptions.VcsException()(tb_err)
583
582
584 @reraise_safe_exceptions
583 @reraise_safe_exceptions
585 def install_hooks(self, wire, force=False):
584 def install_hooks(self, wire, force=False):
586 from vcsserver.hook_utils import install_svn_hooks
585 from vcsserver.hook_utils import install_svn_hooks
587 repo_path = wire['path']
586 repo_path = wire['path']
588 binary_dir = settings.BINARY_DIR
587 binary_dir = settings.BINARY_DIR
589 executable = None
588 executable = None
590 if binary_dir:
589 if binary_dir:
591 executable = os.path.join(binary_dir, 'python3')
590 executable = os.path.join(binary_dir, 'python3')
592 return install_svn_hooks(repo_path, force_create=force)
591 return install_svn_hooks(repo_path, force_create=force)
593
592
594 @reraise_safe_exceptions
593 @reraise_safe_exceptions
595 def get_hooks_info(self, wire):
594 def get_hooks_info(self, wire):
596 from vcsserver.hook_utils import (
595 from vcsserver.hook_utils import (
597 get_svn_pre_hook_version, get_svn_post_hook_version)
596 get_svn_pre_hook_version, get_svn_post_hook_version)
598 repo_path = wire['path']
597 repo_path = wire['path']
599 return {
598 return {
600 'pre_version': get_svn_pre_hook_version(repo_path),
599 'pre_version': get_svn_pre_hook_version(repo_path),
601 'post_version': get_svn_post_hook_version(repo_path),
600 'post_version': get_svn_post_hook_version(repo_path),
602 }
601 }
603
602
604 @reraise_safe_exceptions
603 @reraise_safe_exceptions
605 def set_head_ref(self, wire, head_name):
604 def set_head_ref(self, wire, head_name):
606 pass
605 pass
607
606
608 @reraise_safe_exceptions
607 @reraise_safe_exceptions
609 def archive_repo(self, wire, archive_name_key, kind, mtime, archive_at_path,
608 def archive_repo(self, wire, archive_name_key, kind, mtime, archive_at_path,
610 archive_dir_name, commit_id, cache_config):
609 archive_dir_name, commit_id, cache_config):
611
610
612 def walk_tree(root, root_dir, _commit_id):
611 def walk_tree(root, root_dir, _commit_id):
613 """
612 """
614 Special recursive svn repo walker
613 Special recursive svn repo walker
615 """
614 """
616 root_dir = safe_bytes(root_dir)
615 root_dir = safe_bytes(root_dir)
617
616
618 filemode_default = 0o100644
617 filemode_default = 0o100644
619 filemode_executable = 0o100755
618 filemode_executable = 0o100755
620
619
621 file_iter = svn.fs.dir_entries(root, root_dir)
620 file_iter = svn.fs.dir_entries(root, root_dir)
622 for f_name in file_iter:
621 for f_name in file_iter:
623 f_type = NODE_TYPE_MAPPING.get(file_iter[f_name].kind, None)
622 f_type = NODE_TYPE_MAPPING.get(file_iter[f_name].kind, None)
624
623
625 if f_type == 'dir':
624 if f_type == 'dir':
626 # return only DIR, and then all entries in that dir
625 # return only DIR, and then all entries in that dir
627 yield os.path.join(root_dir, f_name), {'mode': filemode_default}, f_type
626 yield os.path.join(root_dir, f_name), {'mode': filemode_default}, f_type
628 new_root = os.path.join(root_dir, f_name)
627 new_root = os.path.join(root_dir, f_name)
629 yield from walk_tree(root, new_root, _commit_id)
628 yield from walk_tree(root, new_root, _commit_id)
630 else:
629 else:
631
630
632 f_path = os.path.join(root_dir, f_name).rstrip(b'/')
631 f_path = os.path.join(root_dir, f_name).rstrip(b'/')
633 prop_list = svn.fs.node_proplist(root, f_path)
632 prop_list = svn.fs.node_proplist(root, f_path)
634
633
635 f_mode = filemode_default
634 f_mode = filemode_default
636 if prop_list.get('svn:executable'):
635 if prop_list.get('svn:executable'):
637 f_mode = filemode_executable
636 f_mode = filemode_executable
638
637
639 f_is_link = False
638 f_is_link = False
640 if prop_list.get('svn:special'):
639 if prop_list.get('svn:special'):
641 f_is_link = True
640 f_is_link = True
642
641
643 data = {
642 data = {
644 'is_link': f_is_link,
643 'is_link': f_is_link,
645 'mode': f_mode,
644 'mode': f_mode,
646 'content_stream': svn.core.Stream(svn.fs.file_contents(root, f_path)).read
645 'content_stream': svn.core.Stream(svn.fs.file_contents(root, f_path)).read
647 }
646 }
648
647
649 yield f_path, data, f_type
648 yield f_path, data, f_type
650
649
651 def file_walker(_commit_id, path):
650 def file_walker(_commit_id, path):
652 repo = self._factory.repo(wire)
651 repo = self._factory.repo(wire)
653 root = svn.fs.revision_root(svn.repos.fs(repo), int(commit_id))
652 root = svn.fs.revision_root(svn.repos.fs(repo), int(commit_id))
654
653
655 def no_content():
654 def no_content():
656 raise NoContentException()
655 raise NoContentException()
657
656
658 for f_name, f_data, f_type in walk_tree(root, path, _commit_id):
657 for f_name, f_data, f_type in walk_tree(root, path, _commit_id):
659 file_path = f_name
658 file_path = f_name
660
659
661 if f_type == 'dir':
660 if f_type == 'dir':
662 mode = f_data['mode']
661 mode = f_data['mode']
663 yield ArchiveNode(file_path, mode, False, no_content)
662 yield ArchiveNode(file_path, mode, False, no_content)
664 else:
663 else:
665 mode = f_data['mode']
664 mode = f_data['mode']
666 is_link = f_data['is_link']
665 is_link = f_data['is_link']
667 data_stream = f_data['content_stream']
666 data_stream = f_data['content_stream']
668 yield ArchiveNode(file_path, mode, is_link, data_stream)
667 yield ArchiveNode(file_path, mode, is_link, data_stream)
669
668
670 return store_archive_in_cache(
669 return store_archive_in_cache(
671 file_walker, archive_name_key, kind, mtime, archive_at_path, archive_dir_name, commit_id, cache_config=cache_config)
670 file_walker, archive_name_key, kind, mtime, archive_at_path, archive_dir_name, commit_id, cache_config=cache_config)
672
671
673
672
674 class SvnDiffer:
673 class SvnDiffer:
675 """
674 """
676 Utility to create diffs based on difflib and the Subversion api
675 Utility to create diffs based on difflib and the Subversion api
677 """
676 """
678
677
679 binary_content = False
678 binary_content = False
680
679
681 def __init__(
680 def __init__(
682 self, repo, src_rev, src_path, tgt_rev, tgt_path,
681 self, repo, src_rev, src_path, tgt_rev, tgt_path,
683 ignore_whitespace, context):
682 ignore_whitespace, context):
684 self.repo = repo
683 self.repo = repo
685 self.ignore_whitespace = ignore_whitespace
684 self.ignore_whitespace = ignore_whitespace
686 self.context = context
685 self.context = context
687
686
688 fsobj = svn.repos.fs(repo)
687 fsobj = svn.repos.fs(repo)
689
688
690 self.tgt_rev = tgt_rev
689 self.tgt_rev = tgt_rev
691 self.tgt_path = tgt_path or ''
690 self.tgt_path = tgt_path or ''
692 self.tgt_root = svn.fs.revision_root(fsobj, tgt_rev)
691 self.tgt_root = svn.fs.revision_root(fsobj, tgt_rev)
693 self.tgt_kind = svn.fs.check_path(self.tgt_root, self.tgt_path)
692 self.tgt_kind = svn.fs.check_path(self.tgt_root, self.tgt_path)
694
693
695 self.src_rev = src_rev
694 self.src_rev = src_rev
696 self.src_path = src_path or self.tgt_path
695 self.src_path = src_path or self.tgt_path
697 self.src_root = svn.fs.revision_root(fsobj, src_rev)
696 self.src_root = svn.fs.revision_root(fsobj, src_rev)
698 self.src_kind = svn.fs.check_path(self.src_root, self.src_path)
697 self.src_kind = svn.fs.check_path(self.src_root, self.src_path)
699
698
700 self._validate()
699 self._validate()
701
700
702 def _validate(self):
701 def _validate(self):
703 if (self.tgt_kind != svn.core.svn_node_none and
702 if (self.tgt_kind != svn.core.svn_node_none and
704 self.src_kind != svn.core.svn_node_none and
703 self.src_kind != svn.core.svn_node_none and
705 self.src_kind != self.tgt_kind):
704 self.src_kind != self.tgt_kind):
706 # TODO: johbo: proper error handling
705 # TODO: johbo: proper error handling
707 raise Exception(
706 raise Exception(
708 "Source and target are not compatible for diff generation. "
707 "Source and target are not compatible for diff generation. "
709 "Source type: %s, target type: %s" %
708 "Source type: %s, target type: %s" %
710 (self.src_kind, self.tgt_kind))
709 (self.src_kind, self.tgt_kind))
711
710
712 def generate_diff(self) -> bytes:
711 def generate_diff(self) -> bytes:
713 buf = io.BytesIO()
712 buf = io.BytesIO()
714 if self.tgt_kind == svn.core.svn_node_dir:
713 if self.tgt_kind == svn.core.svn_node_dir:
715 self._generate_dir_diff(buf)
714 self._generate_dir_diff(buf)
716 else:
715 else:
717 self._generate_file_diff(buf)
716 self._generate_file_diff(buf)
718 return buf.getvalue()
717 return buf.getvalue()
719
718
720 def _generate_dir_diff(self, buf: io.BytesIO):
719 def _generate_dir_diff(self, buf: io.BytesIO):
721 editor = DiffChangeEditor()
720 editor = DiffChangeEditor()
722 editor_ptr, editor_baton = svn.delta.make_editor(editor)
721 editor_ptr, editor_baton = svn.delta.make_editor(editor)
723 svn.repos.dir_delta2(
722 svn.repos.dir_delta2(
724 self.src_root,
723 self.src_root,
725 self.src_path,
724 self.src_path,
726 '', # src_entry
725 '', # src_entry
727 self.tgt_root,
726 self.tgt_root,
728 self.tgt_path,
727 self.tgt_path,
729 editor_ptr, editor_baton,
728 editor_ptr, editor_baton,
730 authorization_callback_allow_all,
729 authorization_callback_allow_all,
731 False, # text_deltas
730 False, # text_deltas
732 svn.core.svn_depth_infinity, # depth
731 svn.core.svn_depth_infinity, # depth
733 False, # entry_props
732 False, # entry_props
734 False, # ignore_ancestry
733 False, # ignore_ancestry
735 )
734 )
736
735
737 for path, __, change in sorted(editor.changes):
736 for path, __, change in sorted(editor.changes):
738 self._generate_node_diff(
737 self._generate_node_diff(
739 buf, change, path, self.tgt_path, path, self.src_path)
738 buf, change, path, self.tgt_path, path, self.src_path)
740
739
741 def _generate_file_diff(self, buf: io.BytesIO):
740 def _generate_file_diff(self, buf: io.BytesIO):
742 change = None
741 change = None
743 if self.src_kind == svn.core.svn_node_none:
742 if self.src_kind == svn.core.svn_node_none:
744 change = "add"
743 change = "add"
745 elif self.tgt_kind == svn.core.svn_node_none:
744 elif self.tgt_kind == svn.core.svn_node_none:
746 change = "delete"
745 change = "delete"
747 tgt_base, tgt_path = vcspath.split(self.tgt_path)
746 tgt_base, tgt_path = vcspath.split(self.tgt_path)
748 src_base, src_path = vcspath.split(self.src_path)
747 src_base, src_path = vcspath.split(self.src_path)
749 self._generate_node_diff(
748 self._generate_node_diff(
750 buf, change, tgt_path, tgt_base, src_path, src_base)
749 buf, change, tgt_path, tgt_base, src_path, src_base)
751
750
752 def _generate_node_diff(
751 def _generate_node_diff(
753 self, buf: io.BytesIO, change, tgt_path, tgt_base, src_path, src_base):
752 self, buf: io.BytesIO, change, tgt_path, tgt_base, src_path, src_base):
754
753
755 tgt_path_bytes = safe_bytes(tgt_path)
754 tgt_path_bytes = safe_bytes(tgt_path)
756 tgt_path = safe_str(tgt_path)
755 tgt_path = safe_str(tgt_path)
757
756
758 src_path_bytes = safe_bytes(src_path)
757 src_path_bytes = safe_bytes(src_path)
759 src_path = safe_str(src_path)
758 src_path = safe_str(src_path)
760
759
761 if self.src_rev == self.tgt_rev and tgt_base == src_base:
760 if self.src_rev == self.tgt_rev and tgt_base == src_base:
762 # makes consistent behaviour with git/hg to return empty diff if
761 # makes consistent behaviour with git/hg to return empty diff if
763 # we compare same revisions
762 # we compare same revisions
764 return
763 return
765
764
766 tgt_full_path = vcspath.join(tgt_base, tgt_path)
765 tgt_full_path = vcspath.join(tgt_base, tgt_path)
767 src_full_path = vcspath.join(src_base, src_path)
766 src_full_path = vcspath.join(src_base, src_path)
768
767
769 self.binary_content = False
768 self.binary_content = False
770 mime_type = self._get_mime_type(tgt_full_path)
769 mime_type = self._get_mime_type(tgt_full_path)
771
770
772 if mime_type and not mime_type.startswith(b'text'):
771 if mime_type and not mime_type.startswith(b'text'):
773 self.binary_content = True
772 self.binary_content = True
774 buf.write(b"=" * 67 + b'\n')
773 buf.write(b"=" * 67 + b'\n')
775 buf.write(b"Cannot display: file marked as a binary type.\n")
774 buf.write(b"Cannot display: file marked as a binary type.\n")
776 buf.write(b"svn:mime-type = %s\n" % mime_type)
775 buf.write(b"svn:mime-type = %s\n" % mime_type)
777 buf.write(b"Index: %b\n" % tgt_path_bytes)
776 buf.write(b"Index: %b\n" % tgt_path_bytes)
778 buf.write(b"=" * 67 + b'\n')
777 buf.write(b"=" * 67 + b'\n')
779 buf.write(b"diff --git a/%b b/%b\n" % (tgt_path_bytes, tgt_path_bytes))
778 buf.write(b"diff --git a/%b b/%b\n" % (tgt_path_bytes, tgt_path_bytes))
780
779
781 if change == 'add':
780 if change == 'add':
782 # TODO: johbo: SVN is missing a zero here compared to git
781 # TODO: johbo: SVN is missing a zero here compared to git
783 buf.write(b"new file mode 10644\n")
782 buf.write(b"new file mode 10644\n")
784
783
785 # TODO(marcink): intro to binary detection of svn patches
784 # TODO(marcink): intro to binary detection of svn patches
786 # if self.binary_content:
785 # if self.binary_content:
787 # buf.write(b'GIT binary patch\n')
786 # buf.write(b'GIT binary patch\n')
788
787
789 buf.write(b"--- /dev/null\t(revision 0)\n")
788 buf.write(b"--- /dev/null\t(revision 0)\n")
790 src_lines = []
789 src_lines = []
791 else:
790 else:
792 if change == 'delete':
791 if change == 'delete':
793 buf.write(b"deleted file mode 10644\n")
792 buf.write(b"deleted file mode 10644\n")
794
793
795 # TODO(marcink): intro to binary detection of svn patches
794 # TODO(marcink): intro to binary detection of svn patches
796 # if self.binary_content:
795 # if self.binary_content:
797 # buf.write('GIT binary patch\n')
796 # buf.write('GIT binary patch\n')
798
797
799 buf.write(b"--- a/%b\t(revision %d)\n" % (src_path_bytes, self.src_rev))
798 buf.write(b"--- a/%b\t(revision %d)\n" % (src_path_bytes, self.src_rev))
800 src_lines = self._svn_readlines(self.src_root, src_full_path)
799 src_lines = self._svn_readlines(self.src_root, src_full_path)
801
800
802 if change == 'delete':
801 if change == 'delete':
803 buf.write(b"+++ /dev/null\t(revision %d)\n" % self.tgt_rev)
802 buf.write(b"+++ /dev/null\t(revision %d)\n" % self.tgt_rev)
804 tgt_lines = []
803 tgt_lines = []
805 else:
804 else:
806 buf.write(b"+++ b/%b\t(revision %d)\n" % (tgt_path_bytes, self.tgt_rev))
805 buf.write(b"+++ b/%b\t(revision %d)\n" % (tgt_path_bytes, self.tgt_rev))
807 tgt_lines = self._svn_readlines(self.tgt_root, tgt_full_path)
806 tgt_lines = self._svn_readlines(self.tgt_root, tgt_full_path)
808
807
809 # we made our diff header, time to generate the diff content into our buffer
808 # we made our diff header, time to generate the diff content into our buffer
810
809
811 if not self.binary_content:
810 if not self.binary_content:
812 udiff = svn_diff.unified_diff(
811 udiff = svn_diff.unified_diff(
813 src_lines, tgt_lines, context=self.context,
812 src_lines, tgt_lines, context=self.context,
814 ignore_blank_lines=self.ignore_whitespace,
813 ignore_blank_lines=self.ignore_whitespace,
815 ignore_case=False,
814 ignore_case=False,
816 ignore_space_changes=self.ignore_whitespace)
815 ignore_space_changes=self.ignore_whitespace)
817
816
818 buf.writelines(udiff)
817 buf.writelines(udiff)
819
818
820 def _get_mime_type(self, path) -> bytes:
819 def _get_mime_type(self, path) -> bytes:
821 try:
820 try:
822 mime_type = svn.fs.node_prop(
821 mime_type = svn.fs.node_prop(
823 self.tgt_root, path, svn.core.SVN_PROP_MIME_TYPE)
822 self.tgt_root, path, svn.core.SVN_PROP_MIME_TYPE)
824 except svn.core.SubversionException:
823 except svn.core.SubversionException:
825 mime_type = svn.fs.node_prop(
824 mime_type = svn.fs.node_prop(
826 self.src_root, path, svn.core.SVN_PROP_MIME_TYPE)
825 self.src_root, path, svn.core.SVN_PROP_MIME_TYPE)
827 return mime_type
826 return mime_type
828
827
829 def _svn_readlines(self, fs_root, node_path):
828 def _svn_readlines(self, fs_root, node_path):
830 if self.binary_content:
829 if self.binary_content:
831 return []
830 return []
832 node_kind = svn.fs.check_path(fs_root, node_path)
831 node_kind = svn.fs.check_path(fs_root, node_path)
833 if node_kind not in (
832 if node_kind not in (
834 svn.core.svn_node_file, svn.core.svn_node_symlink):
833 svn.core.svn_node_file, svn.core.svn_node_symlink):
835 return []
834 return []
836 content = svn.core.Stream(
835 content = svn.core.Stream(
837 svn.fs.file_contents(fs_root, node_path)).read()
836 svn.fs.file_contents(fs_root, node_path)).read()
838
837
839 return content.splitlines(True)
838 return content.splitlines(True)
840
839
841
840
842 class DiffChangeEditor(svn.delta.Editor):
841 class DiffChangeEditor(svn.delta.Editor):
843 """
842 """
844 Records changes between two given revisions
843 Records changes between two given revisions
845 """
844 """
846
845
847 def __init__(self):
846 def __init__(self):
848 self.changes = []
847 self.changes = []
849
848
850 def delete_entry(self, path, revision, parent_baton, pool=None):
849 def delete_entry(self, path, revision, parent_baton, pool=None):
851 self.changes.append((path, None, 'delete'))
850 self.changes.append((path, None, 'delete'))
852
851
853 def add_file(
852 def add_file(
854 self, path, parent_baton, copyfrom_path, copyfrom_revision,
853 self, path, parent_baton, copyfrom_path, copyfrom_revision,
855 file_pool=None):
854 file_pool=None):
856 self.changes.append((path, 'file', 'add'))
855 self.changes.append((path, 'file', 'add'))
857
856
858 def open_file(self, path, parent_baton, base_revision, file_pool=None):
857 def open_file(self, path, parent_baton, base_revision, file_pool=None):
859 self.changes.append((path, 'file', 'change'))
858 self.changes.append((path, 'file', 'change'))
860
859
861
860
862 def authorization_callback_allow_all(root, path, pool):
861 def authorization_callback_allow_all(root, path, pool):
863 return True
862 return True
864
863
865
864
866 class TxnNodeProcessor:
865 class TxnNodeProcessor:
867 """
866 """
868 Utility to process the change of one node within a transaction root.
867 Utility to process the change of one node within a transaction root.
869
868
870 It encapsulates the knowledge of how to add, update or remove
869 It encapsulates the knowledge of how to add, update or remove
871 a node for a given transaction root. The purpose is to support the method
870 a node for a given transaction root. The purpose is to support the method
872 `SvnRemote.commit`.
871 `SvnRemote.commit`.
873 """
872 """
874
873
875 def __init__(self, node, txn_root):
874 def __init__(self, node, txn_root):
876 assert_bytes(node['path'])
875 assert_bytes(node['path'])
877
876
878 self.node = node
877 self.node = node
879 self.txn_root = txn_root
878 self.txn_root = txn_root
880
879
881 def update(self):
880 def update(self):
882 self._ensure_parent_dirs()
881 self._ensure_parent_dirs()
883 self._add_file_if_node_does_not_exist()
882 self._add_file_if_node_does_not_exist()
884 self._update_file_content()
883 self._update_file_content()
885 self._update_file_properties()
884 self._update_file_properties()
886
885
887 def remove(self):
886 def remove(self):
888 svn.fs.delete(self.txn_root, self.node['path'])
887 svn.fs.delete(self.txn_root, self.node['path'])
889 # TODO: Clean up directory if empty
888 # TODO: Clean up directory if empty
890
889
891 def _ensure_parent_dirs(self):
890 def _ensure_parent_dirs(self):
892 curdir = vcspath.dirname(self.node['path'])
891 curdir = vcspath.dirname(self.node['path'])
893 dirs_to_create = []
892 dirs_to_create = []
894 while not self._svn_path_exists(curdir):
893 while not self._svn_path_exists(curdir):
895 dirs_to_create.append(curdir)
894 dirs_to_create.append(curdir)
896 curdir = vcspath.dirname(curdir)
895 curdir = vcspath.dirname(curdir)
897
896
898 for curdir in reversed(dirs_to_create):
897 for curdir in reversed(dirs_to_create):
899 log.debug('Creating missing directory "%s"', curdir)
898 log.debug('Creating missing directory "%s"', curdir)
900 svn.fs.make_dir(self.txn_root, curdir)
899 svn.fs.make_dir(self.txn_root, curdir)
901
900
902 def _svn_path_exists(self, path):
901 def _svn_path_exists(self, path):
903 path_status = svn.fs.check_path(self.txn_root, path)
902 path_status = svn.fs.check_path(self.txn_root, path)
904 return path_status != svn.core.svn_node_none
903 return path_status != svn.core.svn_node_none
905
904
906 def _add_file_if_node_does_not_exist(self):
905 def _add_file_if_node_does_not_exist(self):
907 kind = svn.fs.check_path(self.txn_root, self.node['path'])
906 kind = svn.fs.check_path(self.txn_root, self.node['path'])
908 if kind == svn.core.svn_node_none:
907 if kind == svn.core.svn_node_none:
909 svn.fs.make_file(self.txn_root, self.node['path'])
908 svn.fs.make_file(self.txn_root, self.node['path'])
910
909
911 def _update_file_content(self):
910 def _update_file_content(self):
912 assert_bytes(self.node['content'])
911 assert_bytes(self.node['content'])
913
912
914 handler, baton = svn.fs.apply_textdelta(
913 handler, baton = svn.fs.apply_textdelta(
915 self.txn_root, self.node['path'], None, None)
914 self.txn_root, self.node['path'], None, None)
916 svn.delta.svn_txdelta_send_string(self.node['content'], handler, baton)
915 svn.delta.svn_txdelta_send_string(self.node['content'], handler, baton)
917
916
918 def _update_file_properties(self):
917 def _update_file_properties(self):
919 properties = self.node.get('properties', {})
918 properties = self.node.get('properties', {})
920 for key, value in properties.items():
919 for key, value in properties.items():
921 svn.fs.change_node_prop(
920 svn.fs.change_node_prop(
922 self.txn_root, self.node['path'], safe_bytes(key), safe_bytes(value))
921 self.txn_root, self.node['path'], safe_bytes(key), safe_bytes(value))
923
922
924
923
925 def apr_time_t(timestamp):
924 def apr_time_t(timestamp):
926 """
925 """
927 Convert a Python timestamp into APR timestamp type apr_time_t
926 Convert a Python timestamp into APR timestamp type apr_time_t
928 """
927 """
929 return int(timestamp * 1E6)
928 return int(timestamp * 1E6)
930
929
931
930
932 def svn_opt_revision_value_t(num):
931 def svn_opt_revision_value_t(num):
933 """
932 """
934 Put `num` into a `svn_opt_revision_value_t` structure.
933 Put `num` into a `svn_opt_revision_value_t` structure.
935 """
934 """
936 value = svn.core.svn_opt_revision_value_t()
935 value = svn.core.svn_opt_revision_value_t()
937 value.number = num
936 value.number = num
938 revision = svn.core.svn_opt_revision_t()
937 revision = svn.core.svn_opt_revision_t()
939 revision.kind = svn.core.svn_opt_revision_number
938 revision.kind = svn.core.svn_opt_revision_number
940 revision.value = value
939 revision.value = value
941 return revision
940 return revision
General Comments 0
You need to be logged in to leave comments. Login now