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