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