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