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