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