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