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