##// END OF EJS Templates
svn-diffs: use consisten behaviour on generting diffs between hg/git backends.
marcink -
r147:82a881ef default
parent child Browse files
Show More
@@ -1,627 +1,651 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
201 if change.item_kind == svn.core.svn_node_dir:
201 if change.item_kind == svn.core.svn_node_dir:
202 continue
202 continue
203 if change.action in [svn.repos.CHANGE_ACTION_ADD]:
203 if change.action in [svn.repos.CHANGE_ACTION_ADD]:
204 added.append(path)
204 added.append(path)
205 elif change.action in [svn.repos.CHANGE_ACTION_MODIFY,
205 elif change.action in [svn.repos.CHANGE_ACTION_MODIFY,
206 svn.repos.CHANGE_ACTION_REPLACE]:
206 svn.repos.CHANGE_ACTION_REPLACE]:
207 changed.append(path)
207 changed.append(path)
208 elif change.action in [svn.repos.CHANGE_ACTION_DELETE]:
208 elif change.action in [svn.repos.CHANGE_ACTION_DELETE]:
209 removed.append(path)
209 removed.append(path)
210 else:
210 else:
211 raise NotImplementedError(
211 raise NotImplementedError(
212 "Action %s not supported on path %s" % (
212 "Action %s not supported on path %s" % (
213 change.action, path))
213 change.action, path))
214
214
215 changes = {
215 changes = {
216 'added': added,
216 'added': added,
217 'changed': changed,
217 'changed': changed,
218 'removed': removed,
218 'removed': removed,
219 }
219 }
220 return changes
220 return changes
221
221
222 def node_history(self, wire, path, revision, limit):
222 def node_history(self, wire, path, revision, limit):
223 cross_copies = False
223 cross_copies = False
224 repo = self._factory.repo(wire)
224 repo = self._factory.repo(wire)
225 fsobj = svn.repos.fs(repo)
225 fsobj = svn.repos.fs(repo)
226 rev_root = svn.fs.revision_root(fsobj, revision)
226 rev_root = svn.fs.revision_root(fsobj, revision)
227
227
228 history_revisions = []
228 history_revisions = []
229 history = svn.fs.node_history(rev_root, path)
229 history = svn.fs.node_history(rev_root, path)
230 history = svn.fs.history_prev(history, cross_copies)
230 history = svn.fs.history_prev(history, cross_copies)
231 while history:
231 while history:
232 __, node_revision = svn.fs.history_location(history)
232 __, node_revision = svn.fs.history_location(history)
233 history_revisions.append(node_revision)
233 history_revisions.append(node_revision)
234 if limit and len(history_revisions) >= limit:
234 if limit and len(history_revisions) >= limit:
235 break
235 break
236 history = svn.fs.history_prev(history, cross_copies)
236 history = svn.fs.history_prev(history, cross_copies)
237 return history_revisions
237 return history_revisions
238
238
239 def node_properties(self, wire, path, revision):
239 def node_properties(self, wire, path, revision):
240 repo = self._factory.repo(wire)
240 repo = self._factory.repo(wire)
241 fsobj = svn.repos.fs(repo)
241 fsobj = svn.repos.fs(repo)
242 rev_root = svn.fs.revision_root(fsobj, revision)
242 rev_root = svn.fs.revision_root(fsobj, revision)
243 return svn.fs.node_proplist(rev_root, path)
243 return svn.fs.node_proplist(rev_root, path)
244
244
245 def file_annotate(self, wire, path, revision):
245 def file_annotate(self, wire, path, revision):
246 abs_path = 'file://' + urllib.pathname2url(
246 abs_path = 'file://' + urllib.pathname2url(
247 vcspath.join(wire['path'], path))
247 vcspath.join(wire['path'], path))
248 file_uri = svn.core.svn_path_canonicalize(abs_path)
248 file_uri = svn.core.svn_path_canonicalize(abs_path)
249
249
250 start_rev = svn_opt_revision_value_t(0)
250 start_rev = svn_opt_revision_value_t(0)
251 peg_rev = svn_opt_revision_value_t(revision)
251 peg_rev = svn_opt_revision_value_t(revision)
252 end_rev = peg_rev
252 end_rev = peg_rev
253
253
254 annotations = []
254 annotations = []
255
255
256 def receiver(line_no, revision, author, date, line, pool):
256 def receiver(line_no, revision, author, date, line, pool):
257 annotations.append((line_no, revision, line))
257 annotations.append((line_no, revision, line))
258
258
259 # TODO: Cannot use blame5, missing typemap function in the swig code
259 # TODO: Cannot use blame5, missing typemap function in the swig code
260 try:
260 try:
261 svn.client.blame2(
261 svn.client.blame2(
262 file_uri, peg_rev, start_rev, end_rev,
262 file_uri, peg_rev, start_rev, end_rev,
263 receiver, svn.client.create_context())
263 receiver, svn.client.create_context())
264 except svn.core.SubversionException as exc:
264 except svn.core.SubversionException as exc:
265 log.exception("Error during blame operation.")
265 log.exception("Error during blame operation.")
266 raise Exception(
266 raise Exception(
267 "Blame not supported or file does not exist at path %s. "
267 "Blame not supported or file does not exist at path %s. "
268 "Error %s." % (path, exc))
268 "Error %s." % (path, exc))
269
269
270 return annotations
270 return annotations
271
271
272 def get_node_type(self, wire, path, rev=None):
272 def get_node_type(self, wire, path, rev=None):
273 repo = self._factory.repo(wire)
273 repo = self._factory.repo(wire)
274 fs_ptr = svn.repos.fs(repo)
274 fs_ptr = svn.repos.fs(repo)
275 if rev is None:
275 if rev is None:
276 rev = svn.fs.youngest_rev(fs_ptr)
276 rev = svn.fs.youngest_rev(fs_ptr)
277 root = svn.fs.revision_root(fs_ptr, rev)
277 root = svn.fs.revision_root(fs_ptr, rev)
278 node = svn.fs.check_path(root, path)
278 node = svn.fs.check_path(root, path)
279 return NODE_TYPE_MAPPING.get(node, None)
279 return NODE_TYPE_MAPPING.get(node, None)
280
280
281 def get_nodes(self, wire, path, revision=None):
281 def get_nodes(self, wire, path, revision=None):
282 repo = self._factory.repo(wire)
282 repo = self._factory.repo(wire)
283 fsobj = svn.repos.fs(repo)
283 fsobj = svn.repos.fs(repo)
284 if revision is None:
284 if revision is None:
285 revision = svn.fs.youngest_rev(fsobj)
285 revision = svn.fs.youngest_rev(fsobj)
286 root = svn.fs.revision_root(fsobj, revision)
286 root = svn.fs.revision_root(fsobj, revision)
287 entries = svn.fs.dir_entries(root, path)
287 entries = svn.fs.dir_entries(root, path)
288 result = []
288 result = []
289 for entry_path, entry_info in entries.iteritems():
289 for entry_path, entry_info in entries.iteritems():
290 result.append(
290 result.append(
291 (entry_path, NODE_TYPE_MAPPING.get(entry_info.kind, None)))
291 (entry_path, NODE_TYPE_MAPPING.get(entry_info.kind, None)))
292 return result
292 return result
293
293
294 def get_file_content(self, wire, path, rev=None):
294 def get_file_content(self, wire, path, rev=None):
295 repo = self._factory.repo(wire)
295 repo = self._factory.repo(wire)
296 fsobj = svn.repos.fs(repo)
296 fsobj = svn.repos.fs(repo)
297 if rev is None:
297 if rev is None:
298 rev = svn.fs.youngest_revision(fsobj)
298 rev = svn.fs.youngest_revision(fsobj)
299 root = svn.fs.revision_root(fsobj, rev)
299 root = svn.fs.revision_root(fsobj, rev)
300 content = svn.core.Stream(svn.fs.file_contents(root, path))
300 content = svn.core.Stream(svn.fs.file_contents(root, path))
301 return content.read()
301 return content.read()
302
302
303 def get_file_size(self, wire, path, revision=None):
303 def get_file_size(self, wire, path, revision=None):
304 repo = self._factory.repo(wire)
304 repo = self._factory.repo(wire)
305 fsobj = svn.repos.fs(repo)
305 fsobj = svn.repos.fs(repo)
306 if revision is None:
306 if revision is None:
307 revision = svn.fs.youngest_revision(fsobj)
307 revision = svn.fs.youngest_revision(fsobj)
308 root = svn.fs.revision_root(fsobj, revision)
308 root = svn.fs.revision_root(fsobj, revision)
309 size = svn.fs.file_length(root, path)
309 size = svn.fs.file_length(root, path)
310 return size
310 return size
311
311
312 def create_repository(self, wire, compatible_version=None):
312 def create_repository(self, wire, compatible_version=None):
313 log.info('Creating Subversion repository in path "%s"', wire['path'])
313 log.info('Creating Subversion repository in path "%s"', wire['path'])
314 self._factory.repo(wire, create=True,
314 self._factory.repo(wire, create=True,
315 compatible_version=compatible_version)
315 compatible_version=compatible_version)
316
316
317 def import_remote_repository(self, wire, src_url):
317 def import_remote_repository(self, wire, src_url):
318 repo_path = wire['path']
318 repo_path = wire['path']
319 if not self.is_path_valid_repository(wire, repo_path):
319 if not self.is_path_valid_repository(wire, repo_path):
320 raise Exception(
320 raise Exception(
321 "Path %s is not a valid Subversion repository." % repo_path)
321 "Path %s is not a valid Subversion repository." % repo_path)
322 # TODO: johbo: URL checks ?
322 # TODO: johbo: URL checks ?
323 rdump = subprocess.Popen(
323 rdump = subprocess.Popen(
324 ['svnrdump', 'dump', '--non-interactive', src_url],
324 ['svnrdump', 'dump', '--non-interactive', src_url],
325 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
325 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
326 load = subprocess.Popen(
326 load = subprocess.Popen(
327 ['svnadmin', 'load', repo_path], stdin=rdump.stdout)
327 ['svnadmin', 'load', repo_path], stdin=rdump.stdout)
328
328
329 # 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
330 # 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
331 # import is done.
331 # import is done.
332 rdump.wait()
332 rdump.wait()
333 load.wait()
333 load.wait()
334
334
335 if rdump.returncode != 0:
335 if rdump.returncode != 0:
336 errors = rdump.stderr.read()
336 errors = rdump.stderr.read()
337 log.error('svnrdump dump failed: statuscode %s: message: %s',
337 log.error('svnrdump dump failed: statuscode %s: message: %s',
338 rdump.returncode, errors)
338 rdump.returncode, errors)
339 reason = 'UNKNOWN'
339 reason = 'UNKNOWN'
340 if 'svnrdump: E230001:' in errors:
340 if 'svnrdump: E230001:' in errors:
341 reason = 'INVALID_CERTIFICATE'
341 reason = 'INVALID_CERTIFICATE'
342 raise Exception(
342 raise Exception(
343 'Failed to dump the remote repository from %s.' % src_url,
343 'Failed to dump the remote repository from %s.' % src_url,
344 reason)
344 reason)
345 if load.returncode != 0:
345 if load.returncode != 0:
346 raise Exception(
346 raise Exception(
347 'Failed to load the dump of remote repository from %s.' %
347 'Failed to load the dump of remote repository from %s.' %
348 (src_url, ))
348 (src_url, ))
349
349
350 def commit(self, wire, message, author, timestamp, updated, removed):
350 def commit(self, wire, message, author, timestamp, updated, removed):
351 assert isinstance(message, str)
351 assert isinstance(message, str)
352 assert isinstance(author, str)
352 assert isinstance(author, str)
353
353
354 repo = self._factory.repo(wire)
354 repo = self._factory.repo(wire)
355 fsobj = svn.repos.fs(repo)
355 fsobj = svn.repos.fs(repo)
356
356
357 rev = svn.fs.youngest_rev(fsobj)
357 rev = svn.fs.youngest_rev(fsobj)
358 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)
359 txn_root = svn.fs.txn_root(txn)
359 txn_root = svn.fs.txn_root(txn)
360
360
361 for node in updated:
361 for node in updated:
362 TxnNodeProcessor(node, txn_root).update()
362 TxnNodeProcessor(node, txn_root).update()
363 for node in removed:
363 for node in removed:
364 TxnNodeProcessor(node, txn_root).remove()
364 TxnNodeProcessor(node, txn_root).remove()
365
365
366 commit_id = svn.repos.fs_commit_txn(repo, txn)
366 commit_id = svn.repos.fs_commit_txn(repo, txn)
367
367
368 if timestamp:
368 if timestamp:
369 apr_time = apr_time_t(timestamp)
369 apr_time = apr_time_t(timestamp)
370 ts_formatted = svn.core.svn_time_to_cstring(apr_time)
370 ts_formatted = svn.core.svn_time_to_cstring(apr_time)
371 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)
372
372
373 log.debug('Committed revision "%s" to "%s".', commit_id, wire['path'])
373 log.debug('Committed revision "%s" to "%s".', commit_id, wire['path'])
374 return commit_id
374 return commit_id
375
375
376 def diff(self, wire, rev1, rev2, path1=None, path2=None,
376 def diff(self, wire, rev1, rev2, path1=None, path2=None,
377 ignore_whitespace=False, context=3):
377 ignore_whitespace=False, context=3):
378
378 wire.update(cache=False)
379 wire.update(cache=False)
379 repo = self._factory.repo(wire)
380 repo = self._factory.repo(wire)
380 diff_creator = SvnDiffer(
381 diff_creator = SvnDiffer(
381 repo, rev1, path1, rev2, path2, ignore_whitespace, context)
382 repo, rev1, path1, rev2, path2, ignore_whitespace, context)
383 try:
382 return diff_creator.generate_diff()
384 return diff_creator.generate_diff()
385 except svn.core.SubversionException as e:
386 log.exception(
387 "Error during diff operation operation. "
388 "Path might not exist %s, %s" % (path1, path2))
389 return ""
383
390
384
391
385 class SvnDiffer(object):
392 class SvnDiffer(object):
386 """
393 """
387 Utility to create diffs based on difflib and the Subversion api
394 Utility to create diffs based on difflib and the Subversion api
388 """
395 """
389
396
390 binary_content = False
397 binary_content = False
391
398
392 def __init__(
399 def __init__(
393 self, repo, src_rev, src_path, tgt_rev, tgt_path,
400 self, repo, src_rev, src_path, tgt_rev, tgt_path,
394 ignore_whitespace, context):
401 ignore_whitespace, context):
395 self.repo = repo
402 self.repo = repo
396 self.ignore_whitespace = ignore_whitespace
403 self.ignore_whitespace = ignore_whitespace
397 self.context = context
404 self.context = context
398
405
399 fsobj = svn.repos.fs(repo)
406 fsobj = svn.repos.fs(repo)
400
407
401 self.tgt_rev = tgt_rev
408 self.tgt_rev = tgt_rev
402 self.tgt_path = tgt_path or ''
409 self.tgt_path = tgt_path or ''
403 self.tgt_root = svn.fs.revision_root(fsobj, tgt_rev)
410 self.tgt_root = svn.fs.revision_root(fsobj, tgt_rev)
404 self.tgt_kind = svn.fs.check_path(self.tgt_root, self.tgt_path)
411 self.tgt_kind = svn.fs.check_path(self.tgt_root, self.tgt_path)
405
412
406 self.src_rev = src_rev
413 self.src_rev = src_rev
407 self.src_path = src_path or self.tgt_path
414 self.src_path = src_path or self.tgt_path
408 self.src_root = svn.fs.revision_root(fsobj, src_rev)
415 self.src_root = svn.fs.revision_root(fsobj, src_rev)
409 self.src_kind = svn.fs.check_path(self.src_root, self.src_path)
416 self.src_kind = svn.fs.check_path(self.src_root, self.src_path)
410
417
411 self._validate()
418 self._validate()
412
419
413 def _validate(self):
420 def _validate(self):
414 if (self.tgt_kind != svn.core.svn_node_none and
421 if (self.tgt_kind != svn.core.svn_node_none and
415 self.src_kind != svn.core.svn_node_none and
422 self.src_kind != svn.core.svn_node_none and
416 self.src_kind != self.tgt_kind):
423 self.src_kind != self.tgt_kind):
417 # TODO: johbo: proper error handling
424 # TODO: johbo: proper error handling
418 raise Exception(
425 raise Exception(
419 "Source and target are not compatible for diff generation. "
426 "Source and target are not compatible for diff generation. "
420 "Source type: %s, target type: %s" %
427 "Source type: %s, target type: %s" %
421 (self.src_kind, self.tgt_kind))
428 (self.src_kind, self.tgt_kind))
422
429
423 def generate_diff(self):
430 def generate_diff(self):
424 buf = StringIO.StringIO()
431 buf = StringIO.StringIO()
425 if self.tgt_kind == svn.core.svn_node_dir:
432 if self.tgt_kind == svn.core.svn_node_dir:
426 self._generate_dir_diff(buf)
433 self._generate_dir_diff(buf)
427 else:
434 else:
428 self._generate_file_diff(buf)
435 self._generate_file_diff(buf)
429 return buf.getvalue()
436 return buf.getvalue()
430
437
431 def _generate_dir_diff(self, buf):
438 def _generate_dir_diff(self, buf):
432 editor = DiffChangeEditor()
439 editor = DiffChangeEditor()
433 editor_ptr, editor_baton = svn.delta.make_editor(editor)
440 editor_ptr, editor_baton = svn.delta.make_editor(editor)
434 svn.repos.dir_delta2(
441 svn.repos.dir_delta2(
435 self.src_root,
442 self.src_root,
436 self.src_path,
443 self.src_path,
437 '', # src_entry
444 '', # src_entry
438 self.tgt_root,
445 self.tgt_root,
439 self.tgt_path,
446 self.tgt_path,
440 editor_ptr, editor_baton,
447 editor_ptr, editor_baton,
441 authorization_callback_allow_all,
448 authorization_callback_allow_all,
442 False, # text_deltas
449 False, # text_deltas
443 svn.core.svn_depth_infinity, # depth
450 svn.core.svn_depth_infinity, # depth
444 False, # entry_props
451 False, # entry_props
445 False, # ignore_ancestry
452 False, # ignore_ancestry
446 )
453 )
447
454
448 for path, __, change in sorted(editor.changes):
455 for path, __, change in sorted(editor.changes):
449 self._generate_node_diff(
456 self._generate_node_diff(
450 buf, change, path, self.tgt_path, path, self.src_path)
457 buf, change, path, self.tgt_path, path, self.src_path)
451
458
452 def _generate_file_diff(self, buf):
459 def _generate_file_diff(self, buf):
453 change = None
460 change = None
454 if self.src_kind == svn.core.svn_node_none:
461 if self.src_kind == svn.core.svn_node_none:
455 change = "add"
462 change = "add"
456 elif self.tgt_kind == svn.core.svn_node_none:
463 elif self.tgt_kind == svn.core.svn_node_none:
457 change = "delete"
464 change = "delete"
458 tgt_base, tgt_path = vcspath.split(self.tgt_path)
465 tgt_base, tgt_path = vcspath.split(self.tgt_path)
459 src_base, src_path = vcspath.split(self.src_path)
466 src_base, src_path = vcspath.split(self.src_path)
460 self._generate_node_diff(
467 self._generate_node_diff(
461 buf, change, tgt_path, tgt_base, src_path, src_base)
468 buf, change, tgt_path, tgt_base, src_path, src_base)
462
469
463 def _generate_node_diff(
470 def _generate_node_diff(
464 self, buf, change, tgt_path, tgt_base, src_path, src_base):
471 self, buf, change, tgt_path, tgt_base, src_path, src_base):
472
473 if self.src_rev == self.tgt_rev and tgt_base == src_base:
474 # makes consistent behaviour with git/hg to return empty diff if
475 # we compare same revisions
476 return
477
465 tgt_full_path = vcspath.join(tgt_base, tgt_path)
478 tgt_full_path = vcspath.join(tgt_base, tgt_path)
466 src_full_path = vcspath.join(src_base, src_path)
479 src_full_path = vcspath.join(src_base, src_path)
467
480
468 self.binary_content = False
481 self.binary_content = False
469 mime_type = self._get_mime_type(tgt_full_path)
482 mime_type = self._get_mime_type(tgt_full_path)
483
470 if mime_type and not mime_type.startswith('text'):
484 if mime_type and not mime_type.startswith('text'):
471 self.binary_content = True
485 self.binary_content = True
472 buf.write("=" * 67 + '\n')
486 buf.write("=" * 67 + '\n')
473 buf.write("Cannot display: file marked as a binary type.\n")
487 buf.write("Cannot display: file marked as a binary type.\n")
474 buf.write("svn:mime-type = %s\n" % mime_type)
488 buf.write("svn:mime-type = %s\n" % mime_type)
475 buf.write("Index: %s\n" % (tgt_path, ))
489 buf.write("Index: %s\n" % (tgt_path, ))
476 buf.write("=" * 67 + '\n')
490 buf.write("=" * 67 + '\n')
477 buf.write("diff --git a/%(tgt_path)s b/%(tgt_path)s\n" % {
491 buf.write("diff --git a/%(tgt_path)s b/%(tgt_path)s\n" % {
478 'tgt_path': tgt_path})
492 'tgt_path': tgt_path})
479
493
480 if change == 'add':
494 if change == 'add':
481 # TODO: johbo: SVN is missing a zero here compared to git
495 # TODO: johbo: SVN is missing a zero here compared to git
482 buf.write("new file mode 10644\n")
496 buf.write("new file mode 10644\n")
497
498 #TODO(marcink): intro to binary detection of svn patches
499 # if self.binary_content:
500 # buf.write('GIT binary patch\n')
501
483 buf.write("--- /dev/null\t(revision 0)\n")
502 buf.write("--- /dev/null\t(revision 0)\n")
484 src_lines = []
503 src_lines = []
485 else:
504 else:
486 if change == 'delete':
505 if change == 'delete':
487 buf.write("deleted file mode 10644\n")
506 buf.write("deleted file mode 10644\n")
507
508 #TODO(marcink): intro to binary detection of svn patches
509 # if self.binary_content:
510 # buf.write('GIT binary patch\n')
511
488 buf.write("--- a/%s\t(revision %s)\n" % (
512 buf.write("--- a/%s\t(revision %s)\n" % (
489 src_path, self.src_rev))
513 src_path, self.src_rev))
490 src_lines = self._svn_readlines(self.src_root, src_full_path)
514 src_lines = self._svn_readlines(self.src_root, src_full_path)
491
515
492 if change == 'delete':
516 if change == 'delete':
493 buf.write("+++ /dev/null\t(revision %s)\n" % (self.tgt_rev, ))
517 buf.write("+++ /dev/null\t(revision %s)\n" % (self.tgt_rev, ))
494 tgt_lines = []
518 tgt_lines = []
495 else:
519 else:
496 buf.write("+++ b/%s\t(revision %s)\n" % (
520 buf.write("+++ b/%s\t(revision %s)\n" % (
497 tgt_path, self.tgt_rev))
521 tgt_path, self.tgt_rev))
498 tgt_lines = self._svn_readlines(self.tgt_root, tgt_full_path)
522 tgt_lines = self._svn_readlines(self.tgt_root, tgt_full_path)
499
523
500 if not self.binary_content:
524 if not self.binary_content:
501 udiff = svn_diff.unified_diff(
525 udiff = svn_diff.unified_diff(
502 src_lines, tgt_lines, context=self.context,
526 src_lines, tgt_lines, context=self.context,
503 ignore_blank_lines=self.ignore_whitespace,
527 ignore_blank_lines=self.ignore_whitespace,
504 ignore_case=False,
528 ignore_case=False,
505 ignore_space_changes=self.ignore_whitespace)
529 ignore_space_changes=self.ignore_whitespace)
506 buf.writelines(udiff)
530 buf.writelines(udiff)
507
531
508 def _get_mime_type(self, path):
532 def _get_mime_type(self, path):
509 try:
533 try:
510 mime_type = svn.fs.node_prop(
534 mime_type = svn.fs.node_prop(
511 self.tgt_root, path, svn.core.SVN_PROP_MIME_TYPE)
535 self.tgt_root, path, svn.core.SVN_PROP_MIME_TYPE)
512 except svn.core.SubversionException:
536 except svn.core.SubversionException:
513 mime_type = svn.fs.node_prop(
537 mime_type = svn.fs.node_prop(
514 self.src_root, path, svn.core.SVN_PROP_MIME_TYPE)
538 self.src_root, path, svn.core.SVN_PROP_MIME_TYPE)
515 return mime_type
539 return mime_type
516
540
517 def _svn_readlines(self, fs_root, node_path):
541 def _svn_readlines(self, fs_root, node_path):
518 if self.binary_content:
542 if self.binary_content:
519 return []
543 return []
520 node_kind = svn.fs.check_path(fs_root, node_path)
544 node_kind = svn.fs.check_path(fs_root, node_path)
521 if node_kind not in (
545 if node_kind not in (
522 svn.core.svn_node_file, svn.core.svn_node_symlink):
546 svn.core.svn_node_file, svn.core.svn_node_symlink):
523 return []
547 return []
524 content = svn.core.Stream(
548 content = svn.core.Stream(
525 svn.fs.file_contents(fs_root, node_path)).read()
549 svn.fs.file_contents(fs_root, node_path)).read()
526 return content.splitlines(True)
550 return content.splitlines(True)
527
551
528
552
529 class DiffChangeEditor(svn.delta.Editor):
553 class DiffChangeEditor(svn.delta.Editor):
530 """
554 """
531 Records changes between two given revisions
555 Records changes between two given revisions
532 """
556 """
533
557
534 def __init__(self):
558 def __init__(self):
535 self.changes = []
559 self.changes = []
536
560
537 def delete_entry(self, path, revision, parent_baton, pool=None):
561 def delete_entry(self, path, revision, parent_baton, pool=None):
538 self.changes.append((path, None, 'delete'))
562 self.changes.append((path, None, 'delete'))
539
563
540 def add_file(
564 def add_file(
541 self, path, parent_baton, copyfrom_path, copyfrom_revision,
565 self, path, parent_baton, copyfrom_path, copyfrom_revision,
542 file_pool=None):
566 file_pool=None):
543 self.changes.append((path, 'file', 'add'))
567 self.changes.append((path, 'file', 'add'))
544
568
545 def open_file(self, path, parent_baton, base_revision, file_pool=None):
569 def open_file(self, path, parent_baton, base_revision, file_pool=None):
546 self.changes.append((path, 'file', 'change'))
570 self.changes.append((path, 'file', 'change'))
547
571
548
572
549 def authorization_callback_allow_all(root, path, pool):
573 def authorization_callback_allow_all(root, path, pool):
550 return True
574 return True
551
575
552
576
553 class TxnNodeProcessor(object):
577 class TxnNodeProcessor(object):
554 """
578 """
555 Utility to process the change of one node within a transaction root.
579 Utility to process the change of one node within a transaction root.
556
580
557 It encapsulates the knowledge of how to add, update or remove
581 It encapsulates the knowledge of how to add, update or remove
558 a node for a given transaction root. The purpose is to support the method
582 a node for a given transaction root. The purpose is to support the method
559 `SvnRemote.commit`.
583 `SvnRemote.commit`.
560 """
584 """
561
585
562 def __init__(self, node, txn_root):
586 def __init__(self, node, txn_root):
563 assert isinstance(node['path'], str)
587 assert isinstance(node['path'], str)
564
588
565 self.node = node
589 self.node = node
566 self.txn_root = txn_root
590 self.txn_root = txn_root
567
591
568 def update(self):
592 def update(self):
569 self._ensure_parent_dirs()
593 self._ensure_parent_dirs()
570 self._add_file_if_node_does_not_exist()
594 self._add_file_if_node_does_not_exist()
571 self._update_file_content()
595 self._update_file_content()
572 self._update_file_properties()
596 self._update_file_properties()
573
597
574 def remove(self):
598 def remove(self):
575 svn.fs.delete(self.txn_root, self.node['path'])
599 svn.fs.delete(self.txn_root, self.node['path'])
576 # TODO: Clean up directory if empty
600 # TODO: Clean up directory if empty
577
601
578 def _ensure_parent_dirs(self):
602 def _ensure_parent_dirs(self):
579 curdir = vcspath.dirname(self.node['path'])
603 curdir = vcspath.dirname(self.node['path'])
580 dirs_to_create = []
604 dirs_to_create = []
581 while not self._svn_path_exists(curdir):
605 while not self._svn_path_exists(curdir):
582 dirs_to_create.append(curdir)
606 dirs_to_create.append(curdir)
583 curdir = vcspath.dirname(curdir)
607 curdir = vcspath.dirname(curdir)
584
608
585 for curdir in reversed(dirs_to_create):
609 for curdir in reversed(dirs_to_create):
586 log.debug('Creating missing directory "%s"', curdir)
610 log.debug('Creating missing directory "%s"', curdir)
587 svn.fs.make_dir(self.txn_root, curdir)
611 svn.fs.make_dir(self.txn_root, curdir)
588
612
589 def _svn_path_exists(self, path):
613 def _svn_path_exists(self, path):
590 path_status = svn.fs.check_path(self.txn_root, path)
614 path_status = svn.fs.check_path(self.txn_root, path)
591 return path_status != svn.core.svn_node_none
615 return path_status != svn.core.svn_node_none
592
616
593 def _add_file_if_node_does_not_exist(self):
617 def _add_file_if_node_does_not_exist(self):
594 kind = svn.fs.check_path(self.txn_root, self.node['path'])
618 kind = svn.fs.check_path(self.txn_root, self.node['path'])
595 if kind == svn.core.svn_node_none:
619 if kind == svn.core.svn_node_none:
596 svn.fs.make_file(self.txn_root, self.node['path'])
620 svn.fs.make_file(self.txn_root, self.node['path'])
597
621
598 def _update_file_content(self):
622 def _update_file_content(self):
599 assert isinstance(self.node['content'], str)
623 assert isinstance(self.node['content'], str)
600 handler, baton = svn.fs.apply_textdelta(
624 handler, baton = svn.fs.apply_textdelta(
601 self.txn_root, self.node['path'], None, None)
625 self.txn_root, self.node['path'], None, None)
602 svn.delta.svn_txdelta_send_string(self.node['content'], handler, baton)
626 svn.delta.svn_txdelta_send_string(self.node['content'], handler, baton)
603
627
604 def _update_file_properties(self):
628 def _update_file_properties(self):
605 properties = self.node.get('properties', {})
629 properties = self.node.get('properties', {})
606 for key, value in properties.iteritems():
630 for key, value in properties.iteritems():
607 svn.fs.change_node_prop(
631 svn.fs.change_node_prop(
608 self.txn_root, self.node['path'], key, value)
632 self.txn_root, self.node['path'], key, value)
609
633
610
634
611 def apr_time_t(timestamp):
635 def apr_time_t(timestamp):
612 """
636 """
613 Convert a Python timestamp into APR timestamp type apr_time_t
637 Convert a Python timestamp into APR timestamp type apr_time_t
614 """
638 """
615 return timestamp * 1E6
639 return timestamp * 1E6
616
640
617
641
618 def svn_opt_revision_value_t(num):
642 def svn_opt_revision_value_t(num):
619 """
643 """
620 Put `num` into a `svn_opt_revision_value_t` structure.
644 Put `num` into a `svn_opt_revision_value_t` structure.
621 """
645 """
622 value = svn.core.svn_opt_revision_value_t()
646 value = svn.core.svn_opt_revision_value_t()
623 value.number = num
647 value.number = num
624 revision = svn.core.svn_opt_revision_t()
648 revision = svn.core.svn_opt_revision_t()
625 revision.kind = svn.core.svn_opt_revision_number
649 revision.kind = svn.core.svn_opt_revision_number
626 revision.value = value
650 revision.value = value
627 return revision
651 return revision
General Comments 0
You need to be logged in to leave comments. Login now