1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 """
18 Access to the subversion respository
19
20 @var version: The version of the subversion library (maj, min, pat, rev)
21 @type version: C{_Version}
22
23 @var _SVN_INVALID_REVNUM: the invalid revision number
24 @type _SVN_INVALID_REVNUM: C{int}
25 """
26 __author__ = "André Malo"
27 __docformat__ = "epytext en"
28 __all__ = [
29 'version', 'Repository', 'Error', 'RepositoryError', 'isUnicodeProperty'
30 'VersionedPathDescriptor', 'LockedPathDescriptor',
31 ]
32
33
34
35 import os
36 from svn import core as svn_core
37 from svn import repos as svn_repos
38 from svn import fs as svn_fs
39 from svn import delta as svn_delta
40
41 _SVN_INVALID_REVNUM = svn_core.SWIG_SVN_INVALID_REVNUM
42
43
44 Error = svn_core.SubversionException
45
46
48 """ SVN version container class
49
50 @ivar major: Major version
51 @type major: C{int}
52
53 @ivar minor: Minor version
54 @type minor: C{int}
55
56 @ivar patch: Patch level
57 @type patch: C{int}
58
59 @ivar revision: Revision number
60 @type revision: C{int}
61
62 @ivar tag: Additional tag
63 @type tag: C{str}
64
65 @ivar min_1_2: SVN >= 1.2?
66 @type min_1_2: C{bool}
67 """
68
70 """ Initialization """
71 self.major = svn_core.SVN_VER_MAJOR
72 self.minor = svn_core.SVN_VER_MINOR
73 self.revision = svn_core.SVN_VER_REVISION
74 self.tag = svn_core.SVN_VER_TAG
75 try:
76 self.patch = svn_core.SVN_VER_PATCH
77 except AttributeError:
78 self.patch = svn_core.SVN_VER_MICRO
79
80 self.min_1_2 = bool(
81 self.major > 1 or (self.major == 1 and self.minor >= 2)
82 )
83 self.min_1_7 = bool(
84 self.major > 1 or (self.major == 1 and self.minor >= 7)
85 )
86
87 version = _Version()
88
89
91 """ Returns if the supplied name represents a translated property
92
93 @param name: The property name
94 @type name: C{str}
95
96 @return: The decision
97 @rtype: C{bool}
98 """
99 return bool(svn_core.svn_prop_needs_translation(name))
100
101
103 """ Returns True if the supplied mime type represents a binary
104
105 @param mtype: The mime type
106 @type mtype: C{str}
107
108 @return: The decision
109 @rtype: C{bool}
110 """
111 return bool(mtype and
112 svn_core.svn_mime_type_is_binary(mtype)
113 )
114
115
117 """ A repository error occured
118
119 @ivar svn_err_code: The SVN error code
120 @type svn_err_code: C{int}
121
122 @ivar svn_err_name: The name of the SVN error (if it could be mapped)
123 @type svn_err_name: C{str}
124
125 @ivar svn_err_str: The SVN error description
126 @type svn_err_str: C{str}
127 """
128
130 """ Initialization
131
132 @param sexc: C{svnmailer.subversion.Error}
133 @type sexc: C{svnmailer.subversion.Error}
134 """
135 Exception.__init__(self)
136 self.svn_err_str, self.svn_err_code = sexc.args
137 self.svn_err_name = dict([(getattr(svn_core, var), var)
138 for var in vars(svn_core) if var.startswith('SVN_ERR')
139 ]).get(self.svn_err_code, 'unknown APR code')
140
141
143 """ Human readable representation
144
145 @return: The string representation
146 @rtype: C{str}
147 """
148 return str((self.svn_err_code, self.svn_err_name, self.svn_err_str))
149
150
151
153 """ Access to the subversion repository
154
155 @cvar _pool: C{None}
156 @ivar _pool: main APR pool
157 @type _pool: swig object
158
159 @cvar _apr_initialized: C{False}
160 @ivar _apr_initialized: is APR initialized?
161 @type _apr_initialized: C{bool}
162
163 @ivar _repos: Reference to the open repository
164 @type _repos: swig object
165
166 @ivar _fs: Reference to the repos filesystem
167 @type _fs: swig object
168
169 @ivar _revRoots: Cached revision root objects
170 @type _revRoots: C{dict}
171
172 @ivar _revChanges: Cached revision change lists
173 @type _revChanges: C{dict}
174
175 @ivar _revProps: Cached revision properties
176 @type _revProps: C{dict}
177
178 @ivar _revTimes: Cached revision times
179 @type _revTimes: C{dict}
180
181 @ivar _pathProps: Cached path properties
182 @type _pathProps: C{dict}
183
184 @ivar _pathPropLists: Cached path propery lists
185 @type _pathPropLists: C{dict}
186
187 @ivar path: The path to the repository
188 @type path: C{unicode}
189 """
190 _pool = None
191 _apr_initialized = False
192
193
195 """ Open the repository
196
197 @param repos_path: The repository path as unicode
198 @type repos_path: C{unicode}
199 """
200
201 svn_core.apr_initialize()
202 self._apr_initialized = True
203
204
205 self._pool = svn_core.svn_pool_create(None)
206
207
208 repos_path = repos_path.encode("utf-8", "strict")
209 repos_path = os.path.normpath(os.path.abspath(repos_path))
210
211
212 self._repos = svn_repos.svn_repos_open(repos_path, self._pool)
213 self._fs = svn_repos.svn_repos_fs(self._repos)
214
215
216 self._revRoots = {}
217 self._revChanges = {}
218 self._revProps = {}
219 self._revTimes = {}
220 self._pathProps = {}
221 self._pathPropLists = {}
222 self.path = repos_path
223
224
226 """ Destroy the pool and release the shared lock """
227 try:
228 if self._pool:
229 pool = self._pool
230 self._pool = None
231 svn_core.svn_pool_destroy(pool)
232 finally:
233 if self._apr_initialized:
234 self._apr_initialized = False
235 svn_core.apr_terminate()
236
237
239 """ Return the list of changes of a revisions sorted by path
240
241 @param revision: The revision
242 @type revision: C{int}
243
244 @return: The Changes list
245 @rtype: C{list}
246 """
247 try:
248 changelist = self._revChanges[revision]
249 except KeyError:
250 editor = self._getChangeCollector(revision)
251 e_pool = editor.getPool()
252 e_ptr, e_baton = svn_delta.make_editor(editor, e_pool)
253 svn_repos.svn_repos_replay(
254 self._getRevisionRoot(revision), e_ptr, e_baton, e_pool
255 )
256
257 e_changes = (version.min_1_2 and
258 [editor.get_changes()] or [editor.changes])[0]
259 changelist = [VersionedPathDescriptor(self, path, revision, change)
260 for path, change in e_changes.items()
261 ]
262 changelist.sort()
263
264
265 self._revChanges[revision] = changelist
266
267 del editor
268
269 return changelist
270
271
273 """ Get a dict of properties for a particular path/revision
274
275 @param path: The path
276 @type path: C{str}
277
278 @param revision: The revision number
279 @type revision: C{int}
280
281 @return: The dict of properties
282 @rtype: C{dict}
283 """
284 try:
285 plist = self._pathPropLists[(path, revision)]
286 except KeyError:
287 rev_root = self._getRevisionRoot(revision)
288 plist = self._pathPropLists[(path, revision)] = dict([
289 (key, str(value)) for key, value in
290 svn_fs.node_proplist(rev_root, path, self._pool).items()
291 ])
292
293 return plist
294
295
297 """ Get the value of a particular property
298
299 @param name: The name of the property
300 @type name: C{str}
301
302 @param path: The path the property is attached to
303 @type path: C{str}
304
305 @param revision: The revision number
306 @type revision: C{int}
307
308 @return: The property value or C{None} if the property
309 doesn't exist.
310 @rtype: C{str}
311 """
312 try:
313 value = self._pathProps[(name, path, revision)]
314 except KeyError:
315 root = self._getRevisionRoot(revision)
316 value = self._pathProps[(name, path, revision)] = svn_fs.node_prop(
317 root, path, name, self._pool
318 )
319
320 return value
321
322
324 """ Get the MIME type of a particular path
325
326 @param path: The path
327 @type path: C{str}
328
329 @param revision: The revision number
330 @type revision: C{int}
331
332 @return: The mime type or C{None}
333 @rtype: C{str}
334 """
335 return self.getPathProperty(
336 svn_core.SVN_PROP_MIME_TYPE, path, revision
337 )
338
339
340 - def dumpPathContent(self, fp, path, revision):
341 """ Dump the contents of a particular path into a file
342
343 @param fp: The file descriptor
344 @type fp: file like object
345
346 @param path: The path to process
347 @type path: C{str}
348
349 @param revision: The revision number
350 @type revision: C{int}
351 """
352 pool = svn_core.svn_pool_create(self._pool)
353
354 try:
355 root = self._getRevisionRoot(revision)
356 stream = svn_fs.file_contents(root, path, pool)
357
358 try:
359 while True:
360 chunk = svn_core.svn_stream_read(
361 stream, svn_core.SVN_STREAM_CHUNK_SIZE
362 )
363 if not chunk:
364 break
365
366 fp.write(chunk)
367 finally:
368 svn_core.svn_stream_close(stream)
369 finally:
370 svn_core.svn_pool_destroy(pool)
371
372
374 """ Returns the time of a particular rev. in seconds since epoch
375
376 @param revision: The revision number
377 @type revision: C{int}
378
379 @return: The time
380 @rtype: C{int}
381 """
382 try:
383 rtime = self._revTimes[revision]
384 except KeyError:
385 svndate = self.getRevisionProperty(
386 revision, svn_core.SVN_PROP_REVISION_DATE
387 )
388 rtime = self._revTimes[revision] = svn_core.secs_from_timestr(
389 svndate, self._pool
390 )
391
392 return rtime
393
394
396 """ Returns the author of a particular revision
397
398 @param revision: The revision number
399 @type revision: C{int}
400
401 @return: The author
402 @rtype: C{str}
403 """
404 return self.getRevisionProperty(
405 revision, svn_core.SVN_PROP_REVISION_AUTHOR
406 )
407
408
410 """ Returns the log entry of a particular revision
411
412 @param revision: The revision number
413 @type revision: C{int}
414
415 @return: The log entry or C{None}
416 @rtype: C{str}
417 """
418 return self.getRevisionProperty(
419 revision, svn_core.SVN_PROP_REVISION_LOG
420 )
421
422
424 """ Returns the value of a revision property
425
426 @param propname: The property name
427 @type propname: C{str}
428
429 @param revision: The revision number
430 @type revision: C{int}
431
432 @return: The property value
433 @rtype: C{str}
434 """
435 try:
436 value = self._revProps[(revision, propname)]
437 except KeyError:
438 value = self._revProps[(revision, propname)] = \
439 svn_fs.revision_prop(self._fs, revision, propname, self._pool)
440
441 return value
442
443
445 """ Return the RevisionChangeCollector instance
446
447 @param revision: The revision
448 @type revision: C{int}
449
450 @return: The Collector instance
451 @rtype: C{_RevisionChangeCollector}
452 """
453 return _RevisionChangeCollector(self, revision)
454
455
457 """ Return the root object of a particular revision
458
459 @note: The root objects are cached
460
461 @param revision: The revision number
462 @type revision: C{int}
463
464 @return: The revision root
465 @rtype: swig object
466 """
467 try:
468 root = self._revRoots[revision]
469 except KeyError:
470 root = self._revRoots[revision] = svn_fs.revision_root(
471 self._fs, revision, self._pool
472 )
473
474 return root
475
476
478 """ Describes the basic information of a particular path
479
480 @ivar path: The path, we're talking about
481 @type path: C{str}
482
483 @ivar repos: The repository this change belongs to
484 @type repos: C{Repository}
485 """
486
487 - def __init__(self, repos, path, *args, **kwargs):
488 """ Initialization
489
490 @note: Don't override this method, override L{init}
491 instead.
492
493 @param repos: The repository reference
494 @type repos: C{Repository}
495
496 @param path: The path
497 @type path: C{str}
498 """
499 self.repos = repos
500 self.path = (path[:1] == '/' and [path[1:]] or [path])[0]
501
502 self.init(*args, **kwargs)
503
504
505 - def init(self, *args, **kwargs):
506 """ Custom initialization """
507 pass
508
509
511 """ Compares two change objects by path
512
513 @param other: The object compared to
514 @type other: hopefully C{VersionedPathDescriptor}
515
516 @return: Returns -1, 0 or 1
517 @rtype: C{int}
518 """
519 return cmp(self.path, other.path)
520
521
523 """ Returns whether the path is a directory
524
525 @return: is a directory?
526 @rtype: C{bool}
527 """
528 raise NotImplementedError()
529
530
532 """ Describes the lock status of a particular path
533
534 @ivar is_locked: is locked?
535 @type is_locked: C{bool}
536 """
537 - def init(self, *args, **kwargs):
538 """ Custom initialization """
539 self._init(*args, **kwargs)
540
541
542 - def _init(self, is_locked):
543 """ Initialization """
544 self.is_locked = is_locked
545 self._cache = None
546
547
549 """ Returns whether the path is a directory """
550
551 return False
552
553
555 """ Returns the lock description object """
556 if self._cache is None:
557 self._cache = svn_fs.get_lock(
558 self.repos._fs, self.path, self.repos._pool
559 )
560
561 return self._cache
562
563
571
572
574 """ Describes the changes of a particular path
575
576 This is a wrapper around svn_repos.ChangedPath instances.
577 outside of this module one shouldn't need to deal with these.
578
579 @ivar revision: The revision number
580 @type revision: C{int}
581
582 @ivar _change: The change
583 @type _change: C{svn_repos.ChangedPath}
584 """
585 - def init(self, *args, **kwargs):
586 """ Custom initialization """
587 self._init(*args, **kwargs)
588
589
590 - def _init(self, revision, change):
591 """ Initialization
592
593 @param revision: The revision number
594 @type revision: C{int}
595
596 @param change: The change specification
597 @type change: C{svn_repos.ChangedPath}
598 """
599 self.revision = revision
600 self._change = change
601
602
604 """ Returns the revision number of the original path
605
606 @return: The revision number
607 @rtype: C{int}
608 """
609 return self._change.base_rev
610
611
613 """ Returns the original path
614
615 @return: The path
616 @rtype: C{str}
617 """
618
619 return (self._change.base_path[:1] == '/' and
620 [self._change.base_path[1:]] or [self._change.base_path])[0]
621
622
624 """ Returns the dict of modified properties
625
626 The dict contains the property names as keys and
627 a 2-tuple as value where the first element contains the
628 old property value and second element the new one.
629
630 @return: The dict of changed properties
631 @rtype: C{dict}
632 """
633 if type(self._change.prop_changes) == type({}):
634 return self._change.prop_changes
635
636 if not self._change.prop_changes:
637 return {}
638
639
640 if self.wasAdded():
641 propdict1 = {}
642 else:
643 propdict1 = self.repos.getPathProperties(
644 self.getBasePath(), self.getBaseRevision()
645 )
646
647 if self.wasDeleted():
648 propdict2 = {}
649 else:
650 propdict2 = self.repos.getPathProperties(
651 self.path, self.revision
652 )
653
654
655
656 self._change.prop_changes = {}
657 for name, value1, value2 in [
658 (key, propdict1.get(key), propdict2.get(key)) for key in
659 dict.fromkeys(propdict1.keys() + propdict2.keys()).keys()]:
660
661 if value1 != value2:
662 self._change.prop_changes[name] = (value1, value2)
663
664 return self._change.prop_changes
665
666
668 """ Returns whether the path is a directory """
669 return bool(self._change.item_kind == svn_core.svn_node_dir)
670
671
690
691
693 """ Returns whether the path has property changes
694
695 @return: has property changes?
696 @rtype: C{bool}
697 """
698 return bool(self._change.prop_changes)
699
700
702 """ Returns whether the path has content changes
703
704 @return: has content changes?
705 @rtype: C{bool}
706 """
707 return bool(self._change.text_changed)
708
709
711 """ Returns whether the path was deleted
712
713 @return: was deleted?
714 @rtype: C{bool}
715 """
716 if version.min_1_7:
717 return self._change.action == svn_repos.CHANGE_ACTION_DELETE
718 return bool(self._change.path is None)
719
720
722 """ Returns whether the path was added
723
724 @return: was added?
725 @rtype: C{bool}
726 """
727 return bool(self._change.added)
728
729
731 """ Returns whether the path was just modified
732
733 @return: was modified?
734 @rtype: C{bool}
735 """
736 return bool(not self._change.added and not self.wasDeleted())
737
738
740 """ Returns whether the path was copied
741
742 @return: was copied?
743 @rtype: C{bool}
744 """
745 return bool(
746 self._change.added and self._change.base_path and
747 self._change.base_rev != _SVN_INVALID_REVNUM
748 )
749
750
751 if version.min_1_2:
752 - class Collector(svn_repos.ChangeCollector, object):
753 """ svn 1.2 collector """
754 pass
755 else:
756 - class Collector(svn_repos.RevisionChangeCollector, object):
757 """ svn 1.[01] collector """
758 pass
759
760
762 """ Collect all changes between two particular revisions
763
764 @cvar __pool: C{None}
765 @ivar __pool: The APR subpool
766 @type __pool: swig object
767 """
768 __pool = None
769
788
789
791 """ Destroy the subpool """
792 if self.__pool:
793 pool = self.__pool
794 self.__pool = None
795 svn_core.svn_pool_destroy(pool)
796
797
799 """ Returns the subpool
800
801 @return: the pool
802 @rtype: swig object
803 """
804 return self.__pool
805
806
808 """ Return the root of a particular revision
809
810 @note: The root objects are cached
811
812 @param rev: The revision number
813 @type rev: C{int}
814
815 @return: The revision root
816 @rtype: swig object
817 """
818 return self.__repos._getRevisionRoot(rev)
819