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