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