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