more README updates
[jelmer/subvertpy.git] / repository.py
1 # Copyright (C) 2006-2008 Jelmer Vernooij <jelmer@samba.org>
2
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation; either version 3 of the License, or
6 # (at your option) any later version.
7
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 # GNU General Public License for more details.
12
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software
15 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16 """Subversion repository access."""
17
18 import bzrlib
19 from bzrlib import osutils, ui, urlutils, xml5
20 from bzrlib.branch import Branch, BranchCheckResult
21 from bzrlib.errors import (InvalidRevisionId, NoSuchRevision, NotBranchError, 
22                            UninitializableFormat, UnrelatedBranches)
23 from bzrlib.inventory import Inventory
24 from bzrlib.lockable_files import LockableFiles, TransportLock
25 from bzrlib.repository import Repository, RepositoryFormat
26 from bzrlib.revisiontree import RevisionTree
27 from bzrlib.revision import Revision, NULL_REVISION
28 from bzrlib.transport import Transport, get_transport
29 from bzrlib.trace import info, mutter
30
31 from svn.core import SubversionException, Pool
32 import svn.core
33
34 import os
35
36 from branchprops import PathPropertyProvider
37 from cache import create_cache_dir, sqlite3
38 from config import SvnRepositoryConfig
39 import errors
40 from graph import Graph, CachingParentsProvider
41 import logwalker
42 from mapping import (SVN_PROP_BZR_REVISION_ID, SVN_REVPROP_BZR_SIGNATURE,
43                      SVN_PROP_BZR_BRANCHING_SCHEME, BzrSvnMappingv3FileProps,
44                      parse_revision_metadata, parse_revid_property, 
45                      parse_merge_property, BzrSvnMapping,
46                      get_default_mapping, parse_revision_id)
47 from revids import RevidMap
48 from scheme import (BranchingScheme, ListBranchingScheme, 
49                     parse_list_scheme_text, guess_scheme_from_history)
50 from svk import (SVN_PROP_SVK_MERGE, svk_features_merged_since, 
51                  parse_svk_feature)
52 from tree import SvnRevisionTree
53 import urllib
54
55 class lazy_dict:
56     def __init__(self, create_fn):
57         self.create_fn = create_fn
58         self.dict = None
59
60     def _ensure_init(self):
61         if self.dict is None:
62             self.dict = self.create_fn()
63
64     def __len__(self):
65         self._ensure_init()
66         return len(self.dict)
67
68     def __getitem__(self, key):
69         self._ensure_init()
70         return self.dict[key]
71
72     def __setitem__(self, key, value):
73         self._ensure_init()
74         self.dict[key] = value
75
76     def get(self, key, default=None):
77         self._ensure_init()
78         return self.dict.get(key, default)
79
80     def has_key(self, key):
81         self._ensure_init()
82         return self.dict.has_key(key)
83
84
85 def svk_feature_to_revision_id(feature, mapping):
86     """Convert a SVK feature to a revision id for this repository.
87
88     :param feature: SVK feature.
89     :return: revision id.
90     """
91     try:
92         (uuid, bp, revnum) = parse_svk_feature(feature)
93     except errors.InvalidPropertyValue:
94         return None
95     if not mapping.is_branch(bp) and not mapping.is_tag(bp):
96         return None
97     return mapping.generate_revision_id(uuid, revnum, bp)
98
99
100 class SvnRepositoryFormat(RepositoryFormat):
101     """Repository format for Subversion repositories (accessed using svn_ra).
102     """
103     rich_root_data = True
104
105     def __get_matchingbzrdir(self):
106         from remote import SvnRemoteFormat
107         return SvnRemoteFormat()
108
109     _matchingbzrdir = property(__get_matchingbzrdir)
110
111     def __init__(self):
112         super(SvnRepositoryFormat, self).__init__()
113
114     def get_format_description(self):
115         return "Subversion Repository"
116
117     def initialize(self, url, shared=False, _internal=False):
118         raise UninitializableFormat(self)
119
120     def check_conversion_target(self, target_repo_format):
121         return target_repo_format.rich_root_data
122
123
124 def changes_path(changes, path):
125     """Check if one of the specified changes applies 
126     to path or one of its children.
127     """
128     for p in changes:
129         assert isinstance(p, str)
130         if p == path or p.startswith(path+"/") or path == "":
131             return True
132     return False
133
134
135
136 CACHE_DB_VERSION = 3
137
138 cachedbs = {}
139
140 class SvnRepository(Repository):
141     """
142     Provides a simplified interface to a Subversion repository 
143     by using the RA (remote access) API from subversion
144     """
145     def __init__(self, bzrdir, transport, branch_path=None):
146         from bzrlib.plugins.svn import lazy_register_optimizers
147         lazy_register_optimizers()
148         from fileids import SimpleFileIdMap
149         _revision_store = None
150
151         assert isinstance(transport, Transport)
152
153         control_files = LockableFiles(transport, '', TransportLock)
154         Repository.__init__(self, SvnRepositoryFormat(), bzrdir, 
155             control_files, None, None, None)
156
157         self.transport = transport
158         self.uuid = transport.get_uuid()
159         assert self.uuid is not None
160         self.base = transport.base
161         assert self.base is not None
162         self._serializer = xml5.serializer_v5
163         self.dir_cache = {}
164         self.pool = Pool()
165         self.get_config().add_location(self.base)
166         cache_dir = self.create_cache_dir()
167         cachedir_transport = get_transport(cache_dir)
168         cache_file = os.path.join(cache_dir, 'cache-v%d' % CACHE_DB_VERSION)
169         if not cachedbs.has_key(cache_file):
170             cachedbs[cache_file] = sqlite3.connect(cache_file)
171         self.cachedb = cachedbs[cache_file]
172
173         self._log = logwalker.LogWalker(transport=transport, 
174                                         cache_db=self.cachedb)
175
176         # TODO: Only use fileid_map when 
177         # fileprops-based mappings are being used
178         self.branchprop_list = PathPropertyProvider(self._log)
179         self.fileid_map = SimpleFileIdMap(self, cachedir_transport)
180         self.revmap = RevidMap(self.cachedb)
181         self._scheme = None
182         self._hinted_branch_path = branch_path
183
184     def lhs_missing_revisions(self, revhistory, stop_revision):
185         missing = []
186         slice = revhistory[:revhistory.index(stop_revision)+1]
187         for revid in reversed(slice):
188             if self.has_revision(revid):
189                 missing.reverse()
190                 return missing
191             missing.append(revid)
192         raise UnrelatedBranches()
193     
194     def get_transaction(self):
195         raise NotImplementedError(self.get_transaction)
196
197     def get_stored_scheme(self):
198         """Retrieve the stored branching scheme, either in the repository 
199         or in the configuration file.
200         """
201         scheme = self.get_config().get_branching_scheme()
202         if scheme is not None:
203             return (scheme, self.get_config().branching_scheme_is_mandatory())
204
205         last_revnum = self.transport.get_latest_revnum()
206         scheme = self._get_property_scheme(last_revnum)
207         if scheme is not None:
208             return (scheme, True)
209
210         return (None, False)
211
212     def get_mapping(self):
213         return get_default_mapping()(self.get_scheme())
214
215     def _make_parents_provider(self):
216         return CachingParentsProvider(self)
217
218     def get_scheme(self):
219         """Determine the branching scheme to use for this repository.
220
221         :return: Branching scheme.
222         """
223         # First, try to use the branching scheme we already know
224         if self._scheme is not None:
225             return self._scheme
226
227         (scheme, mandatory) = self.get_stored_scheme()
228         if mandatory:
229             self._scheme = scheme
230             return scheme
231
232         if scheme is not None:
233             if (self._hinted_branch_path is None or 
234                 scheme.is_branch(self._hinted_branch_path)):
235                 self._scheme = scheme
236                 return scheme
237
238         last_revnum = self.transport.get_latest_revnum()
239         self.set_branching_scheme(
240             self._guess_scheme(last_revnum, self._hinted_branch_path),
241             store=(last_revnum > 20),
242             mandatory=False)
243
244         return self._scheme
245
246     def _get_property_scheme(self, revnum=None):
247         if revnum is None:
248             revnum = self.transport.get_latest_revnum()
249         text = self.branchprop_list.get_properties("", revnum).get(SVN_PROP_BZR_BRANCHING_SCHEME, None)
250         if text is None:
251             return None
252         return ListBranchingScheme(parse_list_scheme_text(text))
253
254     def set_property_scheme(self, scheme):
255         def done(revmetadata, pool):
256             pass
257         editor = self.transport.get_commit_editor(
258                 {svn.core.SVN_PROP_REVISION_LOG: "Updating branching scheme for Bazaar."},
259                 done, None, False)
260         root = editor.open_root(-1)
261         editor.change_dir_prop(root, SVN_PROP_BZR_BRANCHING_SCHEME, 
262                 "".join(map(lambda x: x+"\n", scheme.branch_list)).encode("utf-8"))
263         editor.close_directory(root)
264         editor.close()
265
266     def _guess_scheme(self, last_revnum, branch_path=None):
267         scheme = guess_scheme_from_history(
268             self._log.iter_changes("", last_revnum), last_revnum, 
269             branch_path)
270         mutter("Guessed branching scheme: %r" % scheme)
271         return scheme
272
273     def set_branching_scheme(self, scheme, store=True, mandatory=False):
274         self._scheme = scheme
275         if store:
276             self.get_config().set_branching_scheme(str(scheme), 
277                                                    mandatory=mandatory)
278
279     def _warn_if_deprecated(self):
280         # This class isn't deprecated
281         pass
282
283     def __repr__(self):
284         return '%s(%r)' % (self.__class__.__name__, 
285                            self.base)
286
287     def create_cache_dir(self):
288         cache_dir = create_cache_dir()
289         dir = os.path.join(cache_dir, self.uuid)
290         if not os.path.exists(dir):
291             info("Initialising Subversion metadata cache in %s" % dir)
292             os.mkdir(dir)
293         return dir
294
295     def _check(self, revision_ids):
296         return BranchCheckResult(self)
297
298     def get_inventory(self, revision_id):
299         assert revision_id != None
300         return self.revision_tree(revision_id).inventory
301
302     def get_fileid_map(self, revnum, path, mapping):
303         return self.fileid_map.get_map(self.uuid, revnum, path, 
304                                        self.revision_fileid_renames, mapping)
305
306     def transform_fileid_map(self, uuid, revnum, branch, changes, renames, 
307                              mapping):
308         return self.fileid_map.apply_changes(uuid, revnum, branch, changes, 
309                                              renames, mapping)[0]
310
311     def all_revision_ids(self, mapping=None):
312         if mapping is None:
313             mapping = self.get_mapping()
314         for (bp, rev) in self.follow_history(
315                 self.transport.get_latest_revnum(), mapping):
316             yield self.generate_revision_id(rev, bp, mapping)
317
318     def get_inventory_weave(self):
319         """See Repository.get_inventory_weave()."""
320         raise NotImplementedError(self.get_inventory_weave)
321
322     def set_make_working_trees(self, new_value):
323         """See Repository.set_make_working_trees()."""
324         pass # FIXME: ignored, nowhere to store it... 
325
326     def make_working_trees(self):
327         """See Repository.make_working_trees().
328
329         Always returns False, as working trees are never created inside 
330         Subversion repositories.
331         """
332         return False
333
334     def iter_changes(self):
335         """
336         
337         :return: iterator over tuples with (revid, parent_revids, changes, revprops, branchprops)
338         """
339         raise NotImplementedError
340
341     def iter_reverse_revision_history(self, revision_id):
342         """Iterate backwards through revision ids in the lefthand history
343
344         :param revision_id: The revision id to start with.  All its lefthand
345             ancestors will be traversed.
346         """
347         if revision_id in (None, NULL_REVISION):
348             return
349         for (revid, parent_revid) in self.get_graph().iter_lhs_ancestry(revision_id):
350             yield revid
351
352     def get_graph(self, other_repository=None):
353         """Return the graph walker for this repository format"""
354         parents_provider = self._make_parents_provider()
355         return Graph(parents_provider)
356
357     def get_ancestry(self, revision_id, topo_sorted=True):
358         """See Repository.get_ancestry().
359         """
360         ancestry = []
361         graph = self.get_graph()
362         for rev, parents in graph.iter_ancestry([revision_id]):
363             if rev == NULL_REVISION:
364                 rev = None
365             ancestry.append(rev)
366         ancestry.reverse()
367         return ancestry
368
369     def has_revision(self, revision_id):
370         """See Repository.has_revision()."""
371         if revision_id is None:
372             return True
373
374         try:
375             (path, revnum, _) = self.lookup_revision_id(revision_id)
376         except NoSuchRevision:
377             return False
378
379         try:
380             return (svn.core.svn_node_dir == self.transport.check_path(path, revnum))
381         except SubversionException, (_, num):
382             if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
383                 return False
384             raise
385
386     def revision_trees(self, revids):
387         """See Repository.revision_trees()."""
388         for revid in revids:
389             yield self.revision_tree(revid)
390
391     def revision_tree(self, revision_id):
392         """See Repository.revision_tree()."""
393         if revision_id is None:
394             revision_id = NULL_REVISION
395
396         if revision_id == NULL_REVISION:
397             inventory = Inventory(root_id=None)
398             inventory.revision_id = revision_id
399             return RevisionTree(self, inventory, revision_id)
400
401         return SvnRevisionTree(self, revision_id)
402
403     def revision_fileid_renames(self, path, revnum, mapping,
404                                 revprops=None, fileprops=None):
405         """Check which files were renamed in a particular revision.
406         
407         :param path: Branch path
408         :
409         :return: dictionary with paths as keys, file ids as values
410         """
411         if revprops is None:
412             revprops = lazy_dict(lambda: self._log.transport.revprop_list(revnum))
413         if fileprops is None:
414             fileprops = lazy_dict(lambda: self.branchprop_list.get_changed_properties(path, revnum))
415
416         return mapping.import_fileid_map(revprops, fileprops)
417
418     def lhs_revision_parent(self, path, revnum, mapping):
419         """Find the mainline parent of the specified revision.
420
421         :param path: Path of the revision in Subversion
422         :param revnum: Subversion revision number
423         :param mapping: Mapping.
424         :return: Revision id of the left-hand-side parent or None if 
425                   this is the first revision
426         """
427         assert isinstance(path, str)
428         assert isinstance(revnum, int)
429
430         if not mapping.is_branch(path) and \
431            not mapping.is_tag(path):
432             raise NoSuchRevision(self, 
433                     self.generate_revision_id(revnum, path, mapping))
434
435         # Make sure the specified revision actually exists
436         changes = self._log.get_revision_paths(revnum)
437         if not changes_path(changes, path):
438             # the specified revno should be changing the branch or 
439             # otherwise it is invalid
440             raise NoSuchRevision(self, 
441                     self.generate_revision_id(revnum, path, mapping))
442
443         while True:
444             next = logwalker.changes_find_prev_location(changes, path, revnum)
445             if next is None:
446                 break
447             (path, revnum) = next
448             changes = self._log.get_revision_paths(revnum)
449
450             if changes_path(changes, path):
451                 revid = self.generate_revision_id(revnum, path, mapping)
452                 if not mapping.is_branch(path) and \
453                    not mapping.is_tag(path):
454                        return None
455                 return revid
456         return None
457
458     def get_parent_map(self, revids):
459         parent_map = {}
460         for revision_id in revids:
461             if revision_id == NULL_REVISION:
462                 parent_map[revision_id] = ()
463             else:
464                 try:
465                     parents = self.revision_parents(revision_id)
466                 except NoSuchRevision:
467                     pass
468                 else:
469                     if len(parents) == 0:
470                         parents = (NULL_REVISION,)
471                     parent_map[revision_id] = parents
472         return parent_map
473
474     def _svk_merged_revisions(self, branch, revnum, mapping, 
475                               fileprops):
476         """Find out what SVK features were merged in a revision.
477
478         """
479         current = fileprops.get(SVN_PROP_SVK_MERGE, "")
480         if current == "":
481             return
482         (prev_path, prev_revnum) = self._log.get_previous(branch, revnum)
483         if prev_path is None and prev_revnum == -1:
484             previous = ""
485         else:
486             previous = self.branchprop_list.get_properties(prev_path.encode("utf-8"), prev_revnum).get(SVN_PROP_SVK_MERGE, "")
487         for feature in svk_features_merged_since(current, previous):
488             revid = svk_feature_to_revision_id(feature, mapping)
489             if revid is not None:
490                 yield revid
491
492     def revision_parents(self, revision_id, svn_fileprops=None, svn_revprops=None):
493         """See Repository.revision_parents()."""
494         (branch, revnum, mapping) = self.lookup_revision_id(revision_id)
495         mainline_parent = self.lhs_revision_parent(branch, revnum, mapping)
496         if mainline_parent is None:
497             return ()
498
499         parent_ids = (mainline_parent,)
500
501         if svn_fileprops is None:
502             svn_fileprops = lazy_dict(lambda: self.branchprop_list.get_changed_properties(branch, revnum))
503
504         if svn_revprops is None:
505             svn_revprops = lazy_dict(lambda: self.transport.revprop_list(revnum))
506
507         extra_rhs_parents = mapping.get_rhs_parents(branch, svn_revprops, svn_fileprops)
508         parent_ids += extra_rhs_parents
509
510         if extra_rhs_parents == ():
511             parent_ids += tuple(self._svk_merged_revisions(branch, revnum, mapping, svn_fileprops))
512
513         return parent_ids
514
515     def get_revision(self, revision_id, svn_revprops=None, svn_fileprops=None):
516         """See Repository.get_revision."""
517         if not revision_id or not isinstance(revision_id, str):
518             raise InvalidRevisionId(revision_id=revision_id, branch=self)
519
520         (path, revnum, mapping) = self.lookup_revision_id(revision_id)
521         
522         if svn_revprops is None:
523             svn_revprops = lazy_dict(lambda: self.transport.revprop_list(revnum))
524         if svn_fileprops is None:
525             svn_fileprops = lazy_dict(lambda: self.branchprop_list.get_changed_properties(path, revnum))
526         parent_ids = self.revision_parents(revision_id, svn_fileprops=svn_fileprops, svn_revprops=svn_revprops)
527
528         rev = Revision(revision_id=revision_id, parent_ids=parent_ids,
529                        inventory_sha1=None)
530
531         mapping.import_revision(svn_revprops, svn_fileprops, rev)
532
533         return rev
534
535     def get_revisions(self, revision_ids):
536         """See Repository.get_revisions()."""
537         # TODO: More efficient implementation?
538         return map(self.get_revision, revision_ids)
539
540     def add_revision(self, rev_id, rev, inv=None, config=None):
541         raise NotImplementedError(self.add_revision)
542
543     def generate_revision_id(self, revnum, path, mapping):
544         """Generate an unambiguous revision id. 
545         
546         :param revnum: Subversion revision number.
547         :param path: Branch path.
548         :param mapping: Mapping to use.
549
550         :return: New revision id.
551         """
552         assert isinstance(path, str)
553         assert isinstance(revnum, int)
554         assert isinstance(mapping, BzrSvnMapping)
555
556         # Look in the cache to see if it already has a revision id
557         revid = self.revmap.lookup_branch_revnum(revnum, path, str(mapping.scheme))
558         if revid is not None:
559             return revid
560
561         # See if there is a bzr:revision-id revprop set
562         try:
563             revprops = lazy_dict(lambda: self._log._get_transport().revprop_list(revnum))
564             fileprops = lazy_dict(lambda: self.branchprop_list.get_changed_properties(path, revnum))
565             (bzr_revno, revid) = mapping.get_revision_id(path, revprops, fileprops)
566         except SubversionException, (_, num):
567             if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
568                 raise NoSuchRevision(path, revnum)
569             raise
570
571         # Or generate it
572         if revid is None:
573             revid = mapping.generate_revision_id(self.uuid, revnum, path)
574         self.revmap.insert_revid(revid, path, revnum, revnum, 
575                 str(mapping.scheme))
576         return revid
577
578     def lookup_revision_id(self, revid, scheme=None):
579         """Parse an existing Subversion-based revision id.
580
581         :param revid: The revision id.
582         :param scheme: Optional branching scheme to use when searching for 
583                        revisions
584         :raises: NoSuchRevision
585         :return: Tuple with branch path, revision number and mapping.
586         """
587         def get_scheme(name):
588             assert isinstance(name, str)
589             return BranchingScheme.find_scheme(name)
590
591         # Try a simple parse
592         try:
593             (uuid, branch_path, revnum, mapping) = parse_revision_id(revid)
594             assert isinstance(branch_path, str)
595             assert isinstance(mapping, BzrSvnMapping)
596             if uuid == self.uuid:
597                 return (branch_path, revnum, mapping)
598             # If the UUID doesn't match, this may still be a valid revision
599             # id; a revision from another SVN repository may be pushed into 
600             # this one.
601         except InvalidRevisionId:
602             pass
603
604         # Check the record out of the revmap, if it exists
605         try:
606             (branch_path, min_revnum, max_revnum, \
607                     scheme) = self.revmap.lookup_revid(revid)
608             assert isinstance(branch_path, str)
609             assert isinstance(scheme, str)
610             # Entry already complete?
611             if min_revnum == max_revnum:
612                 return (branch_path, min_revnum, BzrSvnMappingv3FileProps(get_scheme(scheme)))
613         except NoSuchRevision, e:
614             # If there is no entry in the map, walk over all branches:
615             if scheme is None:
616                 scheme = self.get_scheme()
617             last_revnum = self.transport.get_latest_revnum()
618             if (last_revnum <= self.revmap.last_revnum_checked(str(scheme))):
619                 # All revision ids in this repository for the current 
620                 # scheme have already been discovered. No need to 
621                 # check again.
622                 raise e
623             found = False
624             for (branch, revno, _) in self.find_branchpaths(scheme, 
625                     self.revmap.last_revnum_checked(str(scheme)),
626                     last_revnum):
627                 assert isinstance(branch, str)
628                 assert isinstance(revno, int)
629                 # Look at their bzr:revision-id-vX
630                 revids = []
631                 try:
632                     props = self.branchprop_list.get_properties(branch, revno)
633                     for line in props.get(SVN_PROP_BZR_REVISION_ID+str(scheme), "").splitlines():
634                         try:
635                             revids.append(parse_revid_property(line))
636                         except errors.InvalidPropertyValue, ie:
637                             mutter(str(ie))
638                 except SubversionException, (_, svn.core.SVN_ERR_FS_NOT_DIRECTORY):
639                     continue
640
641                 # If there are any new entries that are not yet in the cache, 
642                 # add them
643                 for (entry_revno, entry_revid) in revids:
644                     if entry_revid == revid:
645                         found = True
646                     self.revmap.insert_revid(entry_revid, branch, 0, revno, 
647                             str(scheme))
648                 
649             # We've added all the revision ids for this scheme in the repository,
650             # so no need to check again unless new revisions got added
651             self.revmap.set_last_revnum_checked(str(scheme), last_revnum)
652             if not found:
653                 raise e
654             (branch_path, min_revnum, max_revnum, scheme) = self.revmap.lookup_revid(revid)
655             assert isinstance(branch_path, str)
656
657         # Find the branch property between min_revnum and max_revnum that 
658         # added revid
659         for (bp, rev) in self.follow_branch(branch_path, max_revnum, 
660                                             get_scheme(str(scheme))):
661             try:
662                 (entry_revno, entry_revid) = parse_revid_property(
663                  self.branchprop_list.get_property_diff(bp, rev, 
664                      SVN_PROP_BZR_REVISION_ID+str(scheme)).strip("\n"))
665             except errors.InvalidPropertyValue:
666                 # Don't warn about encountering an invalid property, 
667                 # that will already have happened earlier
668                 continue
669             if entry_revid == revid:
670                 self.revmap.insert_revid(revid, bp, rev, rev, scheme)
671                 return (bp, rev, BzrSvnMappingv3FileProps(get_scheme(scheme)))
672
673         raise AssertionError("Revision id %s was added incorrectly" % revid)
674
675     def get_inventory_xml(self, revision_id):
676         """See Repository.get_inventory_xml()."""
677         return self.serialise_inventory(self.get_inventory(revision_id))
678
679     def get_inventory_sha1(self, revision_id):
680         """Get the sha1 for the XML representation of an inventory.
681
682         :param revision_id: Revision id of the inventory for which to return 
683          the SHA1.
684         :return: XML string
685         """
686
687         return osutils.sha_string(self.get_inventory_xml(revision_id))
688
689     def get_revision_xml(self, revision_id):
690         """Return the XML representation of a revision.
691
692         :param revision_id: Revision for which to return the XML.
693         :return: XML string
694         """
695         return self._serializer.write_revision_to_string(
696             self.get_revision(revision_id))
697
698     def follow_history(self, revnum, mapping):
699         """Yield all the branches found between the start of history 
700         and a specified revision number.
701
702         :param revnum: Revision number up to which to search.
703         :return: iterator over branches in the range 0..revnum
704         """
705         assert mapping is not None
706
707         for (_, paths, revnum) in self._log.iter_changes("", revnum):
708             yielded_paths = []
709             for p in paths:
710                 try:
711                     bp = mapping.scheme.unprefix(p)[0]
712                     if not bp in yielded_paths:
713                         if not paths.has_key(bp) or paths[bp][0] != 'D':
714                             assert revnum > 0 or bp == ""
715                             yield (bp, revnum)
716                         yielded_paths.append(bp)
717                 except NotBranchError:
718                     pass
719
720     def get_lhs_parent(self, revid):
721         (branch_path, revnum, mapping) = self.lookup_revision_id(revid)
722         parent_revid = self.lhs_revision_parent(branch_path, revnum, mapping)
723         if parent_revid is None:
724             return NULL_REVISION
725         return parent_revid
726
727     def follow_branch(self, branch_path, revnum, mapping):
728         """Follow the history of a branch. Will yield all the 
729         left-hand side ancestors of a specified revision.
730     
731         :param branch_path: Subversion path to search.
732         :param revnum: Revision number in Subversion to start.
733         :param mapping: Mapping.
734         :return: iterator over the ancestors
735         """
736         assert branch_path is not None
737         assert isinstance(branch_path, str)
738         assert isinstance(revnum, int) and revnum >= 0
739         assert mapping.is_branch(branch_path) or mapping.is_tag(branch_path)
740         branch_path = branch_path.strip("/")
741
742         for (path, paths, revnum) in self._log.iter_changes(branch_path, revnum):
743             if not mapping.is_branch(path) and not mapping.is_tag(path):
744                 # FIXME: if copyfrom_path is not a branch path, 
745                 # should simulate a reverse "split" of a branch
746                 # for now, just make it look like the branch ended here
747                 break
748             yield (path, revnum)
749         
750     def follow_branch_history(self, branch_path, revnum, mapping):
751         """Return all the changes that happened in a branch 
752         until branch_path,revnum. 
753
754         :return: iterator that returns tuples with branch path, 
755             changed paths and revision number.
756         """
757         assert isinstance(branch_path, str)
758         assert mapping.is_branch(branch_path) or mapping.is_tag(branch_path), \
759                 "Mapping %r doesn't accept %s as branch or tag" % (mapping, branch_path)
760
761         for (bp, paths, revnum) in self._log.iter_changes(branch_path, revnum):
762             assert revnum > 0 or bp == ""
763             assert mapping.is_branch(bp) or mapping.is_tag(bp)
764             # Remove non-bp paths from paths
765             for p in paths.keys():
766                 if not p.startswith(bp+"/") and bp != p and bp != "":
767                     del paths[p]
768
769             if paths == {}:
770                 continue
771
772             if (paths.has_key(bp) and paths[bp][1] is not None and 
773                 not mapping.is_branch(paths[bp][1]) and
774                 not mapping.is_tag(paths[bp][1])):
775                 # FIXME: if copyfrom_path is not a branch path, 
776                 # should simulate a reverse "split" of a branch
777                 # for now, just make it look like the branch ended here
778                 for c in self._log.find_children(paths[bp][1], paths[bp][2]):
779                     path = c.replace(paths[bp][1], bp+"/", 1).replace("//", "/")
780                     paths[path] = ('A', None, -1)
781                 paths[bp] = ('A', None, -1)
782
783                 yield (bp, paths, revnum)
784                 return
785                      
786             yield (bp, paths, revnum)
787
788     def get_config(self):
789         return SvnRepositoryConfig(self.uuid)
790
791     def has_signature_for_revision_id(self, revision_id):
792         """Check whether a signature exists for a particular revision id.
793
794         :param revision_id: Revision id for which the signatures should be looked up.
795         :return: False, as no signatures are stored for revisions in Subversion 
796             at the moment.
797         """
798         try:
799             (path, revnum, mapping) = self.lookup_revision_id(revision_id)
800         except NoSuchRevision:
801             return False
802         revprops = self.transport.revprop_list(revnum)
803         return revprops.has_key(SVN_REVPROP_BZR_SIGNATURE)
804
805     def get_signature_text(self, revision_id):
806         """Return the signature text for a particular revision.
807
808         :param revision_id: Id of the revision for which to return the 
809                             signature.
810         :raises NoSuchRevision: Always
811         """
812         (path, revnum, mapping) = self.lookup_revision_id(revision_id)
813         revprops = self.transport.revprop_list(revnum)
814         try:
815             return revprops[SVN_REVPROP_BZR_SIGNATURE]
816         except KeyError:
817             raise NoSuchRevision(self, revision_id)
818
819     def add_signature_text(self, revision_id, signature):
820         (path, revnum, mapping) = self.lookup_revision_id(revision_id)
821         self.transport.change_rev_prop(revnum, SVN_REVPROP_BZR_SIGNATURE, signature)
822
823     def get_revision_graph(self, revision_id=None):
824         """See Repository.get_revision_graph()."""
825         graph = self.get_graph()
826
827         if revision_id is None:
828             revision_ids = self.all_revision_ids(self.get_mapping())
829         else:
830             revision_ids = [revision_id]
831
832         ret = {}
833         for (revid, parents) in graph.iter_ancestry(revision_ids):
834             if revid == NULL_REVISION:
835                 continue
836             if (NULL_REVISION,) == parents:
837                 ret[revid] = ()
838             else:
839                 ret[revid] = parents
840
841         if revision_id is not None and revision_id != NULL_REVISION and ret[revision_id] is None:
842             raise NoSuchRevision(self, revision_id)
843
844         return ret
845
846     def find_branches(self, using=False, scheme=None):
847         """Find branches underneath this repository.
848
849         This will include branches inside other branches.
850
851         :param using: If True, list only branches using this repository.
852         """
853         # All branches use this repository, so the using argument can be 
854         # ignored.
855         if scheme is None:
856             scheme = self.get_scheme()
857
858         existing_branches = [bp for (bp, revnum, _) in 
859                 filter(lambda (bp, rev, exists): exists,
860                        self.find_branchpaths(scheme))]
861
862         branches = []
863         for bp in existing_branches:
864             try:
865                 branches.append(Branch.open(urlutils.join(self.base, bp)))
866             except NotBranchError: # Skip non-directories
867                 pass
868         return branches
869
870     def find_branchpaths(self, scheme, from_revnum=0, to_revnum=None):
871         """Find all branch paths that were changed in the specified revision 
872         range.
873
874         :param revnum: Revision to search for branches.
875         :return: iterator that returns tuples with (path, revision number, still exists). The revision number is the revision in which the branch last existed.
876         """
877         assert scheme is not None
878         if to_revnum is None:
879             to_revnum = self.transport.get_latest_revnum()
880
881         created_branches = {}
882
883         ret = []
884
885         pb = ui.ui_factory.nested_progress_bar()
886         try:
887             for i in range(from_revnum, to_revnum+1):
888                 pb.update("finding branches", i, to_revnum+1)
889                 paths = self._log.get_revision_paths(i)
890                 for p in sorted(paths.keys()):
891                     if scheme.is_branch(p) or scheme.is_tag(p):
892                         if paths[p][0] in ('R', 'D') and p in created_branches:
893                             del created_branches[p]
894                             if paths[p][1]:
895                                 prev_path = paths[p][1]
896                                 prev_rev = paths[p][2]
897                             else:
898                                 prev_path = p
899                                 prev_rev = self._log.find_latest_change(p, 
900                                     i-1, include_parents=True, 
901                                     include_children=True)
902                             assert isinstance(prev_rev, int)
903                             ret.append((prev_path, prev_rev, False))
904
905                         if paths[p][0] in ('A', 'R'): 
906                             created_branches[p] = i
907                     elif scheme.is_branch_parent(p) or \
908                             scheme.is_tag_parent(p):
909                         if paths[p][0] in ('R', 'D'):
910                             k = created_branches.keys()
911                             for c in k:
912                                 if c.startswith(p+"/") and c in created_branches:
913                                     del created_branches[c] 
914                                     j = self._log.find_latest_change(c, i-1, 
915                                             include_parents=True, 
916                                             include_children=True)
917                                     assert isinstance(j, int)
918                                     ret.append((c, j, False))
919                         if paths[p][0] in ('A', 'R'):
920                             parents = [p]
921                             while parents:
922                                 p = parents.pop()
923                                 try:
924                                     for c in self.transport.get_dir(p, i)[0].keys():
925                                         n = p+"/"+c
926                                         if scheme.is_branch(n) or scheme.is_tag(n):
927                                             created_branches[n] = i
928                                         elif (scheme.is_branch_parent(n) or 
929                                               scheme.is_tag_parent(n)):
930                                             parents.append(n)
931                                 except SubversionException, (_, svn.core.SVN_ERR_FS_NOT_DIRECTORY):
932                                     pass
933         finally:
934             pb.finished()
935
936         pb = ui.ui_factory.nested_progress_bar()
937         i = 0
938         for p in created_branches:
939             pb.update("determining branch last changes", 
940                       i, len(created_branches))
941             j = self._log.find_latest_change(p, to_revnum, 
942                                              include_parents=True,
943                                              include_children=True)
944             if j is None:
945                 j = created_branches[p]
946             assert isinstance(j, int)
947             ret.append((p, j, True))
948             i += 1
949         pb.finished()
950
951         return ret
952
953     def is_shared(self):
954         """Return True if this repository is flagged as a shared repository."""
955         return True
956
957     def get_physical_lock_status(self):
958         return False
959
960     def get_commit_builder(self, branch, parents, config, timestamp=None, 
961                            timezone=None, committer=None, revprops=None, 
962                            revision_id=None):
963         from commit import SvnCommitBuilder
964         return SvnCommitBuilder(self, branch, parents, config, timestamp, 
965                 timezone, committer, revprops, revision_id)
966
967
968