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