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