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