##// END OF EJS Templates
svn: don't print exceptions in case of safe calls
milka -
r914:2f4cea33 default
parent child Browse files
Show More
@@ -1,856 +1,856 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-2020 RhodeCode GmbH
2 # Copyright (C) 2014-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 from __future__ import absolute_import
18 from __future__ import absolute_import
19
19
20 import os
20 import os
21 import subprocess
21 import subprocess
22 import time
22 import time
23 from urllib2 import URLError
23 from urllib2 import URLError
24 import urlparse
24 import urlparse
25 import logging
25 import logging
26 import posixpath as vcspath
26 import posixpath as vcspath
27 import StringIO
27 import StringIO
28 import urllib
28 import urllib
29 import traceback
29 import traceback
30
30
31 import svn.client
31 import svn.client
32 import svn.core
32 import svn.core
33 import svn.delta
33 import svn.delta
34 import svn.diff
34 import svn.diff
35 import svn.fs
35 import svn.fs
36 import svn.repos
36 import svn.repos
37
37
38 from vcsserver import svn_diff, exceptions, subprocessio, settings
38 from vcsserver import svn_diff, exceptions, subprocessio, settings
39 from vcsserver.base import RepoFactory, raise_from_original, ArchiveNode, archive_repo
39 from vcsserver.base import RepoFactory, raise_from_original, ArchiveNode, archive_repo
40 from vcsserver.exceptions import NoContentException
40 from vcsserver.exceptions import NoContentException
41 from vcsserver.vcs_base import RemoteBase
41 from vcsserver.vcs_base import RemoteBase
42
42
43 log = logging.getLogger(__name__)
43 log = logging.getLogger(__name__)
44
44
45
45
46 svn_compatible_versions_map = {
46 svn_compatible_versions_map = {
47 'pre-1.4-compatible': '1.3',
47 'pre-1.4-compatible': '1.3',
48 'pre-1.5-compatible': '1.4',
48 'pre-1.5-compatible': '1.4',
49 'pre-1.6-compatible': '1.5',
49 'pre-1.6-compatible': '1.5',
50 'pre-1.8-compatible': '1.7',
50 'pre-1.8-compatible': '1.7',
51 'pre-1.9-compatible': '1.8',
51 'pre-1.9-compatible': '1.8',
52 }
52 }
53
53
54 current_compatible_version = '1.12'
54 current_compatible_version = '1.12'
55
55
56
56
57 def reraise_safe_exceptions(func):
57 def reraise_safe_exceptions(func):
58 """Decorator for converting svn exceptions to something neutral."""
58 """Decorator for converting svn exceptions to something neutral."""
59 def wrapper(*args, **kwargs):
59 def wrapper(*args, **kwargs):
60 try:
60 try:
61 return func(*args, **kwargs)
61 return func(*args, **kwargs)
62 except Exception as e:
62 except Exception as e:
63 if not hasattr(e, '_vcs_kind'):
63 if not hasattr(e, '_vcs_kind'):
64 log.exception("Unhandled exception in svn remote call")
64 log.exception("Unhandled exception in svn remote call")
65 raise_from_original(exceptions.UnhandledException(e))
65 raise_from_original(exceptions.UnhandledException(e))
66 raise
66 raise
67 return wrapper
67 return wrapper
68
68
69
69
70 class SubversionFactory(RepoFactory):
70 class SubversionFactory(RepoFactory):
71 repo_type = 'svn'
71 repo_type = 'svn'
72
72
73 def _create_repo(self, wire, create, compatible_version):
73 def _create_repo(self, wire, create, compatible_version):
74 path = svn.core.svn_path_canonicalize(wire['path'])
74 path = svn.core.svn_path_canonicalize(wire['path'])
75 if create:
75 if create:
76 fs_config = {'compatible-version': current_compatible_version}
76 fs_config = {'compatible-version': current_compatible_version}
77 if compatible_version:
77 if compatible_version:
78
78
79 compatible_version_string = \
79 compatible_version_string = \
80 svn_compatible_versions_map.get(compatible_version) \
80 svn_compatible_versions_map.get(compatible_version) \
81 or compatible_version
81 or compatible_version
82 fs_config['compatible-version'] = compatible_version_string
82 fs_config['compatible-version'] = compatible_version_string
83
83
84 log.debug('Create SVN repo with config "%s"', fs_config)
84 log.debug('Create SVN repo with config "%s"', fs_config)
85 repo = svn.repos.create(path, "", "", None, fs_config)
85 repo = svn.repos.create(path, "", "", None, fs_config)
86 else:
86 else:
87 repo = svn.repos.open(path)
87 repo = svn.repos.open(path)
88
88
89 log.debug('Got SVN object: %s', repo)
89 log.debug('Got SVN object: %s', repo)
90 return repo
90 return repo
91
91
92 def repo(self, wire, create=False, compatible_version=None):
92 def repo(self, wire, create=False, compatible_version=None):
93 """
93 """
94 Get a repository instance for the given path.
94 Get a repository instance for the given path.
95 """
95 """
96 return self._create_repo(wire, create, compatible_version)
96 return self._create_repo(wire, create, compatible_version)
97
97
98
98
99 NODE_TYPE_MAPPING = {
99 NODE_TYPE_MAPPING = {
100 svn.core.svn_node_file: 'file',
100 svn.core.svn_node_file: 'file',
101 svn.core.svn_node_dir: 'dir',
101 svn.core.svn_node_dir: 'dir',
102 }
102 }
103
103
104
104
105 class SvnRemote(RemoteBase):
105 class SvnRemote(RemoteBase):
106
106
107 def __init__(self, factory, hg_factory=None):
107 def __init__(self, factory, hg_factory=None):
108 self._factory = factory
108 self._factory = factory
109 # TODO: Remove once we do not use internal Mercurial objects anymore
109 # TODO: Remove once we do not use internal Mercurial objects anymore
110 # for subversion
110 # for subversion
111 self._hg_factory = hg_factory
111 self._hg_factory = hg_factory
112
112
113 @reraise_safe_exceptions
113 @reraise_safe_exceptions
114 def discover_svn_version(self):
114 def discover_svn_version(self):
115 try:
115 try:
116 import svn.core
116 import svn.core
117 svn_ver = svn.core.SVN_VERSION
117 svn_ver = svn.core.SVN_VERSION
118 except ImportError:
118 except ImportError:
119 svn_ver = None
119 svn_ver = None
120 return svn_ver
120 return svn_ver
121
121
122 @reraise_safe_exceptions
122 @reraise_safe_exceptions
123 def is_empty(self, wire):
123 def is_empty(self, wire):
124
124
125 try:
125 try:
126 return self.lookup(wire, -1) == 0
126 return self.lookup(wire, -1) == 0
127 except Exception:
127 except Exception:
128 log.exception("failed to read object_store")
128 log.exception("failed to read object_store")
129 return False
129 return False
130
130
131 def check_url(self, url, config_items):
131 def check_url(self, url, config_items):
132 # this can throw exception if not installed, but we detect this
132 # this can throw exception if not installed, but we detect this
133 from hgsubversion import svnrepo
133 from hgsubversion import svnrepo
134
134
135 baseui = self._hg_factory._create_config(config_items)
135 baseui = self._hg_factory._create_config(config_items)
136 # uuid function get's only valid UUID from proper repo, else
136 # uuid function get's only valid UUID from proper repo, else
137 # throws exception
137 # throws exception
138 try:
138 try:
139 svnrepo.svnremoterepo(baseui, url).svn.uuid
139 svnrepo.svnremoterepo(baseui, url).svn.uuid
140 except Exception:
140 except Exception:
141 tb = traceback.format_exc()
141 tb = traceback.format_exc()
142 log.debug("Invalid Subversion url: `%s`, tb: %s", url, tb)
142 log.debug("Invalid Subversion url: `%s`, tb: %s", url, tb)
143 raise URLError(
143 raise URLError(
144 '"%s" is not a valid Subversion source url.' % (url, ))
144 '"%s" is not a valid Subversion source url.' % (url, ))
145 return True
145 return True
146
146
147 def is_path_valid_repository(self, wire, path):
147 def is_path_valid_repository(self, wire, path):
148
148
149 # NOTE(marcink): short circuit the check for SVN repo
149 # NOTE(marcink): short circuit the check for SVN repo
150 # the repos.open might be expensive to check, but we have one cheap
150 # the repos.open might be expensive to check, but we have one cheap
151 # pre condition that we can use, to check for 'format' file
151 # pre condition that we can use, to check for 'format' file
152
152
153 if not os.path.isfile(os.path.join(path, 'format')):
153 if not os.path.isfile(os.path.join(path, 'format')):
154 return False
154 return False
155
155
156 try:
156 try:
157 svn.repos.open(path)
157 svn.repos.open(path)
158 except svn.core.SubversionException:
158 except svn.core.SubversionException:
159 tb = traceback.format_exc()
159 tb = traceback.format_exc()
160 log.debug("Invalid Subversion path `%s`, tb: %s", path, tb)
160 log.debug("Invalid Subversion path `%s`, tb: %s", path, tb)
161 return False
161 return False
162 return True
162 return True
163
163
164 @reraise_safe_exceptions
164 @reraise_safe_exceptions
165 def verify(self, wire,):
165 def verify(self, wire,):
166 repo_path = wire['path']
166 repo_path = wire['path']
167 if not self.is_path_valid_repository(wire, repo_path):
167 if not self.is_path_valid_repository(wire, repo_path):
168 raise Exception(
168 raise Exception(
169 "Path %s is not a valid Subversion repository." % repo_path)
169 "Path %s is not a valid Subversion repository." % repo_path)
170
170
171 cmd = ['svnadmin', 'info', repo_path]
171 cmd = ['svnadmin', 'info', repo_path]
172 stdout, stderr = subprocessio.run_command(cmd)
172 stdout, stderr = subprocessio.run_command(cmd)
173 return stdout
173 return stdout
174
174
175 def lookup(self, wire, revision):
175 def lookup(self, wire, revision):
176 if revision not in [-1, None, 'HEAD']:
176 if revision not in [-1, None, 'HEAD']:
177 raise NotImplementedError
177 raise NotImplementedError
178 repo = self._factory.repo(wire)
178 repo = self._factory.repo(wire)
179 fs_ptr = svn.repos.fs(repo)
179 fs_ptr = svn.repos.fs(repo)
180 head = svn.fs.youngest_rev(fs_ptr)
180 head = svn.fs.youngest_rev(fs_ptr)
181 return head
181 return head
182
182
183 def lookup_interval(self, wire, start_ts, end_ts):
183 def lookup_interval(self, wire, start_ts, end_ts):
184 repo = self._factory.repo(wire)
184 repo = self._factory.repo(wire)
185 fsobj = svn.repos.fs(repo)
185 fsobj = svn.repos.fs(repo)
186 start_rev = None
186 start_rev = None
187 end_rev = None
187 end_rev = None
188 if start_ts:
188 if start_ts:
189 start_ts_svn = apr_time_t(start_ts)
189 start_ts_svn = apr_time_t(start_ts)
190 start_rev = svn.repos.dated_revision(repo, start_ts_svn) + 1
190 start_rev = svn.repos.dated_revision(repo, start_ts_svn) + 1
191 else:
191 else:
192 start_rev = 1
192 start_rev = 1
193 if end_ts:
193 if end_ts:
194 end_ts_svn = apr_time_t(end_ts)
194 end_ts_svn = apr_time_t(end_ts)
195 end_rev = svn.repos.dated_revision(repo, end_ts_svn)
195 end_rev = svn.repos.dated_revision(repo, end_ts_svn)
196 else:
196 else:
197 end_rev = svn.fs.youngest_rev(fsobj)
197 end_rev = svn.fs.youngest_rev(fsobj)
198 return start_rev, end_rev
198 return start_rev, end_rev
199
199
200 def revision_properties(self, wire, revision):
200 def revision_properties(self, wire, revision):
201
201
202 cache_on, context_uid, repo_id = self._cache_on(wire)
202 cache_on, context_uid, repo_id = self._cache_on(wire)
203 @self.region.conditional_cache_on_arguments(condition=cache_on)
203 @self.region.conditional_cache_on_arguments(condition=cache_on)
204 def _revision_properties(_repo_id, _revision):
204 def _revision_properties(_repo_id, _revision):
205 repo = self._factory.repo(wire)
205 repo = self._factory.repo(wire)
206 fs_ptr = svn.repos.fs(repo)
206 fs_ptr = svn.repos.fs(repo)
207 return svn.fs.revision_proplist(fs_ptr, revision)
207 return svn.fs.revision_proplist(fs_ptr, revision)
208 return _revision_properties(repo_id, revision)
208 return _revision_properties(repo_id, revision)
209
209
210 def revision_changes(self, wire, revision):
210 def revision_changes(self, wire, revision):
211
211
212 repo = self._factory.repo(wire)
212 repo = self._factory.repo(wire)
213 fsobj = svn.repos.fs(repo)
213 fsobj = svn.repos.fs(repo)
214 rev_root = svn.fs.revision_root(fsobj, revision)
214 rev_root = svn.fs.revision_root(fsobj, revision)
215
215
216 editor = svn.repos.ChangeCollector(fsobj, rev_root)
216 editor = svn.repos.ChangeCollector(fsobj, rev_root)
217 editor_ptr, editor_baton = svn.delta.make_editor(editor)
217 editor_ptr, editor_baton = svn.delta.make_editor(editor)
218 base_dir = ""
218 base_dir = ""
219 send_deltas = False
219 send_deltas = False
220 svn.repos.replay2(
220 svn.repos.replay2(
221 rev_root, base_dir, svn.core.SVN_INVALID_REVNUM, send_deltas,
221 rev_root, base_dir, svn.core.SVN_INVALID_REVNUM, send_deltas,
222 editor_ptr, editor_baton, None)
222 editor_ptr, editor_baton, None)
223
223
224 added = []
224 added = []
225 changed = []
225 changed = []
226 removed = []
226 removed = []
227
227
228 # TODO: CHANGE_ACTION_REPLACE: Figure out where it belongs
228 # TODO: CHANGE_ACTION_REPLACE: Figure out where it belongs
229 for path, change in editor.changes.iteritems():
229 for path, change in editor.changes.iteritems():
230 # TODO: Decide what to do with directory nodes. Subversion can add
230 # TODO: Decide what to do with directory nodes. Subversion can add
231 # empty directories.
231 # empty directories.
232
232
233 if change.item_kind == svn.core.svn_node_dir:
233 if change.item_kind == svn.core.svn_node_dir:
234 continue
234 continue
235 if change.action in [svn.repos.CHANGE_ACTION_ADD]:
235 if change.action in [svn.repos.CHANGE_ACTION_ADD]:
236 added.append(path)
236 added.append(path)
237 elif change.action in [svn.repos.CHANGE_ACTION_MODIFY,
237 elif change.action in [svn.repos.CHANGE_ACTION_MODIFY,
238 svn.repos.CHANGE_ACTION_REPLACE]:
238 svn.repos.CHANGE_ACTION_REPLACE]:
239 changed.append(path)
239 changed.append(path)
240 elif change.action in [svn.repos.CHANGE_ACTION_DELETE]:
240 elif change.action in [svn.repos.CHANGE_ACTION_DELETE]:
241 removed.append(path)
241 removed.append(path)
242 else:
242 else:
243 raise NotImplementedError(
243 raise NotImplementedError(
244 "Action %s not supported on path %s" % (
244 "Action %s not supported on path %s" % (
245 change.action, path))
245 change.action, path))
246
246
247 changes = {
247 changes = {
248 'added': added,
248 'added': added,
249 'changed': changed,
249 'changed': changed,
250 'removed': removed,
250 'removed': removed,
251 }
251 }
252 return changes
252 return changes
253
253
254 @reraise_safe_exceptions
254 @reraise_safe_exceptions
255 def node_history(self, wire, path, revision, limit):
255 def node_history(self, wire, path, revision, limit):
256 cache_on, context_uid, repo_id = self._cache_on(wire)
256 cache_on, context_uid, repo_id = self._cache_on(wire)
257 @self.region.conditional_cache_on_arguments(condition=cache_on)
257 @self.region.conditional_cache_on_arguments(condition=cache_on)
258 def _assert_correct_path(_context_uid, _repo_id, _path, _revision, _limit):
258 def _assert_correct_path(_context_uid, _repo_id, _path, _revision, _limit):
259 cross_copies = False
259 cross_copies = False
260 repo = self._factory.repo(wire)
260 repo = self._factory.repo(wire)
261 fsobj = svn.repos.fs(repo)
261 fsobj = svn.repos.fs(repo)
262 rev_root = svn.fs.revision_root(fsobj, revision)
262 rev_root = svn.fs.revision_root(fsobj, revision)
263
263
264 history_revisions = []
264 history_revisions = []
265 history = svn.fs.node_history(rev_root, path)
265 history = svn.fs.node_history(rev_root, path)
266 history = svn.fs.history_prev(history, cross_copies)
266 history = svn.fs.history_prev(history, cross_copies)
267 while history:
267 while history:
268 __, node_revision = svn.fs.history_location(history)
268 __, node_revision = svn.fs.history_location(history)
269 history_revisions.append(node_revision)
269 history_revisions.append(node_revision)
270 if limit and len(history_revisions) >= limit:
270 if limit and len(history_revisions) >= limit:
271 break
271 break
272 history = svn.fs.history_prev(history, cross_copies)
272 history = svn.fs.history_prev(history, cross_copies)
273 return history_revisions
273 return history_revisions
274 return _assert_correct_path(context_uid, repo_id, path, revision, limit)
274 return _assert_correct_path(context_uid, repo_id, path, revision, limit)
275
275
276 def node_properties(self, wire, path, revision):
276 def node_properties(self, wire, path, revision):
277 cache_on, context_uid, repo_id = self._cache_on(wire)
277 cache_on, context_uid, repo_id = self._cache_on(wire)
278 @self.region.conditional_cache_on_arguments(condition=cache_on)
278 @self.region.conditional_cache_on_arguments(condition=cache_on)
279 def _node_properties(_repo_id, _path, _revision):
279 def _node_properties(_repo_id, _path, _revision):
280 repo = self._factory.repo(wire)
280 repo = self._factory.repo(wire)
281 fsobj = svn.repos.fs(repo)
281 fsobj = svn.repos.fs(repo)
282 rev_root = svn.fs.revision_root(fsobj, revision)
282 rev_root = svn.fs.revision_root(fsobj, revision)
283 return svn.fs.node_proplist(rev_root, path)
283 return svn.fs.node_proplist(rev_root, path)
284 return _node_properties(repo_id, path, revision)
284 return _node_properties(repo_id, path, revision)
285
285
286 def file_annotate(self, wire, path, revision):
286 def file_annotate(self, wire, path, revision):
287 abs_path = 'file://' + urllib.pathname2url(
287 abs_path = 'file://' + urllib.pathname2url(
288 vcspath.join(wire['path'], path))
288 vcspath.join(wire['path'], path))
289 file_uri = svn.core.svn_path_canonicalize(abs_path)
289 file_uri = svn.core.svn_path_canonicalize(abs_path)
290
290
291 start_rev = svn_opt_revision_value_t(0)
291 start_rev = svn_opt_revision_value_t(0)
292 peg_rev = svn_opt_revision_value_t(revision)
292 peg_rev = svn_opt_revision_value_t(revision)
293 end_rev = peg_rev
293 end_rev = peg_rev
294
294
295 annotations = []
295 annotations = []
296
296
297 def receiver(line_no, revision, author, date, line, pool):
297 def receiver(line_no, revision, author, date, line, pool):
298 annotations.append((line_no, revision, line))
298 annotations.append((line_no, revision, line))
299
299
300 # TODO: Cannot use blame5, missing typemap function in the swig code
300 # TODO: Cannot use blame5, missing typemap function in the swig code
301 try:
301 try:
302 svn.client.blame2(
302 svn.client.blame2(
303 file_uri, peg_rev, start_rev, end_rev,
303 file_uri, peg_rev, start_rev, end_rev,
304 receiver, svn.client.create_context())
304 receiver, svn.client.create_context())
305 except svn.core.SubversionException as exc:
305 except svn.core.SubversionException as exc:
306 log.exception("Error during blame operation.")
306 log.exception("Error during blame operation.")
307 raise Exception(
307 raise Exception(
308 "Blame not supported or file does not exist at path %s. "
308 "Blame not supported or file does not exist at path %s. "
309 "Error %s." % (path, exc))
309 "Error %s." % (path, exc))
310
310
311 return annotations
311 return annotations
312
312
313 def get_node_type(self, wire, path, revision=None):
313 def get_node_type(self, wire, path, revision=None):
314
314
315 cache_on, context_uid, repo_id = self._cache_on(wire)
315 cache_on, context_uid, repo_id = self._cache_on(wire)
316 @self.region.conditional_cache_on_arguments(condition=cache_on)
316 @self.region.conditional_cache_on_arguments(condition=cache_on)
317 def _get_node_type(_repo_id, _path, _revision):
317 def _get_node_type(_repo_id, _path, _revision):
318 repo = self._factory.repo(wire)
318 repo = self._factory.repo(wire)
319 fs_ptr = svn.repos.fs(repo)
319 fs_ptr = svn.repos.fs(repo)
320 if _revision is None:
320 if _revision is None:
321 _revision = svn.fs.youngest_rev(fs_ptr)
321 _revision = svn.fs.youngest_rev(fs_ptr)
322 root = svn.fs.revision_root(fs_ptr, _revision)
322 root = svn.fs.revision_root(fs_ptr, _revision)
323 node = svn.fs.check_path(root, path)
323 node = svn.fs.check_path(root, path)
324 return NODE_TYPE_MAPPING.get(node, None)
324 return NODE_TYPE_MAPPING.get(node, None)
325 return _get_node_type(repo_id, path, revision)
325 return _get_node_type(repo_id, path, revision)
326
326
327 def get_nodes(self, wire, path, revision=None):
327 def get_nodes(self, wire, path, revision=None):
328
328
329 cache_on, context_uid, repo_id = self._cache_on(wire)
329 cache_on, context_uid, repo_id = self._cache_on(wire)
330 @self.region.conditional_cache_on_arguments(condition=cache_on)
330 @self.region.conditional_cache_on_arguments(condition=cache_on)
331 def _get_nodes(_repo_id, _path, _revision):
331 def _get_nodes(_repo_id, _path, _revision):
332 repo = self._factory.repo(wire)
332 repo = self._factory.repo(wire)
333 fsobj = svn.repos.fs(repo)
333 fsobj = svn.repos.fs(repo)
334 if _revision is None:
334 if _revision is None:
335 _revision = svn.fs.youngest_rev(fsobj)
335 _revision = svn.fs.youngest_rev(fsobj)
336 root = svn.fs.revision_root(fsobj, _revision)
336 root = svn.fs.revision_root(fsobj, _revision)
337 entries = svn.fs.dir_entries(root, path)
337 entries = svn.fs.dir_entries(root, path)
338 result = []
338 result = []
339 for entry_path, entry_info in entries.iteritems():
339 for entry_path, entry_info in entries.iteritems():
340 result.append(
340 result.append(
341 (entry_path, NODE_TYPE_MAPPING.get(entry_info.kind, None)))
341 (entry_path, NODE_TYPE_MAPPING.get(entry_info.kind, None)))
342 return result
342 return result
343 return _get_nodes(repo_id, path, revision)
343 return _get_nodes(repo_id, path, revision)
344
344
345 def get_file_content(self, wire, path, rev=None):
345 def get_file_content(self, wire, path, rev=None):
346 repo = self._factory.repo(wire)
346 repo = self._factory.repo(wire)
347 fsobj = svn.repos.fs(repo)
347 fsobj = svn.repos.fs(repo)
348 if rev is None:
348 if rev is None:
349 rev = svn.fs.youngest_revision(fsobj)
349 rev = svn.fs.youngest_revision(fsobj)
350 root = svn.fs.revision_root(fsobj, rev)
350 root = svn.fs.revision_root(fsobj, rev)
351 content = svn.core.Stream(svn.fs.file_contents(root, path))
351 content = svn.core.Stream(svn.fs.file_contents(root, path))
352 return content.read()
352 return content.read()
353
353
354 def get_file_size(self, wire, path, revision=None):
354 def get_file_size(self, wire, path, revision=None):
355
355
356 cache_on, context_uid, repo_id = self._cache_on(wire)
356 cache_on, context_uid, repo_id = self._cache_on(wire)
357 @self.region.conditional_cache_on_arguments(condition=cache_on)
357 @self.region.conditional_cache_on_arguments(condition=cache_on)
358 def _get_file_size(_repo_id, _path, _revision):
358 def _get_file_size(_repo_id, _path, _revision):
359 repo = self._factory.repo(wire)
359 repo = self._factory.repo(wire)
360 fsobj = svn.repos.fs(repo)
360 fsobj = svn.repos.fs(repo)
361 if _revision is None:
361 if _revision is None:
362 _revision = svn.fs.youngest_revision(fsobj)
362 _revision = svn.fs.youngest_revision(fsobj)
363 root = svn.fs.revision_root(fsobj, _revision)
363 root = svn.fs.revision_root(fsobj, _revision)
364 size = svn.fs.file_length(root, path)
364 size = svn.fs.file_length(root, path)
365 return size
365 return size
366 return _get_file_size(repo_id, path, revision)
366 return _get_file_size(repo_id, path, revision)
367
367
368 def create_repository(self, wire, compatible_version=None):
368 def create_repository(self, wire, compatible_version=None):
369 log.info('Creating Subversion repository in path "%s"', wire['path'])
369 log.info('Creating Subversion repository in path "%s"', wire['path'])
370 self._factory.repo(wire, create=True,
370 self._factory.repo(wire, create=True,
371 compatible_version=compatible_version)
371 compatible_version=compatible_version)
372
372
373 def get_url_and_credentials(self, src_url):
373 def get_url_and_credentials(self, src_url):
374 obj = urlparse.urlparse(src_url)
374 obj = urlparse.urlparse(src_url)
375 username = obj.username or None
375 username = obj.username or None
376 password = obj.password or None
376 password = obj.password or None
377 return username, password, src_url
377 return username, password, src_url
378
378
379 def import_remote_repository(self, wire, src_url):
379 def import_remote_repository(self, wire, src_url):
380 repo_path = wire['path']
380 repo_path = wire['path']
381 if not self.is_path_valid_repository(wire, repo_path):
381 if not self.is_path_valid_repository(wire, repo_path):
382 raise Exception(
382 raise Exception(
383 "Path %s is not a valid Subversion repository." % repo_path)
383 "Path %s is not a valid Subversion repository." % repo_path)
384
384
385 username, password, src_url = self.get_url_and_credentials(src_url)
385 username, password, src_url = self.get_url_and_credentials(src_url)
386 rdump_cmd = ['svnrdump', 'dump', '--non-interactive',
386 rdump_cmd = ['svnrdump', 'dump', '--non-interactive',
387 '--trust-server-cert-failures=unknown-ca']
387 '--trust-server-cert-failures=unknown-ca']
388 if username and password:
388 if username and password:
389 rdump_cmd += ['--username', username, '--password', password]
389 rdump_cmd += ['--username', username, '--password', password]
390 rdump_cmd += [src_url]
390 rdump_cmd += [src_url]
391
391
392 rdump = subprocess.Popen(
392 rdump = subprocess.Popen(
393 rdump_cmd,
393 rdump_cmd,
394 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
394 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
395 load = subprocess.Popen(
395 load = subprocess.Popen(
396 ['svnadmin', 'load', repo_path], stdin=rdump.stdout)
396 ['svnadmin', 'load', repo_path], stdin=rdump.stdout)
397
397
398 # TODO: johbo: This can be a very long operation, might be better
398 # TODO: johbo: This can be a very long operation, might be better
399 # to track some kind of status and provide an api to check if the
399 # to track some kind of status and provide an api to check if the
400 # import is done.
400 # import is done.
401 rdump.wait()
401 rdump.wait()
402 load.wait()
402 load.wait()
403
403
404 log.debug('Return process ended with code: %s', rdump.returncode)
404 log.debug('Return process ended with code: %s', rdump.returncode)
405 if rdump.returncode != 0:
405 if rdump.returncode != 0:
406 errors = rdump.stderr.read()
406 errors = rdump.stderr.read()
407 log.error('svnrdump dump failed: statuscode %s: message: %s',
407 log.error('svnrdump dump failed: statuscode %s: message: %s',
408 rdump.returncode, errors)
408 rdump.returncode, errors)
409 reason = 'UNKNOWN'
409 reason = 'UNKNOWN'
410 if 'svnrdump: E230001:' in errors:
410 if 'svnrdump: E230001:' in errors:
411 reason = 'INVALID_CERTIFICATE'
411 reason = 'INVALID_CERTIFICATE'
412
412
413 if reason == 'UNKNOWN':
413 if reason == 'UNKNOWN':
414 reason = 'UNKNOWN:{}'.format(errors)
414 reason = 'UNKNOWN:{}'.format(errors)
415 raise Exception(
415 raise Exception(
416 'Failed to dump the remote repository from %s. Reason:%s' % (
416 'Failed to dump the remote repository from %s. Reason:%s' % (
417 src_url, reason))
417 src_url, reason))
418 if load.returncode != 0:
418 if load.returncode != 0:
419 raise Exception(
419 raise Exception(
420 'Failed to load the dump of remote repository from %s.' %
420 'Failed to load the dump of remote repository from %s.' %
421 (src_url, ))
421 (src_url, ))
422
422
423 def commit(self, wire, message, author, timestamp, updated, removed):
423 def commit(self, wire, message, author, timestamp, updated, removed):
424 assert isinstance(message, str)
424 assert isinstance(message, str)
425 assert isinstance(author, str)
425 assert isinstance(author, str)
426
426
427 repo = self._factory.repo(wire)
427 repo = self._factory.repo(wire)
428 fsobj = svn.repos.fs(repo)
428 fsobj = svn.repos.fs(repo)
429
429
430 rev = svn.fs.youngest_rev(fsobj)
430 rev = svn.fs.youngest_rev(fsobj)
431 txn = svn.repos.fs_begin_txn_for_commit(repo, rev, author, message)
431 txn = svn.repos.fs_begin_txn_for_commit(repo, rev, author, message)
432 txn_root = svn.fs.txn_root(txn)
432 txn_root = svn.fs.txn_root(txn)
433
433
434 for node in updated:
434 for node in updated:
435 TxnNodeProcessor(node, txn_root).update()
435 TxnNodeProcessor(node, txn_root).update()
436 for node in removed:
436 for node in removed:
437 TxnNodeProcessor(node, txn_root).remove()
437 TxnNodeProcessor(node, txn_root).remove()
438
438
439 commit_id = svn.repos.fs_commit_txn(repo, txn)
439 commit_id = svn.repos.fs_commit_txn(repo, txn)
440
440
441 if timestamp:
441 if timestamp:
442 apr_time = apr_time_t(timestamp)
442 apr_time = apr_time_t(timestamp)
443 ts_formatted = svn.core.svn_time_to_cstring(apr_time)
443 ts_formatted = svn.core.svn_time_to_cstring(apr_time)
444 svn.fs.change_rev_prop(fsobj, commit_id, 'svn:date', ts_formatted)
444 svn.fs.change_rev_prop(fsobj, commit_id, 'svn:date', ts_formatted)
445
445
446 log.debug('Committed revision "%s" to "%s".', commit_id, wire['path'])
446 log.debug('Committed revision "%s" to "%s".', commit_id, wire['path'])
447 return commit_id
447 return commit_id
448
448
449 def diff(self, wire, rev1, rev2, path1=None, path2=None,
449 def diff(self, wire, rev1, rev2, path1=None, path2=None,
450 ignore_whitespace=False, context=3):
450 ignore_whitespace=False, context=3):
451
451
452 wire.update(cache=False)
452 wire.update(cache=False)
453 repo = self._factory.repo(wire)
453 repo = self._factory.repo(wire)
454 diff_creator = SvnDiffer(
454 diff_creator = SvnDiffer(
455 repo, rev1, path1, rev2, path2, ignore_whitespace, context)
455 repo, rev1, path1, rev2, path2, ignore_whitespace, context)
456 try:
456 try:
457 return diff_creator.generate_diff()
457 return diff_creator.generate_diff()
458 except svn.core.SubversionException as e:
458 except svn.core.SubversionException as e:
459 log.exception(
459 log.exception(
460 "Error during diff operation operation. "
460 "Error during diff operation operation. "
461 "Path might not exist %s, %s" % (path1, path2))
461 "Path might not exist %s, %s" % (path1, path2))
462 return ""
462 return ""
463
463
464 @reraise_safe_exceptions
464 @reraise_safe_exceptions
465 def is_large_file(self, wire, path):
465 def is_large_file(self, wire, path):
466 return False
466 return False
467
467
468 @reraise_safe_exceptions
468 @reraise_safe_exceptions
469 def is_binary(self, wire, rev, path):
469 def is_binary(self, wire, rev, path):
470 cache_on, context_uid, repo_id = self._cache_on(wire)
470 cache_on, context_uid, repo_id = self._cache_on(wire)
471
471
472 @self.region.conditional_cache_on_arguments(condition=cache_on)
472 @self.region.conditional_cache_on_arguments(condition=cache_on)
473 def _is_binary(_repo_id, _rev, _path):
473 def _is_binary(_repo_id, _rev, _path):
474 raw_bytes = self.get_file_content(wire, path, rev)
474 raw_bytes = self.get_file_content(wire, path, rev)
475 return raw_bytes and '\0' in raw_bytes
475 return raw_bytes and '\0' in raw_bytes
476
476
477 return _is_binary(repo_id, rev, path)
477 return _is_binary(repo_id, rev, path)
478
478
479 @reraise_safe_exceptions
479 @reraise_safe_exceptions
480 def run_svn_command(self, wire, cmd, **opts):
480 def run_svn_command(self, wire, cmd, **opts):
481 path = wire.get('path', None)
481 path = wire.get('path', None)
482
482
483 if path and os.path.isdir(path):
483 if path and os.path.isdir(path):
484 opts['cwd'] = path
484 opts['cwd'] = path
485
485
486 safe_call = False
486 safe_call = False
487 if '_safe' in opts:
487 if '_safe' in opts:
488 safe_call = True
488 safe_call = True
489
489
490 svnenv = os.environ.copy()
490 svnenv = os.environ.copy()
491 svnenv.update(opts.pop('extra_env', {}))
491 svnenv.update(opts.pop('extra_env', {}))
492
492
493 _opts = {'env': svnenv, 'shell': False}
493 _opts = {'env': svnenv, 'shell': False}
494
494
495 try:
495 try:
496 _opts.update(opts)
496 _opts.update(opts)
497 p = subprocessio.SubprocessIOChunker(cmd, **_opts)
497 p = subprocessio.SubprocessIOChunker(cmd, **_opts)
498
498
499 return ''.join(p), ''.join(p.error)
499 return ''.join(p), ''.join(p.error)
500 except (EnvironmentError, OSError) as err:
500 except (EnvironmentError, OSError) as err:
501 cmd = ' '.join(cmd) # human friendly CMD
502 tb_err = ("Couldn't run svn command (%s).\n"
503 "Original error was:%s\n"
504 "Call options:%s\n"
505 % (cmd, err, _opts))
506 log.exception(tb_err)
507 if safe_call:
501 if safe_call:
508 return '', err
502 return '', err
509 else:
503 else:
504 cmd = ' '.join(cmd) # human friendly CMD
505 tb_err = ("Couldn't run svn command (%s).\n"
506 "Original error was:%s\n"
507 "Call options:%s\n"
508 % (cmd, err, _opts))
509 log.exception(tb_err)
510 raise exceptions.VcsException()(tb_err)
510 raise exceptions.VcsException()(tb_err)
511
511
512 @reraise_safe_exceptions
512 @reraise_safe_exceptions
513 def install_hooks(self, wire, force=False):
513 def install_hooks(self, wire, force=False):
514 from vcsserver.hook_utils import install_svn_hooks
514 from vcsserver.hook_utils import install_svn_hooks
515 repo_path = wire['path']
515 repo_path = wire['path']
516 binary_dir = settings.BINARY_DIR
516 binary_dir = settings.BINARY_DIR
517 executable = None
517 executable = None
518 if binary_dir:
518 if binary_dir:
519 executable = os.path.join(binary_dir, 'python')
519 executable = os.path.join(binary_dir, 'python')
520 return install_svn_hooks(
520 return install_svn_hooks(
521 repo_path, executable=executable, force_create=force)
521 repo_path, executable=executable, force_create=force)
522
522
523 @reraise_safe_exceptions
523 @reraise_safe_exceptions
524 def get_hooks_info(self, wire):
524 def get_hooks_info(self, wire):
525 from vcsserver.hook_utils import (
525 from vcsserver.hook_utils import (
526 get_svn_pre_hook_version, get_svn_post_hook_version)
526 get_svn_pre_hook_version, get_svn_post_hook_version)
527 repo_path = wire['path']
527 repo_path = wire['path']
528 return {
528 return {
529 'pre_version': get_svn_pre_hook_version(repo_path),
529 'pre_version': get_svn_pre_hook_version(repo_path),
530 'post_version': get_svn_post_hook_version(repo_path),
530 'post_version': get_svn_post_hook_version(repo_path),
531 }
531 }
532
532
533 @reraise_safe_exceptions
533 @reraise_safe_exceptions
534 def archive_repo(self, wire, archive_dest_path, kind, mtime, archive_at_path,
534 def archive_repo(self, wire, archive_dest_path, kind, mtime, archive_at_path,
535 archive_dir_name, commit_id):
535 archive_dir_name, commit_id):
536
536
537 def walk_tree(root, root_dir, _commit_id):
537 def walk_tree(root, root_dir, _commit_id):
538 """
538 """
539 Special recursive svn repo walker
539 Special recursive svn repo walker
540 """
540 """
541
541
542 filemode_default = 0o100644
542 filemode_default = 0o100644
543 filemode_executable = 0o100755
543 filemode_executable = 0o100755
544
544
545 file_iter = svn.fs.dir_entries(root, root_dir)
545 file_iter = svn.fs.dir_entries(root, root_dir)
546 for f_name in file_iter:
546 for f_name in file_iter:
547 f_type = NODE_TYPE_MAPPING.get(file_iter[f_name].kind, None)
547 f_type = NODE_TYPE_MAPPING.get(file_iter[f_name].kind, None)
548
548
549 if f_type == 'dir':
549 if f_type == 'dir':
550 # return only DIR, and then all entries in that dir
550 # return only DIR, and then all entries in that dir
551 yield os.path.join(root_dir, f_name), {'mode': filemode_default}, f_type
551 yield os.path.join(root_dir, f_name), {'mode': filemode_default}, f_type
552 new_root = os.path.join(root_dir, f_name)
552 new_root = os.path.join(root_dir, f_name)
553 for _f_name, _f_data, _f_type in walk_tree(root, new_root, _commit_id):
553 for _f_name, _f_data, _f_type in walk_tree(root, new_root, _commit_id):
554 yield _f_name, _f_data, _f_type
554 yield _f_name, _f_data, _f_type
555 else:
555 else:
556 f_path = os.path.join(root_dir, f_name).rstrip('/')
556 f_path = os.path.join(root_dir, f_name).rstrip('/')
557 prop_list = svn.fs.node_proplist(root, f_path)
557 prop_list = svn.fs.node_proplist(root, f_path)
558
558
559 f_mode = filemode_default
559 f_mode = filemode_default
560 if prop_list.get('svn:executable'):
560 if prop_list.get('svn:executable'):
561 f_mode = filemode_executable
561 f_mode = filemode_executable
562
562
563 f_is_link = False
563 f_is_link = False
564 if prop_list.get('svn:special'):
564 if prop_list.get('svn:special'):
565 f_is_link = True
565 f_is_link = True
566
566
567 data = {
567 data = {
568 'is_link': f_is_link,
568 'is_link': f_is_link,
569 'mode': f_mode,
569 'mode': f_mode,
570 'content_stream': svn.core.Stream(svn.fs.file_contents(root, f_path)).read
570 'content_stream': svn.core.Stream(svn.fs.file_contents(root, f_path)).read
571 }
571 }
572
572
573 yield f_path, data, f_type
573 yield f_path, data, f_type
574
574
575 def file_walker(_commit_id, path):
575 def file_walker(_commit_id, path):
576 repo = self._factory.repo(wire)
576 repo = self._factory.repo(wire)
577 root = svn.fs.revision_root(svn.repos.fs(repo), int(commit_id))
577 root = svn.fs.revision_root(svn.repos.fs(repo), int(commit_id))
578
578
579 def no_content():
579 def no_content():
580 raise NoContentException()
580 raise NoContentException()
581
581
582 for f_name, f_data, f_type in walk_tree(root, path, _commit_id):
582 for f_name, f_data, f_type in walk_tree(root, path, _commit_id):
583 file_path = f_name
583 file_path = f_name
584
584
585 if f_type == 'dir':
585 if f_type == 'dir':
586 mode = f_data['mode']
586 mode = f_data['mode']
587 yield ArchiveNode(file_path, mode, False, no_content)
587 yield ArchiveNode(file_path, mode, False, no_content)
588 else:
588 else:
589 mode = f_data['mode']
589 mode = f_data['mode']
590 is_link = f_data['is_link']
590 is_link = f_data['is_link']
591 data_stream = f_data['content_stream']
591 data_stream = f_data['content_stream']
592 yield ArchiveNode(file_path, mode, is_link, data_stream)
592 yield ArchiveNode(file_path, mode, is_link, data_stream)
593
593
594 return archive_repo(file_walker, archive_dest_path, kind, mtime, archive_at_path,
594 return archive_repo(file_walker, archive_dest_path, kind, mtime, archive_at_path,
595 archive_dir_name, commit_id)
595 archive_dir_name, commit_id)
596
596
597
597
598 class SvnDiffer(object):
598 class SvnDiffer(object):
599 """
599 """
600 Utility to create diffs based on difflib and the Subversion api
600 Utility to create diffs based on difflib and the Subversion api
601 """
601 """
602
602
603 binary_content = False
603 binary_content = False
604
604
605 def __init__(
605 def __init__(
606 self, repo, src_rev, src_path, tgt_rev, tgt_path,
606 self, repo, src_rev, src_path, tgt_rev, tgt_path,
607 ignore_whitespace, context):
607 ignore_whitespace, context):
608 self.repo = repo
608 self.repo = repo
609 self.ignore_whitespace = ignore_whitespace
609 self.ignore_whitespace = ignore_whitespace
610 self.context = context
610 self.context = context
611
611
612 fsobj = svn.repos.fs(repo)
612 fsobj = svn.repos.fs(repo)
613
613
614 self.tgt_rev = tgt_rev
614 self.tgt_rev = tgt_rev
615 self.tgt_path = tgt_path or ''
615 self.tgt_path = tgt_path or ''
616 self.tgt_root = svn.fs.revision_root(fsobj, tgt_rev)
616 self.tgt_root = svn.fs.revision_root(fsobj, tgt_rev)
617 self.tgt_kind = svn.fs.check_path(self.tgt_root, self.tgt_path)
617 self.tgt_kind = svn.fs.check_path(self.tgt_root, self.tgt_path)
618
618
619 self.src_rev = src_rev
619 self.src_rev = src_rev
620 self.src_path = src_path or self.tgt_path
620 self.src_path = src_path or self.tgt_path
621 self.src_root = svn.fs.revision_root(fsobj, src_rev)
621 self.src_root = svn.fs.revision_root(fsobj, src_rev)
622 self.src_kind = svn.fs.check_path(self.src_root, self.src_path)
622 self.src_kind = svn.fs.check_path(self.src_root, self.src_path)
623
623
624 self._validate()
624 self._validate()
625
625
626 def _validate(self):
626 def _validate(self):
627 if (self.tgt_kind != svn.core.svn_node_none and
627 if (self.tgt_kind != svn.core.svn_node_none and
628 self.src_kind != svn.core.svn_node_none and
628 self.src_kind != svn.core.svn_node_none and
629 self.src_kind != self.tgt_kind):
629 self.src_kind != self.tgt_kind):
630 # TODO: johbo: proper error handling
630 # TODO: johbo: proper error handling
631 raise Exception(
631 raise Exception(
632 "Source and target are not compatible for diff generation. "
632 "Source and target are not compatible for diff generation. "
633 "Source type: %s, target type: %s" %
633 "Source type: %s, target type: %s" %
634 (self.src_kind, self.tgt_kind))
634 (self.src_kind, self.tgt_kind))
635
635
636 def generate_diff(self):
636 def generate_diff(self):
637 buf = StringIO.StringIO()
637 buf = StringIO.StringIO()
638 if self.tgt_kind == svn.core.svn_node_dir:
638 if self.tgt_kind == svn.core.svn_node_dir:
639 self._generate_dir_diff(buf)
639 self._generate_dir_diff(buf)
640 else:
640 else:
641 self._generate_file_diff(buf)
641 self._generate_file_diff(buf)
642 return buf.getvalue()
642 return buf.getvalue()
643
643
644 def _generate_dir_diff(self, buf):
644 def _generate_dir_diff(self, buf):
645 editor = DiffChangeEditor()
645 editor = DiffChangeEditor()
646 editor_ptr, editor_baton = svn.delta.make_editor(editor)
646 editor_ptr, editor_baton = svn.delta.make_editor(editor)
647 svn.repos.dir_delta2(
647 svn.repos.dir_delta2(
648 self.src_root,
648 self.src_root,
649 self.src_path,
649 self.src_path,
650 '', # src_entry
650 '', # src_entry
651 self.tgt_root,
651 self.tgt_root,
652 self.tgt_path,
652 self.tgt_path,
653 editor_ptr, editor_baton,
653 editor_ptr, editor_baton,
654 authorization_callback_allow_all,
654 authorization_callback_allow_all,
655 False, # text_deltas
655 False, # text_deltas
656 svn.core.svn_depth_infinity, # depth
656 svn.core.svn_depth_infinity, # depth
657 False, # entry_props
657 False, # entry_props
658 False, # ignore_ancestry
658 False, # ignore_ancestry
659 )
659 )
660
660
661 for path, __, change in sorted(editor.changes):
661 for path, __, change in sorted(editor.changes):
662 self._generate_node_diff(
662 self._generate_node_diff(
663 buf, change, path, self.tgt_path, path, self.src_path)
663 buf, change, path, self.tgt_path, path, self.src_path)
664
664
665 def _generate_file_diff(self, buf):
665 def _generate_file_diff(self, buf):
666 change = None
666 change = None
667 if self.src_kind == svn.core.svn_node_none:
667 if self.src_kind == svn.core.svn_node_none:
668 change = "add"
668 change = "add"
669 elif self.tgt_kind == svn.core.svn_node_none:
669 elif self.tgt_kind == svn.core.svn_node_none:
670 change = "delete"
670 change = "delete"
671 tgt_base, tgt_path = vcspath.split(self.tgt_path)
671 tgt_base, tgt_path = vcspath.split(self.tgt_path)
672 src_base, src_path = vcspath.split(self.src_path)
672 src_base, src_path = vcspath.split(self.src_path)
673 self._generate_node_diff(
673 self._generate_node_diff(
674 buf, change, tgt_path, tgt_base, src_path, src_base)
674 buf, change, tgt_path, tgt_base, src_path, src_base)
675
675
676 def _generate_node_diff(
676 def _generate_node_diff(
677 self, buf, change, tgt_path, tgt_base, src_path, src_base):
677 self, buf, change, tgt_path, tgt_base, src_path, src_base):
678
678
679 if self.src_rev == self.tgt_rev and tgt_base == src_base:
679 if self.src_rev == self.tgt_rev and tgt_base == src_base:
680 # makes consistent behaviour with git/hg to return empty diff if
680 # makes consistent behaviour with git/hg to return empty diff if
681 # we compare same revisions
681 # we compare same revisions
682 return
682 return
683
683
684 tgt_full_path = vcspath.join(tgt_base, tgt_path)
684 tgt_full_path = vcspath.join(tgt_base, tgt_path)
685 src_full_path = vcspath.join(src_base, src_path)
685 src_full_path = vcspath.join(src_base, src_path)
686
686
687 self.binary_content = False
687 self.binary_content = False
688 mime_type = self._get_mime_type(tgt_full_path)
688 mime_type = self._get_mime_type(tgt_full_path)
689
689
690 if mime_type and not mime_type.startswith('text'):
690 if mime_type and not mime_type.startswith('text'):
691 self.binary_content = True
691 self.binary_content = True
692 buf.write("=" * 67 + '\n')
692 buf.write("=" * 67 + '\n')
693 buf.write("Cannot display: file marked as a binary type.\n")
693 buf.write("Cannot display: file marked as a binary type.\n")
694 buf.write("svn:mime-type = %s\n" % mime_type)
694 buf.write("svn:mime-type = %s\n" % mime_type)
695 buf.write("Index: %s\n" % (tgt_path, ))
695 buf.write("Index: %s\n" % (tgt_path, ))
696 buf.write("=" * 67 + '\n')
696 buf.write("=" * 67 + '\n')
697 buf.write("diff --git a/%(tgt_path)s b/%(tgt_path)s\n" % {
697 buf.write("diff --git a/%(tgt_path)s b/%(tgt_path)s\n" % {
698 'tgt_path': tgt_path})
698 'tgt_path': tgt_path})
699
699
700 if change == 'add':
700 if change == 'add':
701 # TODO: johbo: SVN is missing a zero here compared to git
701 # TODO: johbo: SVN is missing a zero here compared to git
702 buf.write("new file mode 10644\n")
702 buf.write("new file mode 10644\n")
703
703
704 #TODO(marcink): intro to binary detection of svn patches
704 #TODO(marcink): intro to binary detection of svn patches
705 # if self.binary_content:
705 # if self.binary_content:
706 # buf.write('GIT binary patch\n')
706 # buf.write('GIT binary patch\n')
707
707
708 buf.write("--- /dev/null\t(revision 0)\n")
708 buf.write("--- /dev/null\t(revision 0)\n")
709 src_lines = []
709 src_lines = []
710 else:
710 else:
711 if change == 'delete':
711 if change == 'delete':
712 buf.write("deleted file mode 10644\n")
712 buf.write("deleted file mode 10644\n")
713
713
714 #TODO(marcink): intro to binary detection of svn patches
714 #TODO(marcink): intro to binary detection of svn patches
715 # if self.binary_content:
715 # if self.binary_content:
716 # buf.write('GIT binary patch\n')
716 # buf.write('GIT binary patch\n')
717
717
718 buf.write("--- a/%s\t(revision %s)\n" % (
718 buf.write("--- a/%s\t(revision %s)\n" % (
719 src_path, self.src_rev))
719 src_path, self.src_rev))
720 src_lines = self._svn_readlines(self.src_root, src_full_path)
720 src_lines = self._svn_readlines(self.src_root, src_full_path)
721
721
722 if change == 'delete':
722 if change == 'delete':
723 buf.write("+++ /dev/null\t(revision %s)\n" % (self.tgt_rev, ))
723 buf.write("+++ /dev/null\t(revision %s)\n" % (self.tgt_rev, ))
724 tgt_lines = []
724 tgt_lines = []
725 else:
725 else:
726 buf.write("+++ b/%s\t(revision %s)\n" % (
726 buf.write("+++ b/%s\t(revision %s)\n" % (
727 tgt_path, self.tgt_rev))
727 tgt_path, self.tgt_rev))
728 tgt_lines = self._svn_readlines(self.tgt_root, tgt_full_path)
728 tgt_lines = self._svn_readlines(self.tgt_root, tgt_full_path)
729
729
730 if not self.binary_content:
730 if not self.binary_content:
731 udiff = svn_diff.unified_diff(
731 udiff = svn_diff.unified_diff(
732 src_lines, tgt_lines, context=self.context,
732 src_lines, tgt_lines, context=self.context,
733 ignore_blank_lines=self.ignore_whitespace,
733 ignore_blank_lines=self.ignore_whitespace,
734 ignore_case=False,
734 ignore_case=False,
735 ignore_space_changes=self.ignore_whitespace)
735 ignore_space_changes=self.ignore_whitespace)
736 buf.writelines(udiff)
736 buf.writelines(udiff)
737
737
738 def _get_mime_type(self, path):
738 def _get_mime_type(self, path):
739 try:
739 try:
740 mime_type = svn.fs.node_prop(
740 mime_type = svn.fs.node_prop(
741 self.tgt_root, path, svn.core.SVN_PROP_MIME_TYPE)
741 self.tgt_root, path, svn.core.SVN_PROP_MIME_TYPE)
742 except svn.core.SubversionException:
742 except svn.core.SubversionException:
743 mime_type = svn.fs.node_prop(
743 mime_type = svn.fs.node_prop(
744 self.src_root, path, svn.core.SVN_PROP_MIME_TYPE)
744 self.src_root, path, svn.core.SVN_PROP_MIME_TYPE)
745 return mime_type
745 return mime_type
746
746
747 def _svn_readlines(self, fs_root, node_path):
747 def _svn_readlines(self, fs_root, node_path):
748 if self.binary_content:
748 if self.binary_content:
749 return []
749 return []
750 node_kind = svn.fs.check_path(fs_root, node_path)
750 node_kind = svn.fs.check_path(fs_root, node_path)
751 if node_kind not in (
751 if node_kind not in (
752 svn.core.svn_node_file, svn.core.svn_node_symlink):
752 svn.core.svn_node_file, svn.core.svn_node_symlink):
753 return []
753 return []
754 content = svn.core.Stream(svn.fs.file_contents(fs_root, node_path)).read()
754 content = svn.core.Stream(svn.fs.file_contents(fs_root, node_path)).read()
755 return content.splitlines(True)
755 return content.splitlines(True)
756
756
757
757
758 class DiffChangeEditor(svn.delta.Editor):
758 class DiffChangeEditor(svn.delta.Editor):
759 """
759 """
760 Records changes between two given revisions
760 Records changes between two given revisions
761 """
761 """
762
762
763 def __init__(self):
763 def __init__(self):
764 self.changes = []
764 self.changes = []
765
765
766 def delete_entry(self, path, revision, parent_baton, pool=None):
766 def delete_entry(self, path, revision, parent_baton, pool=None):
767 self.changes.append((path, None, 'delete'))
767 self.changes.append((path, None, 'delete'))
768
768
769 def add_file(
769 def add_file(
770 self, path, parent_baton, copyfrom_path, copyfrom_revision,
770 self, path, parent_baton, copyfrom_path, copyfrom_revision,
771 file_pool=None):
771 file_pool=None):
772 self.changes.append((path, 'file', 'add'))
772 self.changes.append((path, 'file', 'add'))
773
773
774 def open_file(self, path, parent_baton, base_revision, file_pool=None):
774 def open_file(self, path, parent_baton, base_revision, file_pool=None):
775 self.changes.append((path, 'file', 'change'))
775 self.changes.append((path, 'file', 'change'))
776
776
777
777
778 def authorization_callback_allow_all(root, path, pool):
778 def authorization_callback_allow_all(root, path, pool):
779 return True
779 return True
780
780
781
781
782 class TxnNodeProcessor(object):
782 class TxnNodeProcessor(object):
783 """
783 """
784 Utility to process the change of one node within a transaction root.
784 Utility to process the change of one node within a transaction root.
785
785
786 It encapsulates the knowledge of how to add, update or remove
786 It encapsulates the knowledge of how to add, update or remove
787 a node for a given transaction root. The purpose is to support the method
787 a node for a given transaction root. The purpose is to support the method
788 `SvnRemote.commit`.
788 `SvnRemote.commit`.
789 """
789 """
790
790
791 def __init__(self, node, txn_root):
791 def __init__(self, node, txn_root):
792 assert isinstance(node['path'], str)
792 assert isinstance(node['path'], str)
793
793
794 self.node = node
794 self.node = node
795 self.txn_root = txn_root
795 self.txn_root = txn_root
796
796
797 def update(self):
797 def update(self):
798 self._ensure_parent_dirs()
798 self._ensure_parent_dirs()
799 self._add_file_if_node_does_not_exist()
799 self._add_file_if_node_does_not_exist()
800 self._update_file_content()
800 self._update_file_content()
801 self._update_file_properties()
801 self._update_file_properties()
802
802
803 def remove(self):
803 def remove(self):
804 svn.fs.delete(self.txn_root, self.node['path'])
804 svn.fs.delete(self.txn_root, self.node['path'])
805 # TODO: Clean up directory if empty
805 # TODO: Clean up directory if empty
806
806
807 def _ensure_parent_dirs(self):
807 def _ensure_parent_dirs(self):
808 curdir = vcspath.dirname(self.node['path'])
808 curdir = vcspath.dirname(self.node['path'])
809 dirs_to_create = []
809 dirs_to_create = []
810 while not self._svn_path_exists(curdir):
810 while not self._svn_path_exists(curdir):
811 dirs_to_create.append(curdir)
811 dirs_to_create.append(curdir)
812 curdir = vcspath.dirname(curdir)
812 curdir = vcspath.dirname(curdir)
813
813
814 for curdir in reversed(dirs_to_create):
814 for curdir in reversed(dirs_to_create):
815 log.debug('Creating missing directory "%s"', curdir)
815 log.debug('Creating missing directory "%s"', curdir)
816 svn.fs.make_dir(self.txn_root, curdir)
816 svn.fs.make_dir(self.txn_root, curdir)
817
817
818 def _svn_path_exists(self, path):
818 def _svn_path_exists(self, path):
819 path_status = svn.fs.check_path(self.txn_root, path)
819 path_status = svn.fs.check_path(self.txn_root, path)
820 return path_status != svn.core.svn_node_none
820 return path_status != svn.core.svn_node_none
821
821
822 def _add_file_if_node_does_not_exist(self):
822 def _add_file_if_node_does_not_exist(self):
823 kind = svn.fs.check_path(self.txn_root, self.node['path'])
823 kind = svn.fs.check_path(self.txn_root, self.node['path'])
824 if kind == svn.core.svn_node_none:
824 if kind == svn.core.svn_node_none:
825 svn.fs.make_file(self.txn_root, self.node['path'])
825 svn.fs.make_file(self.txn_root, self.node['path'])
826
826
827 def _update_file_content(self):
827 def _update_file_content(self):
828 assert isinstance(self.node['content'], str)
828 assert isinstance(self.node['content'], str)
829 handler, baton = svn.fs.apply_textdelta(
829 handler, baton = svn.fs.apply_textdelta(
830 self.txn_root, self.node['path'], None, None)
830 self.txn_root, self.node['path'], None, None)
831 svn.delta.svn_txdelta_send_string(self.node['content'], handler, baton)
831 svn.delta.svn_txdelta_send_string(self.node['content'], handler, baton)
832
832
833 def _update_file_properties(self):
833 def _update_file_properties(self):
834 properties = self.node.get('properties', {})
834 properties = self.node.get('properties', {})
835 for key, value in properties.iteritems():
835 for key, value in properties.iteritems():
836 svn.fs.change_node_prop(
836 svn.fs.change_node_prop(
837 self.txn_root, self.node['path'], key, value)
837 self.txn_root, self.node['path'], key, value)
838
838
839
839
840 def apr_time_t(timestamp):
840 def apr_time_t(timestamp):
841 """
841 """
842 Convert a Python timestamp into APR timestamp type apr_time_t
842 Convert a Python timestamp into APR timestamp type apr_time_t
843 """
843 """
844 return timestamp * 1E6
844 return timestamp * 1E6
845
845
846
846
847 def svn_opt_revision_value_t(num):
847 def svn_opt_revision_value_t(num):
848 """
848 """
849 Put `num` into a `svn_opt_revision_value_t` structure.
849 Put `num` into a `svn_opt_revision_value_t` structure.
850 """
850 """
851 value = svn.core.svn_opt_revision_value_t()
851 value = svn.core.svn_opt_revision_value_t()
852 value.number = num
852 value.number = num
853 revision = svn.core.svn_opt_revision_t()
853 revision = svn.core.svn_opt_revision_t()
854 revision.kind = svn.core.svn_opt_revision_number
854 revision.kind = svn.core.svn_opt_revision_number
855 revision.value = value
855 revision.value = value
856 return revision
856 return revision
General Comments 0
You need to be logged in to leave comments. Login now