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