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