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