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