##// END OF EJS Templates
svn-backend: add handling of rename file as changes....
marcink -
r104:76a3f0e8 default
parent child Browse files
Show More
@@ -1,625 +1,627 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 if change.item_kind == svn.core.svn_node_dir:
201 202 continue
202 if change.action == svn.repos.CHANGE_ACTION_ADD:
203 if change.action in [svn.repos.CHANGE_ACTION_ADD]:
203 204 added.append(path)
204 elif change.action == svn.repos.CHANGE_ACTION_MODIFY:
205 elif change.action in [svn.repos.CHANGE_ACTION_MODIFY,
206 svn.repos.CHANGE_ACTION_REPLACE]:
205 207 changed.append(path)
206 elif change.action == svn.repos.CHANGE_ACTION_DELETE:
208 elif change.action in [svn.repos.CHANGE_ACTION_DELETE]:
207 209 removed.append(path)
208 210 else:
209 211 raise NotImplementedError(
210 212 "Action %s not supported on path %s" % (
211 213 change.action, path))
212 214
213 215 changes = {
214 216 'added': added,
215 217 'changed': changed,
216 218 'removed': removed,
217 219 }
218 220 return changes
219 221
220 222 def node_history(self, wire, path, revision, limit):
221 223 cross_copies = False
222 224 repo = self._factory.repo(wire)
223 225 fsobj = svn.repos.fs(repo)
224 226 rev_root = svn.fs.revision_root(fsobj, revision)
225 227
226 228 history_revisions = []
227 229 history = svn.fs.node_history(rev_root, path)
228 230 history = svn.fs.history_prev(history, cross_copies)
229 231 while history:
230 232 __, node_revision = svn.fs.history_location(history)
231 233 history_revisions.append(node_revision)
232 234 if limit and len(history_revisions) >= limit:
233 235 break
234 236 history = svn.fs.history_prev(history, cross_copies)
235 237 return history_revisions
236 238
237 239 def node_properties(self, wire, path, revision):
238 240 repo = self._factory.repo(wire)
239 241 fsobj = svn.repos.fs(repo)
240 242 rev_root = svn.fs.revision_root(fsobj, revision)
241 243 return svn.fs.node_proplist(rev_root, path)
242 244
243 245 def file_annotate(self, wire, path, revision):
244 246 abs_path = 'file://' + urllib.pathname2url(
245 247 vcspath.join(wire['path'], path))
246 248 file_uri = svn.core.svn_path_canonicalize(abs_path)
247 249
248 250 start_rev = svn_opt_revision_value_t(0)
249 251 peg_rev = svn_opt_revision_value_t(revision)
250 252 end_rev = peg_rev
251 253
252 254 annotations = []
253 255
254 256 def receiver(line_no, revision, author, date, line, pool):
255 257 annotations.append((line_no, revision, line))
256 258
257 259 # TODO: Cannot use blame5, missing typemap function in the swig code
258 260 try:
259 261 svn.client.blame2(
260 262 file_uri, peg_rev, start_rev, end_rev,
261 263 receiver, svn.client.create_context())
262 264 except svn.core.SubversionException as exc:
263 265 log.exception("Error during blame operation.")
264 266 raise Exception(
265 267 "Blame not supported or file does not exist at path %s. "
266 268 "Error %s." % (path, exc))
267 269
268 270 return annotations
269 271
270 272 def get_node_type(self, wire, path, rev=None):
271 273 repo = self._factory.repo(wire)
272 274 fs_ptr = svn.repos.fs(repo)
273 275 if rev is None:
274 276 rev = svn.fs.youngest_rev(fs_ptr)
275 277 root = svn.fs.revision_root(fs_ptr, rev)
276 278 node = svn.fs.check_path(root, path)
277 279 return NODE_TYPE_MAPPING.get(node, None)
278 280
279 281 def get_nodes(self, wire, path, revision=None):
280 282 repo = self._factory.repo(wire)
281 283 fsobj = svn.repos.fs(repo)
282 284 if revision is None:
283 285 revision = svn.fs.youngest_rev(fsobj)
284 286 root = svn.fs.revision_root(fsobj, revision)
285 287 entries = svn.fs.dir_entries(root, path)
286 288 result = []
287 289 for entry_path, entry_info in entries.iteritems():
288 290 result.append(
289 291 (entry_path, NODE_TYPE_MAPPING.get(entry_info.kind, None)))
290 292 return result
291 293
292 294 def get_file_content(self, wire, path, rev=None):
293 295 repo = self._factory.repo(wire)
294 296 fsobj = svn.repos.fs(repo)
295 297 if rev is None:
296 298 rev = svn.fs.youngest_revision(fsobj)
297 299 root = svn.fs.revision_root(fsobj, rev)
298 300 content = svn.core.Stream(svn.fs.file_contents(root, path))
299 301 return content.read()
300 302
301 303 def get_file_size(self, wire, path, revision=None):
302 304 repo = self._factory.repo(wire)
303 305 fsobj = svn.repos.fs(repo)
304 306 if revision is None:
305 307 revision = svn.fs.youngest_revision(fsobj)
306 308 root = svn.fs.revision_root(fsobj, revision)
307 309 size = svn.fs.file_length(root, path)
308 310 return size
309 311
310 312 def create_repository(self, wire, compatible_version=None):
311 313 log.info('Creating Subversion repository in path "%s"', wire['path'])
312 314 self._factory.repo(wire, create=True,
313 315 compatible_version=compatible_version)
314 316
315 317 def import_remote_repository(self, wire, src_url):
316 318 repo_path = wire['path']
317 319 if not self.is_path_valid_repository(wire, repo_path):
318 320 raise Exception(
319 321 "Path %s is not a valid Subversion repository." % repo_path)
320 322 # TODO: johbo: URL checks ?
321 323 rdump = subprocess.Popen(
322 324 ['svnrdump', 'dump', '--non-interactive', src_url],
323 325 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
324 326 load = subprocess.Popen(
325 327 ['svnadmin', 'load', repo_path], stdin=rdump.stdout)
326 328
327 329 # TODO: johbo: This can be a very long operation, might be better
328 330 # to track some kind of status and provide an api to check if the
329 331 # import is done.
330 332 rdump.wait()
331 333 load.wait()
332 334
333 335 if rdump.returncode != 0:
334 336 errors = rdump.stderr.read()
335 337 log.error('svnrdump dump failed: statuscode %s: message: %s',
336 338 rdump.returncode, errors)
337 339 reason = 'UNKNOWN'
338 340 if 'svnrdump: E230001:' in errors:
339 341 reason = 'INVALID_CERTIFICATE'
340 342 raise Exception(
341 343 'Failed to dump the remote repository from %s.' % src_url,
342 344 reason)
343 345 if load.returncode != 0:
344 346 raise Exception(
345 347 'Failed to load the dump of remote repository from %s.' %
346 348 (src_url, ))
347 349
348 350 def commit(self, wire, message, author, timestamp, updated, removed):
349 351 assert isinstance(message, str)
350 352 assert isinstance(author, str)
351 353
352 354 repo = self._factory.repo(wire)
353 355 fsobj = svn.repos.fs(repo)
354 356
355 357 rev = svn.fs.youngest_rev(fsobj)
356 358 txn = svn.repos.fs_begin_txn_for_commit(repo, rev, author, message)
357 359 txn_root = svn.fs.txn_root(txn)
358 360
359 361 for node in updated:
360 362 TxnNodeProcessor(node, txn_root).update()
361 363 for node in removed:
362 364 TxnNodeProcessor(node, txn_root).remove()
363 365
364 366 commit_id = svn.repos.fs_commit_txn(repo, txn)
365 367
366 368 if timestamp:
367 369 apr_time = apr_time_t(timestamp)
368 370 ts_formatted = svn.core.svn_time_to_cstring(apr_time)
369 371 svn.fs.change_rev_prop(fsobj, commit_id, 'svn:date', ts_formatted)
370 372
371 373 log.debug('Committed revision "%s" to "%s".', commit_id, wire['path'])
372 374 return commit_id
373 375
374 376 def diff(self, wire, rev1, rev2, path1=None, path2=None,
375 377 ignore_whitespace=False, context=3):
376 378 wire.update(cache=False)
377 379 repo = self._factory.repo(wire)
378 380 diff_creator = SvnDiffer(
379 381 repo, rev1, path1, rev2, path2, ignore_whitespace, context)
380 382 return diff_creator.generate_diff()
381 383
382 384
383 385 class SvnDiffer(object):
384 386 """
385 387 Utility to create diffs based on difflib and the Subversion api
386 388 """
387 389
388 390 binary_content = False
389 391
390 392 def __init__(
391 393 self, repo, src_rev, src_path, tgt_rev, tgt_path,
392 394 ignore_whitespace, context):
393 395 self.repo = repo
394 396 self.ignore_whitespace = ignore_whitespace
395 397 self.context = context
396 398
397 399 fsobj = svn.repos.fs(repo)
398 400
399 401 self.tgt_rev = tgt_rev
400 402 self.tgt_path = tgt_path or ''
401 403 self.tgt_root = svn.fs.revision_root(fsobj, tgt_rev)
402 404 self.tgt_kind = svn.fs.check_path(self.tgt_root, self.tgt_path)
403 405
404 406 self.src_rev = src_rev
405 407 self.src_path = src_path or self.tgt_path
406 408 self.src_root = svn.fs.revision_root(fsobj, src_rev)
407 409 self.src_kind = svn.fs.check_path(self.src_root, self.src_path)
408 410
409 411 self._validate()
410 412
411 413 def _validate(self):
412 414 if (self.tgt_kind != svn.core.svn_node_none and
413 415 self.src_kind != svn.core.svn_node_none and
414 416 self.src_kind != self.tgt_kind):
415 417 # TODO: johbo: proper error handling
416 418 raise Exception(
417 419 "Source and target are not compatible for diff generation. "
418 420 "Source type: %s, target type: %s" %
419 421 (self.src_kind, self.tgt_kind))
420 422
421 423 def generate_diff(self):
422 424 buf = StringIO.StringIO()
423 425 if self.tgt_kind == svn.core.svn_node_dir:
424 426 self._generate_dir_diff(buf)
425 427 else:
426 428 self._generate_file_diff(buf)
427 429 return buf.getvalue()
428 430
429 431 def _generate_dir_diff(self, buf):
430 432 editor = DiffChangeEditor()
431 433 editor_ptr, editor_baton = svn.delta.make_editor(editor)
432 434 svn.repos.dir_delta2(
433 435 self.src_root,
434 436 self.src_path,
435 437 '', # src_entry
436 438 self.tgt_root,
437 439 self.tgt_path,
438 440 editor_ptr, editor_baton,
439 441 authorization_callback_allow_all,
440 442 False, # text_deltas
441 443 svn.core.svn_depth_infinity, # depth
442 444 False, # entry_props
443 445 False, # ignore_ancestry
444 446 )
445 447
446 448 for path, __, change in sorted(editor.changes):
447 449 self._generate_node_diff(
448 450 buf, change, path, self.tgt_path, path, self.src_path)
449 451
450 452 def _generate_file_diff(self, buf):
451 453 change = None
452 454 if self.src_kind == svn.core.svn_node_none:
453 455 change = "add"
454 456 elif self.tgt_kind == svn.core.svn_node_none:
455 457 change = "delete"
456 458 tgt_base, tgt_path = vcspath.split(self.tgt_path)
457 459 src_base, src_path = vcspath.split(self.src_path)
458 460 self._generate_node_diff(
459 461 buf, change, tgt_path, tgt_base, src_path, src_base)
460 462
461 463 def _generate_node_diff(
462 464 self, buf, change, tgt_path, tgt_base, src_path, src_base):
463 465 tgt_full_path = vcspath.join(tgt_base, tgt_path)
464 466 src_full_path = vcspath.join(src_base, src_path)
465 467
466 468 self.binary_content = False
467 469 mime_type = self._get_mime_type(tgt_full_path)
468 470 if mime_type and not mime_type.startswith('text'):
469 471 self.binary_content = True
470 472 buf.write("=" * 67 + '\n')
471 473 buf.write("Cannot display: file marked as a binary type.\n")
472 474 buf.write("svn:mime-type = %s\n" % mime_type)
473 475 buf.write("Index: %s\n" % (tgt_path, ))
474 476 buf.write("=" * 67 + '\n')
475 477 buf.write("diff --git a/%(tgt_path)s b/%(tgt_path)s\n" % {
476 478 'tgt_path': tgt_path})
477 479
478 480 if change == 'add':
479 481 # TODO: johbo: SVN is missing a zero here compared to git
480 482 buf.write("new file mode 10644\n")
481 483 buf.write("--- /dev/null\t(revision 0)\n")
482 484 src_lines = []
483 485 else:
484 486 if change == 'delete':
485 487 buf.write("deleted file mode 10644\n")
486 488 buf.write("--- a/%s\t(revision %s)\n" % (
487 489 src_path, self.src_rev))
488 490 src_lines = self._svn_readlines(self.src_root, src_full_path)
489 491
490 492 if change == 'delete':
491 493 buf.write("+++ /dev/null\t(revision %s)\n" % (self.tgt_rev, ))
492 494 tgt_lines = []
493 495 else:
494 496 buf.write("+++ b/%s\t(revision %s)\n" % (
495 497 tgt_path, self.tgt_rev))
496 498 tgt_lines = self._svn_readlines(self.tgt_root, tgt_full_path)
497 499
498 500 if not self.binary_content:
499 501 udiff = svn_diff.unified_diff(
500 502 src_lines, tgt_lines, context=self.context,
501 503 ignore_blank_lines=self.ignore_whitespace,
502 504 ignore_case=False,
503 505 ignore_space_changes=self.ignore_whitespace)
504 506 buf.writelines(udiff)
505 507
506 508 def _get_mime_type(self, path):
507 509 try:
508 510 mime_type = svn.fs.node_prop(
509 511 self.tgt_root, path, svn.core.SVN_PROP_MIME_TYPE)
510 512 except svn.core.SubversionException:
511 513 mime_type = svn.fs.node_prop(
512 514 self.src_root, path, svn.core.SVN_PROP_MIME_TYPE)
513 515 return mime_type
514 516
515 517 def _svn_readlines(self, fs_root, node_path):
516 518 if self.binary_content:
517 519 return []
518 520 node_kind = svn.fs.check_path(fs_root, node_path)
519 521 if node_kind not in (
520 522 svn.core.svn_node_file, svn.core.svn_node_symlink):
521 523 return []
522 524 content = svn.core.Stream(
523 525 svn.fs.file_contents(fs_root, node_path)).read()
524 526 return content.splitlines(True)
525 527
526 528
527 529 class DiffChangeEditor(svn.delta.Editor):
528 530 """
529 531 Records changes between two given revisions
530 532 """
531 533
532 534 def __init__(self):
533 535 self.changes = []
534 536
535 537 def delete_entry(self, path, revision, parent_baton, pool=None):
536 538 self.changes.append((path, None, 'delete'))
537 539
538 540 def add_file(
539 541 self, path, parent_baton, copyfrom_path, copyfrom_revision,
540 542 file_pool=None):
541 543 self.changes.append((path, 'file', 'add'))
542 544
543 545 def open_file(self, path, parent_baton, base_revision, file_pool=None):
544 546 self.changes.append((path, 'file', 'change'))
545 547
546 548
547 549 def authorization_callback_allow_all(root, path, pool):
548 550 return True
549 551
550 552
551 553 class TxnNodeProcessor(object):
552 554 """
553 555 Utility to process the change of one node within a transaction root.
554 556
555 557 It encapsulates the knowledge of how to add, update or remove
556 558 a node for a given transaction root. The purpose is to support the method
557 559 `SvnRemote.commit`.
558 560 """
559 561
560 562 def __init__(self, node, txn_root):
561 563 assert isinstance(node['path'], str)
562 564
563 565 self.node = node
564 566 self.txn_root = txn_root
565 567
566 568 def update(self):
567 569 self._ensure_parent_dirs()
568 570 self._add_file_if_node_does_not_exist()
569 571 self._update_file_content()
570 572 self._update_file_properties()
571 573
572 574 def remove(self):
573 575 svn.fs.delete(self.txn_root, self.node['path'])
574 576 # TODO: Clean up directory if empty
575 577
576 578 def _ensure_parent_dirs(self):
577 579 curdir = vcspath.dirname(self.node['path'])
578 580 dirs_to_create = []
579 581 while not self._svn_path_exists(curdir):
580 582 dirs_to_create.append(curdir)
581 583 curdir = vcspath.dirname(curdir)
582 584
583 585 for curdir in reversed(dirs_to_create):
584 586 log.debug('Creating missing directory "%s"', curdir)
585 587 svn.fs.make_dir(self.txn_root, curdir)
586 588
587 589 def _svn_path_exists(self, path):
588 590 path_status = svn.fs.check_path(self.txn_root, path)
589 591 return path_status != svn.core.svn_node_none
590 592
591 593 def _add_file_if_node_does_not_exist(self):
592 594 kind = svn.fs.check_path(self.txn_root, self.node['path'])
593 595 if kind == svn.core.svn_node_none:
594 596 svn.fs.make_file(self.txn_root, self.node['path'])
595 597
596 598 def _update_file_content(self):
597 599 assert isinstance(self.node['content'], str)
598 600 handler, baton = svn.fs.apply_textdelta(
599 601 self.txn_root, self.node['path'], None, None)
600 602 svn.delta.svn_txdelta_send_string(self.node['content'], handler, baton)
601 603
602 604 def _update_file_properties(self):
603 605 properties = self.node.get('properties', {})
604 606 for key, value in properties.iteritems():
605 607 svn.fs.change_node_prop(
606 608 self.txn_root, self.node['path'], key, value)
607 609
608 610
609 611 def apr_time_t(timestamp):
610 612 """
611 613 Convert a Python timestamp into APR timestamp type apr_time_t
612 614 """
613 615 return timestamp * 1E6
614 616
615 617
616 618 def svn_opt_revision_value_t(num):
617 619 """
618 620 Put `num` into a `svn_opt_revision_value_t` structure.
619 621 """
620 622 value = svn.core.svn_opt_revision_value_t()
621 623 value.number = num
622 624 revision = svn.core.svn_opt_revision_t()
623 625 revision.kind = svn.core.svn_opt_revision_number
624 626 revision.value = value
625 627 return revision
General Comments 0
You need to be logged in to leave comments. Login now