Move cache message to directory creation.
[jelmer/subvertpy.git] / repository.py
index efbd46381fc434992198e9e06e74e5e4f83add46..2b0af5471abecf1c9d1a7e619bce4b8c917584fd 100644 (file)
 """Subversion repository access."""
 
 import bzrlib
-from bzrlib.branch import BranchCheckResult
-from bzrlib.errors import (InvalidRevisionId, NoSuchRevision, 
-                           NotBranchError, UninitializableFormat, BzrError)
+from bzrlib import osutils, ui, urlutils
+from bzrlib.branch import Branch, BranchCheckResult
+from bzrlib.errors import (InvalidRevisionId, NoSuchRevision, NotBranchError, 
+                           UninitializableFormat, UnrelatedBranches)
 from bzrlib.inventory import Inventory
 from bzrlib.lockable_files import LockableFiles, TransportLock
-import bzrlib.osutils as osutils
 from bzrlib.repository import Repository, RepositoryFormat
 from bzrlib.revisiontree import RevisionTree
 from bzrlib.revision import Revision, NULL_REVISION
-from bzrlib.transport import Transport
-from bzrlib.timestamp import unpack_highres_date, format_highres_date
-from bzrlib.trace import mutter
+from bzrlib.transport import Transport, get_transport
+from bzrlib.trace import info, mutter
 
 from svn.core import SubversionException, Pool
 import svn.core
@@ -36,75 +35,35 @@ import os
 
 from branchprops import BranchPropertyList
 from cache import create_cache_dir, sqlite3
+import calendar
+from config import SvnRepositoryConfig
 import errors
 import logwalker
-from revids import (generate_svn_revision_id, parse_svn_revision_id, 
-                    MAPPING_VERSION, RevidMap)
+from mapping import (default_mapping, SVN_PROP_BZR_REVISION_ID, 
+                     SVN_PROP_BZR_REVISION_INFO, SVN_PROP_BZR_BRANCHING_SCHEME,
+                     SVN_PROP_BZR_ANCESTRY, SVN_PROP_BZR_FILEIDS,
+                     parse_revision_metadata, parse_revid_property)
+                      
+from revids import RevidMap
+from scheme import (BranchingScheme, ListBranchingScheme, 
+                    parse_list_scheme_text, guess_scheme_from_history)
 from tree import SvnRevisionTree
+import urllib
 
-SVN_PROP_BZR_PREFIX = 'bzr:'
-SVN_PROP_BZR_MERGE = 'bzr:merge'
-SVN_PROP_BZR_FILEIDS = 'bzr:file-ids'
 SVN_PROP_SVK_MERGE = 'svk:merge'
-SVN_PROP_BZR_FILEIDS = 'bzr:file-ids'
-SVN_PROP_BZR_REVISION_INFO = 'bzr:revision-info'
-SVN_REVPROP_BZR_SIGNATURE = 'bzr:gpg-signature'
-SVN_PROP_BZR_REVISION_ID = 'bzr:revision-id-v%d' % MAPPING_VERSION
 
-def parse_revision_metadata(text, rev):
-    """Parse a revision info text (as set in bzr:revision-info).
-
-    :param text: text to parse
-    :param rev: Revision object to apply read parameters to
-    """
-    in_properties = False
-    for l in text.splitlines():
-        try:
-            key, value = l.split(": ", 2)
-        except ValueError:
-            raise BzrError("Missing : in revision metadata")
-        if key == "committer":
-            rev.committer = str(value)
-        elif key == "timestamp":
-            (rev.timestamp, rev.timezone) = unpack_highres_date(value)
-        elif key == "properties":
-            in_properties = True
-        elif key[0] == "\t" and in_properties:
-            rev.properties[str(key[1:])] = str(value)
-        else:
-            raise BzrError("Invalid key %r" % key)
-
-def generate_revision_metadata(timestamp, timezone, committer, revprops):
-    """Generate revision metadata text for the specified revision 
-    properties.
-
-    :param timestamp: timestamp of the revision, in seconds since epoch
-    :param timezone: timezone, specified by offset from GMT in seconds
-    :param committer: name/email of the committer
-    :param revprops: dictionary with custom revision properties
-    :return: text with data to set bzr:revision-info to.
-    """
-    assert timestamp is None or isinstance(timestamp, float)
-    text = ""
-    if timestamp is not None:
-        text += "timestamp: %s\n" % format_highres_date(timestamp, timezone) 
-    if committer is not None:
-        text += "committer: %s\n" % committer
-    if revprops is not None and revprops != {}:
-        text += "properties: \n"
-        for k, v in sorted(revprops.items()):
-            text += "\t%s: %s\n" % (k, v)
-    return text
-
-
-def svk_feature_to_revision_id(feature):
-    """Create a revision id from a svk feature identifier.
+def parse_svk_feature(feature):
+    """Parse a svk feature identifier.
 
     :param feature: The feature identifier as string.
-    :return: Matching revision id.
+    :return: tuple with uuid, branch path and revnum
     """
-    (uuid, branch, revnum) = feature.split(":")
-    return generate_svn_revision_id(uuid, int(revnum), branch.strip("/"))
+    try:
+        (uuid, branch, revnum) = feature.split(":", 3)
+    except ValueError:
+        raise errors.InvalidPropertyValue(SVN_PROP_SVK_MERGE, 
+                "not enough colons")
+    return (uuid, branch.strip("/"), int(revnum))
 
 
 def revision_id_to_svk_feature(revid):
@@ -113,19 +72,26 @@ def revision_id_to_svk_feature(revid):
     :param revid: Revision id to convert.
     :return: Matching SVK feature identifier.
     """
-    (uuid, branch, revnum) = parse_svn_revision_id(revid)
+    assert isinstance(revid, str)
+    (uuid, branch, revnum, _) = default_mapping.parse_revision_id(revid)
+    # TODO: What about renamed revisions? Should use 
+    # repository.lookup_revision_id here.
     return "%s:/%s:%d" % (uuid, branch, revnum)
 
 
 class SvnRepositoryFormat(RepositoryFormat):
     """Repository format for Subversion repositories (accessed using svn_ra).
     """
-    rich_root_data = False
+    rich_root_data = True
+
+    def __get_matchingbzrdir(self):
+        from remote import SvnRemoteFormat
+        return SvnRemoteFormat()
+
+    _matchingbzrdir = property(__get_matchingbzrdir)
 
     def __init__(self):
         super(SvnRepositoryFormat, self).__init__()
-        from format import SvnFormat
-        self._matchingbzrdir = SvnFormat()
 
     def get_format_description(self):
         return "Subversion Repository"
@@ -134,6 +100,11 @@ class SvnRepositoryFormat(RepositoryFormat):
         """Svn repositories cannot be created (yet)."""
         raise UninitializableFormat(self)
 
+    def check_conversion_target(self, target_repo_format):
+        return target_repo_format.rich_root_data
+
+CACHE_DB_VERSION = 3
+
 cachedbs = {}
 
 class SvnRepository(Repository):
@@ -141,7 +112,9 @@ class SvnRepository(Repository):
     Provides a simplified interface to a Subversion repository 
     by using the RA (remote access) API from subversion
     """
-    def __init__(self, bzrdir, transport):
+    def __init__(self, bzrdir, transport, branch_path=None):
+        from bzrlib.plugins.svn import lazy_register_optimizers
+        lazy_register_optimizers()
         from fileids import SimpleFileIdMap
         _revision_store = None
 
@@ -153,32 +126,118 @@ class SvnRepository(Repository):
 
         self.transport = transport
         self.uuid = transport.get_uuid()
+        assert self.uuid is not None
         self.base = transport.base
+        assert self.base is not None
         self._serializer = None
         self.dir_cache = {}
-        self.scheme = bzrdir.scheme
         self.pool = Pool()
-
-        assert self.base
-        assert self.uuid
-
-        cache_file = os.path.join(self.create_cache_dir(), 
-                                  'cache-v%d' % MAPPING_VERSION)
+        self.get_config().add_location(self.base)
+        cache_dir = self.create_cache_dir()
+        cachedir_transport = get_transport(cache_dir)
+        cache_file = os.path.join(cache_dir, 'cache-v%d' % CACHE_DB_VERSION)
         if not cachedbs.has_key(cache_file):
             cachedbs[cache_file] = sqlite3.connect(cache_file)
         self.cachedb = cachedbs[cache_file]
 
-        self._latest_revnum = transport.get_latest_revnum()
         self._log = logwalker.LogWalker(transport=transport, 
-                                        cache_db=self.cachedb, 
-                                        last_revnum=self._latest_revnum)
+                                        cache_db=self.cachedb)
 
         self.branchprop_list = BranchPropertyList(self._log, self.cachedb)
-        self.fileid_map = SimpleFileIdMap(self, self.cachedb)
+        self.fileid_map = SimpleFileIdMap(self, cachedir_transport)
         self.revmap = RevidMap(self.cachedb)
+        self._scheme = None
+        self._hinted_branch_path = branch_path
+
+    def lhs_missing_revisions(self, revhistory, stop_revision):
+        missing = []
+        slice = revhistory[:revhistory.index(stop_revision)+1]
+        for revid in reversed(slice):
+            if self.has_revision(revid):
+                missing.reverse()
+                return missing
+            missing.append(revid)
+        raise UnrelatedBranches()
+    
+    def get_transaction(self):
+        raise NotImplementedError(self.get_transaction)
 
-    def set_branching_scheme(self, scheme):
-        self.scheme = scheme
+    def get_stored_scheme(self):
+        """Retrieve the stored branching scheme, either in the repository 
+        or in the configuration file.
+        """
+        scheme = self.get_config().get_branching_scheme()
+        if scheme is not None:
+            return (scheme, self.get_config().branching_scheme_is_mandatory())
+
+        last_revnum = self.transport.get_latest_revnum()
+        scheme = self._get_property_scheme(last_revnum)
+        if scheme is not None:
+            return (scheme, True)
+
+        return (None, False)
+
+    def get_scheme(self):
+        """Determine the branching scheme to use for this repository.
+
+        :return: Branching scheme.
+        """
+        # First, try to use the branching scheme we already know
+        if self._scheme is not None:
+            return self._scheme
+
+        (scheme, mandatory) = self.get_stored_scheme()
+        if mandatory:
+            self._scheme = scheme
+            return scheme
+
+        if scheme is not None:
+            if (self._hinted_branch_path is None or 
+                scheme.is_branch(self._hinted_branch_path)):
+                self._scheme = scheme
+                return scheme
+
+        last_revnum = self.transport.get_latest_revnum()
+        self.set_branching_scheme(
+            self._guess_scheme(last_revnum, self._hinted_branch_path),
+            store=(last_revnum > 20),
+            mandatory=False)
+
+        return self._scheme
+
+    def _get_property_scheme(self, revnum=None):
+        if revnum is None:
+            revnum = self.transport.get_latest_revnum()
+        text = self.branchprop_list.get_property("", 
+            revnum, SVN_PROP_BZR_BRANCHING_SCHEME, None)
+        if text is None:
+            return None
+        return ListBranchingScheme(parse_list_scheme_text(text))
+
+    def set_property_scheme(self, scheme):
+        def done(revision, date, author):
+            pass
+        editor = self.transport.get_commit_editor(
+                {svn.core.SVN_PROP_REVISION_LOG: "Updating branching scheme for Bazaar."},
+                done, None, False)
+        root = editor.open_root(-1)
+        editor.change_dir_prop(root, SVN_PROP_BZR_BRANCHING_SCHEME, 
+                "".join(map(lambda x: x+"\n", scheme.branch_list)).encode("utf-8"))
+        editor.close_directory(root)
+        editor.close()
+
+    def _guess_scheme(self, last_revnum, branch_path=None):
+        scheme = guess_scheme_from_history(
+            self._log.follow_path("", last_revnum), last_revnum, 
+            branch_path)
+        mutter("Guessed branching scheme: %r" % scheme)
+        return scheme
+
+    def set_branching_scheme(self, scheme, store=True, mandatory=False):
+        self._scheme = scheme
+        if store:
+            self.get_config().set_branching_scheme(str(scheme), 
+                                                   mandatory=mandatory)
 
     def _warn_if_deprecated(self):
         # This class isn't deprecated
@@ -192,6 +251,7 @@ class SvnRepository(Repository):
         cache_dir = create_cache_dir()
         dir = os.path.join(cache_dir, self.uuid)
         if not os.path.exists(dir):
+            info("Initialising Subversion metadata cache in %s" % dir)
             os.mkdir(dir)
         return dir
 
@@ -202,19 +262,24 @@ class SvnRepository(Repository):
         assert revision_id != None
         return self.revision_tree(revision_id).inventory
 
-    def get_fileid_map(self, revnum, path):
-        return self.fileid_map.get_map(self.uuid, revnum, path,
-                                       self.revision_fileid_renames)
+    def get_fileid_map(self, revnum, path, scheme):
+        return self.fileid_map.get_map(self.uuid, revnum, path, 
+                                       self.revision_fileid_renames, scheme)
 
-    def transform_fileid_map(self, uuid, revnum, branch, changes, renames):
+    def transform_fileid_map(self, uuid, revnum, branch, changes, renames, 
+                             scheme):
         return self.fileid_map.apply_changes(uuid, revnum, branch, changes, 
-                                             renames)
+                                             renames, scheme)
 
-    def all_revision_ids(self):
-        for (bp, rev) in self.follow_history(self.transport.get_latest_revnum()):
-            yield self.generate_revision_id(rev, bp)
+    def all_revision_ids(self, scheme=None):
+        if scheme is None:
+            scheme = self.get_scheme()
+        for (bp, rev) in self.follow_history(
+                self.transport.get_latest_revnum(), scheme):
+            yield self.generate_revision_id(rev, bp, str(scheme))
 
     def get_inventory_weave(self):
+        """See Repository.get_inventory_weave()."""
         raise NotImplementedError(self.get_inventory_weave)
 
     def set_make_working_trees(self, new_value):
@@ -229,7 +294,7 @@ class SvnRepository(Repository):
         """
         return False
 
-    def get_ancestry(self, revision_id):
+    def get_ancestry(self, revision_id, topo_sorted=True):
         """See Repository.get_ancestry().
         
         Note: only the first bit is topologically ordered!
@@ -237,17 +302,18 @@ class SvnRepository(Repository):
         if revision_id is None: 
             return [None]
 
-        (path, revnum) = self.lookup_revision_id(revision_id)
+        (path, revnum, scheme) = self.lookup_revision_id(revision_id)
 
         ancestry = [revision_id]
 
         for l in self.branchprop_list.get_property(path, revnum, 
-                                    SVN_PROP_BZR_MERGE, "").splitlines():
+                                    SVN_PROP_BZR_ANCESTRY+str(scheme), "").splitlines():
             ancestry.extend(l.split("\n"))
 
         if revnum > 0:
-            for (branch, rev) in self.follow_branch(path, revnum - 1):
-                ancestry.append(self.generate_revision_id(rev, branch))
+            for (branch, rev) in self.follow_branch(path, revnum - 1, scheme):
+                ancestry.append(
+                    self.generate_revision_id(rev, branch, str(scheme)))
 
         ancestry.append(None)
         ancestry.reverse()
@@ -259,17 +325,18 @@ class SvnRepository(Repository):
             return True
 
         try:
-            (path, revnum) = self.lookup_revision_id(revision_id)
+            (path, revnum, _) = self.lookup_revision_id(revision_id)
         except NoSuchRevision:
             return False
 
         try:
-            return (svn.core.svn_node_none != self.transport.check_path(path.encode('utf8'), revnum))
+            return (svn.core.svn_node_dir == self.transport.check_path(path, revnum))
         except SubversionException, (_, num):
             if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
                 return False
             raise
 
+
     def revision_trees(self, revids):
         """See Repository.revision_trees()."""
         for revid in revids:
@@ -288,92 +355,182 @@ class SvnRepository(Repository):
         return SvnRevisionTree(self, revision_id)
 
     def revision_fileid_renames(self, revid):
-        """Check which files were renamed in a particular revision."""
-        (path, revnum) = self.lookup_revision_id(revid)
-        items = self.branchprop_list.get_property_diff(path, revnum, 
-                                  SVN_PROP_BZR_FILEIDS).splitlines()
-        return dict(map(lambda x: x.split("\t"), items))
-
-    def _mainline_revision_parent(self, path, revnum):
-        assert isinstance(path, basestring)
+        """Check which files were renamed in a particular revision.
+        
+        :param revid: Id of revision to look up.
+        :return: dictionary with paths as keys, file ids as values
+        """
+        (path, revnum, _) = self.lookup_revision_id(revid)
+        # Only consider bzr:file-ids if this is a bzr revision
+        if not self.branchprop_list.touches_property(path, revnum, 
+                SVN_PROP_BZR_REVISION_INFO):
+            return {}
+        fileids = self.branchprop_list.get_property(path, revnum, 
+                                                    SVN_PROP_BZR_FILEIDS)
+        if fileids is None:
+            return {}
+        ret = {}
+        for line in fileids.splitlines():
+            (path, key) = line.split("\t", 2)
+            ret[urllib.unquote(path)] = osutils.safe_file_id(key)
+        return ret
+
+    def _mainline_revision_parent(self, path, revnum, scheme):
+        """Find the mainline parent of the specified revision.
+
+        :param path: Path of the revision in Subversion
+        :param revnum: Subversion revision number
+        :param scheme: Name of branching scheme to use
+        :return: Revision id of the left-hand-side parent or None if 
+                  this is the first revision
+        """
+        assert isinstance(path, str)
         assert isinstance(revnum, int)
 
-        if not self.scheme.is_branch(path) and \
-           not self.scheme.is_tag(path):
-            raise NoSuchRevision(self, self.generate_revision_id(revnum, path))
+        if not scheme.is_branch(path) and \
+           not scheme.is_tag(path):
+            raise NoSuchRevision(self, 
+                    self.generate_revision_id(revnum, path, str(scheme)))
 
-        it = self.follow_branch(path, revnum)
+        it = self.follow_branch(path, revnum, scheme)
         # the first tuple returned should match the one specified. 
         # if it's not, then the branch, revnum didn't change in the specified 
         # revision and so it is invalid
         if (path, revnum) != it.next():
-            raise NoSuchRevision(self, self.generate_revision_id(revnum, path))
+            raise NoSuchRevision(self, 
+                    self.generate_revision_id(revnum, path, str(scheme)))
         try:
             (branch, rev) = it.next()
-            return self.generate_revision_id(rev, branch)
+            return self.generate_revision_id(rev, branch, str(scheme))
         except StopIteration:
             # The specified revision was the first one in the branch
             return None
 
-    def revision_parents(self, revision_id, merged_data=None):
+    def _bzr_merged_revisions(self, branch, revnum, scheme):
+        """Find out what revisions were merged by Bazaar in a revision.
+
+        :param branch: Subversion branch path.
+        :param revnum: Subversion revision number.
+        :param scheme: Branching scheme.
+        """
+        change = self.branchprop_list.get_property_diff(branch, revnum, 
+                                       SVN_PROP_BZR_ANCESTRY+str(scheme)).splitlines()
+        if len(change) == 0:
+            return []
+
+        assert len(change) == 1
+
+        return parse_merge_property(change[0])
+
+    def _svk_feature_to_revision_id(self, scheme, feature):
+        """Convert a SVK feature to a revision id for this repository.
+
+        :param scheme: Branching scheme.
+        :param feature: SVK feature.
+        :return: revision id.
+        """
+        try:
+            (uuid, bp, revnum) = parse_svk_feature(feature)
+        except errors.InvalidPropertyValue:
+            return None
+        if uuid != self.uuid:
+            return None
+        if not scheme.is_branch(bp) and not scheme.is_tag(bp):
+            return None
+        return self.generate_revision_id(revnum, bp, str(scheme))
+
+    def _svk_merged_revisions(self, branch, revnum, scheme):
+        """Find out what SVK features were merged in a revision.
+
+        :param branch: Subversion branch path.
+        :param revnum: Subversion revision number.
+        :param scheme: Branching scheme.
+        """
+        current = set(self.branchprop_list.get_property(branch, revnum, SVN_PROP_SVK_MERGE, "").splitlines())
+        (prev_path, prev_revnum) = self._log.get_previous(branch, revnum)
+        if prev_path is None and prev_revnum == -1:
+            previous = set()
+        else:
+            previous = set(self.branchprop_list.get_property(prev_path.encode("utf-8"), 
+                         prev_revnum, SVN_PROP_SVK_MERGE, "").splitlines())
+        for feature in current.difference(previous):
+            revid = self._svk_feature_to_revision_id(scheme, feature)
+            if revid is not None:
+                yield revid
+
+    def get_parents(self, revids):
+        parents_list = []
+        for revision_id in revids:
+            if revision_id == NULL_REVISION:
+                parents = []
+            else:
+                try:
+                    parents = self.revision_parents(revision_id)
+                except NoSuchRevision:
+                    parents = None
+                else:
+                    if len(parents) == 0:
+                        parents = [NULL_REVISION]
+            parents_list.append(parents)
+        return parents_list
+
+    def revision_parents(self, revision_id, bzr_merges=None, svk_merges=None):
+        """See Repository.revision_parents()."""
         parent_ids = []
-        (branch, revnum) = self.lookup_revision_id(revision_id)
-        mainline_parent = self._mainline_revision_parent(branch, revnum)
+        (branch, revnum, scheme) = self.lookup_revision_id(revision_id)
+        mainline_parent = self._mainline_revision_parent(branch, revnum, scheme)
         if mainline_parent is not None:
             parent_ids.append(mainline_parent)
-            (parent_path, parent_revnum) = self.lookup_revision_id(mainline_parent)
-        else:
-            parent_path = None
 
-        # if the branch didn't change, bzr:merge can't have changed
+        # if the branch didn't change, bzr:merge or svk:merge can't have changed
         if not self._log.touches_path(branch, revnum):
             return parent_ids
        
-        if merged_data is None:
-            new_merge = self.branchprop_list.get_property(branch, revnum, 
-                                           SVN_PROP_BZR_MERGE, "").splitlines()
-
-            if len(new_merge) == 0 or parent_path is None:
-                old_merge = ""
-            else:
-                old_merge = self.branchprop_list.get_property(parent_path, parent_revnum, 
-                        SVN_PROP_BZR_MERGE, "").splitlines()
-
-            assert (len(old_merge) == len(new_merge) or 
-                    len(old_merge) + 1 == len(new_merge))
+        if bzr_merges is None:
+            bzr_merges = self._bzr_merged_revisions(branch, revnum, scheme)
+        if svk_merges is None:
+            svk_merges = self._svk_merged_revisions(branch, revnum, scheme)
 
-            if len(old_merge) < len(new_merge):
-                merged_data = new_merge[-1]
-            else:
-                merged_data = ""
+        parent_ids.extend(bzr_merges)
 
-        if ' ' in merged_data:
-            mutter('invalid revision id %r in merged property, skipping' % merged_data)
-            merged_data = ""
-
-        if merged_data != "":
-            parent_ids.extend(merged_data.split("\t"))
+        if bzr_merges == []:
+            # Commit was doing using svk apparently
+            parent_ids.extend(svk_merges)
 
         return parent_ids
 
     def get_revision(self, revision_id):
         """See Repository.get_revision."""
-        if not revision_id or not isinstance(revision_id, basestring):
+        if not revision_id or not isinstance(revision_id, str):
             raise InvalidRevisionId(revision_id=revision_id, branch=self)
 
-        (path, revnum) = self.lookup_revision_id(revision_id)
+        (path, revnum, _) = self.lookup_revision_id(revision_id)
         
         parent_ids = self.revision_parents(revision_id)
 
         # Commit SVN revision properties to a Revision object
-        rev = Revision(revision_id=revision_id, parent_ids=parent_ids)
+        class LazySvnRevision(Revision):
+            inventory_sha1 = property(lambda rev: self.get_inventory_sha1(rev.revision_id))
+
+        rev = LazySvnRevision(revision_id=revision_id, parent_ids=parent_ids)
 
-        (rev.committer, rev.message, date) = self._log.get_revision_info(revnum)
-        if rev.committer is None:
+        svn_revprops = self.transport.revprop_list(revnum)
+
+        if svn_revprops.has_key(svn.core.SVN_PROP_REVISION_AUTHOR):
+            rev.committer = svn_revprops[svn.core.SVN_PROP_REVISION_AUTHOR]
+        else:
             rev.committer = ""
 
-        if date is not None:
-            rev.timestamp = 1.0 * svn.core.secs_from_timestr(date, None)
+        rev.message = svn_revprops.get(svn.core.SVN_PROP_REVISION_LOG)
+
+        if rev.message:
+            try:
+                rev.message = rev.message.decode("utf-8")
+            except UnicodeDecodeError:
+                pass
+
+        if svn_revprops.has_key(svn.core.SVN_PROP_REVISION_DATE):
+            rev.timestamp = 1.0 * svn.core.secs_from_timestr(svn_revprops[svn.core.SVN_PROP_REVISION_DATE], None)
         else:
             rev.timestamp = 0.0 # FIXME: Obtain repository creation time
         rev.timezone = None
@@ -382,67 +539,74 @@ class SvnRepository(Repository):
                 self.branchprop_list.get_property(path, revnum, 
                      SVN_PROP_BZR_REVISION_INFO, ""), rev)
 
-        rev.inventory_sha1 = property(lambda: self.get_inventory_sha1(revision_id))
-
         return rev
 
     def get_revisions(self, revision_ids):
+        """See Repository.get_revisions()."""
         # TODO: More efficient implementation?
         return map(self.get_revision, revision_ids)
 
     def add_revision(self, rev_id, rev, inv=None, config=None):
         raise NotImplementedError(self.add_revision)
 
-    def fileid_involved_between_revs(self, from_revid, to_revid):
-        raise NotImplementedError(self.fileid_involved_by_set)
-
-    def fileid_involved(self, last_revid=None):
-        raise NotImplementedError(self.fileid_involved)
-
-    def fileids_altered_by_revision_ids(self, revision_ids):
-        raise NotImplementedError(self.fileids_altered_by_revision_ids)
-
-    def fileid_involved_by_set(self, changes):
-        raise NotImplementedError(self.fileid_involved_by_set)
-
-    def generate_revision_id(self, revnum, path):
+    def generate_revision_id(self, revnum, path, scheme):
         """Generate an unambiguous revision id. 
         
         :param revnum: Subversion revision number.
         :param path: Branch path.
+        :param scheme: Branching scheme name
 
         :return: New revision id.
         """
+        assert isinstance(path, str)
+        assert isinstance(revnum, int)
+
         # Look in the cache to see if it already has a revision id
-        revid = self.revmap.lookup_branch_revnum(revnum, path)
+        revid = self.revmap.lookup_branch_revnum(revnum, path, scheme)
         if revid is not None:
             return revid
 
         # Lookup the revision from the bzr:revision-id-vX property
-        revid = self.branchprop_list.get_property_diff(path, revnum, 
-                SVN_PROP_BZR_REVISION_ID).strip("\n")
+        line = self.branchprop_list.get_property_diff(path, revnum, 
+                SVN_PROP_BZR_REVISION_ID+str(scheme)).strip("\n")
         # Or generate it
-        if revid == "":
-            revid = generate_svn_revision_id(self.uuid, revnum, path)
-
-        self.revmap.insert_revid(revid, path, revnum, revnum, "undefined")
+        if line == "":
+            revid = default_mapping.generate_revision_id(
+                        self.uuid, revnum, path, scheme)
+        else:
+            try:
+                (bzr_revno, revid) = parse_revid_property(line)
+                self.revmap.insert_revid(revid, path, revnum, revnum, 
+                        scheme, bzr_revno)
+            except errors.InvalidPropertyValue, e:
+                mutter(str(e))
+                revid = default_mapping.generate_revision_id(self.uuid, 
+                            revnum, path, scheme)
+                self.revmap.insert_revid(revid, path, revnum, revnum, 
+                        scheme)
 
         return revid
 
-    def lookup_revision_id(self, revid):
+    def lookup_revision_id(self, revid, scheme=None):
         """Parse an existing Subversion-based revision id.
 
         :param revid: The revision id.
+        :param scheme: Optional branching scheme to use when searching for 
+                       revisions
         :raises: NoSuchRevision
-        :return: Tuple with branch path and revision number.
+        :return: Tuple with branch path, revision number and scheme.
         """
+        def get_scheme(name):
+            assert isinstance(name, str)
+            return BranchingScheme.find_scheme(name)
 
         # Try a simple parse
         try:
-            (uuid, branch_path, revnum) = parse_svn_revision_id(revid)
+            (uuid, branch_path, revnum, schemen) = default_mapping.parse_revision_id(revid)
             assert isinstance(branch_path, str)
+            assert isinstance(schemen, str)
             if uuid == self.uuid:
-                return (branch_path, revnum)
+                return (branch_path, revnum, get_scheme(schemen))
             # If the UUID doesn't match, this may still be a valid revision
             # id; a revision from another SVN repository may be pushed into 
             # this one.
@@ -454,73 +618,112 @@ class SvnRepository(Repository):
             (branch_path, min_revnum, max_revnum, \
                     scheme) = self.revmap.lookup_revid(revid)
             assert isinstance(branch_path, str)
+            assert isinstance(scheme, str)
             # Entry already complete?
             if min_revnum == max_revnum:
-                return (branch_path, min_revnum)
-        except NoSuchRevision:
+                return (branch_path, min_revnum, get_scheme(scheme))
+        except NoSuchRevision, e:
             # If there is no entry in the map, walk over all branches:
-            for (branch, revno, exists) in self.find_branches():
+            if scheme is None:
+                scheme = self.get_scheme()
+            last_revnum = self.transport.get_latest_revnum()
+            if (last_revnum <= self.revmap.last_revnum_checked(str(scheme))):
+                # All revision ids in this repository for the current 
+                # scheme have already been discovered. No need to 
+                # check again.
+                raise e
+            found = False
+            for (branch, revno, _) in self.find_branchpaths(scheme, 
+                    self.revmap.last_revnum_checked(str(scheme)),
+                    last_revnum):
+                assert isinstance(branch, str)
+                assert isinstance(revno, int)
                 # Look at their bzr:revision-id-vX
-                revids = self.branchprop_list.get_property(branch, revno, 
-                        SVN_PROP_BZR_REVISION_ID, "").splitlines()
+                revids = []
+                try:
+                    for line in self.branchprop_list.get_property(branch, revno, 
+                            SVN_PROP_BZR_REVISION_ID+str(scheme), "").splitlines():
+                        try:
+                            revids.append(parse_revid_property(line))
+                        except errors.InvalidPropertyValue, ie:
+                            mutter(str(ie))
+                except SubversionException, (_, svn.core.SVN_ERR_FS_NOT_DIRECTORY):
+                    continue
 
                 # If there are any new entries that are not yet in the cache, 
                 # add them
-                for r in revids:
-                    self.revmap.insert_revid(r, branch, 0, revno, 
-                            "undefined")
-
-                if revid in revids:
-                    break
+                for (entry_revno, entry_revid) in revids:
+                    if entry_revid == revid:
+                        found = True
+                    self.revmap.insert_revid(entry_revid, branch, 0, revno, 
+                            str(scheme), entry_revno)
                 
+            # We've added all the revision ids for this scheme in the repository,
+            # so no need to check again unless new revisions got added
+            self.revmap.set_last_revnum_checked(str(scheme), last_revnum)
+            if not found:
+                raise e
             (branch_path, min_revnum, max_revnum, scheme) = self.revmap.lookup_revid(revid)
             assert isinstance(branch_path, str)
 
         # Find the branch property between min_revnum and max_revnum that 
         # added revid
-        i = min_revnum
-        for (bp, rev) in self.follow_branch(branch_path, max_revnum):
-            if self.branchprop_list.get_property_diff(bp, rev, SVN_PROP_BZR_REVISION_ID).strip("\n") == revid:
-                self.revmap.insert_revid(revid, bp, rev, rev, scheme)
-                return (bp, rev)
+        for (bp, rev) in self.follow_branch(branch_path, max_revnum, 
+                                            get_scheme(str(scheme))):
+            try:
+                (entry_revno, entry_revid) = parse_revid_property(
+                 self.branchprop_list.get_property_diff(bp, rev, 
+                     SVN_PROP_BZR_REVISION_ID+str(scheme)).strip("\n"))
+            except errors.InvalidPropertyValue:
+                # Don't warn about encountering an invalid property, 
+                # that will already have happened earlier
+                continue
+            if entry_revid == revid:
+                self.revmap.insert_revid(revid, bp, rev, rev, scheme, 
+                                         entry_revno)
+                return (bp, rev, get_scheme(scheme))
 
         raise AssertionError("Revision id %s was added incorrectly" % revid)
 
     def get_inventory_xml(self, revision_id):
+        """See Repository.get_inventory_xml()."""
         return bzrlib.xml5.serializer_v5.write_inventory_to_string(
             self.get_inventory(revision_id))
 
-    """Get the sha1 for the XML representation of an inventory.
-
-    :param revision_id: Revision id of the inventory for which to return the 
-        SHA1.
-    :return: XML string
-    """
     def get_inventory_sha1(self, revision_id):
-        return osutils.sha_string(self.get_inventory_xml(revision_id))
+        """Get the sha1 for the XML representation of an inventory.
+
+        :param revision_id: Revision id of the inventory for which to return 
+         the SHA1.
+        :return: XML string
+        """
 
-    """Return the XML representation of a revision.
+        return osutils.sha_string(self.get_inventory_xml(revision_id))
 
-    :param revision_id: Revision for which to return the XML.
-    :return: XML string
-    """
     def get_revision_xml(self, revision_id):
+        """Return the XML representation of a revision.
+
+        :param revision_id: Revision for which to return the XML.
+        :return: XML string
+        """
         return bzrlib.xml5.serializer_v5.write_revision_to_string(
             self.get_revision(revision_id))
 
-    """Yield all the branches found between the start of history 
-    and a specified revision number.
+    def follow_history(self, revnum, scheme):
+        """Yield all the branches found between the start of history 
+        and a specified revision number.
+
+        :param revnum: Revision number up to which to search.
+        :return: iterator over branches in the range 0..revnum
+        """
+        assert scheme is not None
 
-    :param revnum: Revision number up to which to search.
-    :return: iterator over branches in the range 0..revnum
-    """
-    def follow_history(self, revnum):
         while revnum >= 0:
             yielded_paths = []
             paths = self._log.get_revision_paths(revnum)
             for p in paths:
                 try:
-                    bp = self.scheme.unprefix(p)[0]
+                    bp = scheme.unprefix(p)[0]
                     if not bp in yielded_paths:
                         if not paths.has_key(bp) or paths[bp][0] != 'D':
                             assert revnum > 0 or bp == ""
@@ -530,29 +733,33 @@ class SvnRepository(Repository):
                     pass
             revnum -= 1
 
-    """Follow the history of a branch. Will yield all the 
-    left-hand side ancestors of a specified revision.
+    def follow_branch(self, branch_path, revnum, scheme):
+        """Follow the history of a branch. Will yield all the 
+        left-hand side ancestors of a specified revision.
     
-    :param branch_path: Subversion path to search.
-    :param revnum: Revision number in Subversion to start.
-    :return: iterator over the ancestors
-    """
-    def follow_branch(self, branch_path, revnum):
+        :param branch_path: Subversion path to search.
+        :param revnum: Revision number in Subversion to start.
+        :param scheme: Name of the branching scheme to use
+        :return: iterator over the ancestors
+        """
         assert branch_path is not None
+        assert isinstance(branch_path, str)
         assert isinstance(revnum, int) and revnum >= 0
-        if not self.scheme.is_branch(branch_path) and \
-           not self.scheme.is_tag(branch_path):
-            raise errors.NotSvnBranchPath(branch_path, revnum)
+        assert scheme.is_branch(branch_path) or scheme.is_tag(branch_path)
         branch_path = branch_path.strip("/")
 
         while revnum >= 0:
+            assert revnum > 0 or branch_path == ""
             paths = self._log.get_revision_paths(revnum)
 
             yielded = False
             # If something underneath branch_path changed, there is a 
             # revision there, so yield it.
             for p in paths:
-                if p.startswith(branch_path+"/") or branch_path == "":
+                assert isinstance(p, str)
+                if (p == branch_path or 
+                    p.startswith(branch_path+"/") or 
+                    branch_path == ""):
                     yield (branch_path, revnum)
                     yielded = True
                     break
@@ -569,14 +776,14 @@ class SvnRepository(Repository):
                     yield (branch_path, revnum+1)
                 if paths[branch_path][1] is None:
                     return
-                if not self.scheme.is_branch(paths[branch_path][1]) and \
-                   not self.scheme.is_tag(paths[branch_path][1]):
+                if not scheme.is_branch(paths[branch_path][1]) and \
+                   not scheme.is_tag(paths[branch_path][1]):
                     # FIXME: if copyfrom_path is not a branch path, 
                     # should simulate a reverse "split" of a branch
                     # for now, just make it look like the branch ended here
                     return
                 revnum = paths[branch_path][2]
-                branch_path = paths[branch_path][1]
+                branch_path = paths[branch_path][1].encode("utf-8")
                 continue
             
             # Make sure we get the right location for the next time if 
@@ -584,32 +791,42 @@ class SvnRepository(Repository):
 
             # Path names need to be sorted so the longer paths 
             # override the shorter ones
-            path_names = paths.keys()
-            path_names.sort()
-            for p in path_names:
+            for p in sorted(paths.keys(), reverse=True):
+                if paths[p][0] == 'M':
+                    continue
                 if branch_path.startswith(p+"/"):
-                    assert paths[p][1] is not None and paths[p][0] in ('A', 'R'), "Parent didn't exist yet, but child wasn't added !?"
+                    assert paths[p][0] in ('A', 'R'), "Parent wasn't added"
+                    assert paths[p][1] is not None, \
+                        "Empty parent added, but child wasn't added !?"
 
                     revnum = paths[p][2]
-                    branch_path = paths[p][1] + branch_path[len(p):]
+                    branch_path = paths[p][1].encode("utf-8") + branch_path[len(p):]
+                    break
 
-    """Return all the changes that happened in a branch 
-    between branch_path and revnum. 
+    def follow_branch_history(self, branch_path, revnum, scheme):
+        """Return all the changes that happened in a branch 
+        between branch_path and revnum. 
 
-    :return: iterator that returns tuples with branch path, 
-    changed paths and revision number.
-    """
-    def follow_branch_history(self, branch_path, revnum):
+        :return: iterator that returns tuples with branch path, 
+            changed paths and revision number.
+        """
         assert branch_path is not None
-        if not self.scheme.is_branch(branch_path) and \
-           not self.scheme.is_tag(branch_path):
-            raise errors.NotSvnBranchPath(branch_path, revnum)
+        assert scheme.is_branch(branch_path) or scheme.is_tag(branch_path)
 
         for (bp, paths, revnum) in self._log.follow_path(branch_path, revnum):
-            if (paths.has_key(bp) and 
-                paths[bp][1] is not None and 
-                not self.scheme.is_branch(paths[bp][1]) and
-                not self.scheme.is_tag(paths[bp][1])):
+            assert revnum > 0 or bp == ""
+            assert scheme.is_branch(bp) or scheme.is_tag(bp)
+            # Remove non-bp paths from paths
+            for p in paths.keys():
+                if not p.startswith(bp+"/") and bp != p and bp != "":
+                    del paths[p]
+
+            if paths == {}:
+                continue
+
+            if (paths.has_key(bp) and paths[bp][1] is not None and 
+                not scheme.is_branch(paths[bp][1]) and
+                not scheme.is_tag(paths[bp][1])):
                 # FIXME: if copyfrom_path is not a branch path, 
                 # should simulate a reverse "split" of a branch
                 # for now, just make it look like the branch ended here
@@ -623,50 +840,59 @@ class SvnRepository(Repository):
                      
             yield (bp, paths, revnum)
 
-    """Check whether a signature exists for a particular revision id.
+    def get_config(self):
+        return SvnRepositoryConfig(self.uuid)
 
-    :param revision_id: Revision id for which the signatures should be looked up.
-    :return: False, as no signatures are stored for revisions in Subversion 
-        at the moment.
-    """
     def has_signature_for_revision_id(self, revision_id):
+        """Check whether a signature exists for a particular revision id.
+
+        :param revision_id: Revision id for which the signatures should be looked up.
+        :return: False, as no signatures are stored for revisions in Subversion 
+            at the moment.
+        """
         # TODO: Retrieve from SVN_PROP_BZR_SIGNATURE 
         return False # SVN doesn't store GPG signatures. Perhaps 
                      # store in SVN revision property?
 
-    """Return the signature text for a particular revision.
 
-    :param revision_id: Id of the revision for which to return the signature.
-    :raises NoSuchRevision: Always
-    """
     def get_signature_text(self, revision_id):
+        """Return the signature text for a particular revision.
+
+        :param revision_id: Id of the revision for which to return the 
+                            signature.
+        :raises NoSuchRevision: Always
+        """
         # TODO: Retrieve from SVN_PROP_BZR_SIGNATURE 
         # SVN doesn't store GPG signatures
         raise NoSuchRevision(self, revision_id)
 
-    def _full_revision_graph(self):
+    def _full_revision_graph(self, scheme, _latest_revnum=None):
+        if _latest_revnum is None:
+            _latest_revnum = self.transport.get_latest_revnum()
         graph = {}
-        for (branch, revnum) in self.follow_history(self._latest_revnum):
+        for (branch, revnum) in self.follow_history(_latest_revnum, 
+                                                    scheme):
             mutter('%r, %r' % (branch, revnum))
-            revid = self.generate_revision_id(revnum, branch)
+            revid = self.generate_revision_id(revnum, branch, str(scheme))
             graph[revid] = self.revision_parents(revid)
         return graph
 
     def get_revision_graph(self, revision_id=None):
+        """See Repository.get_revision_graph()."""
         if revision_id == NULL_REVISION:
             return {}
 
         if revision_id is None:
-            return self._full_revision_graph()
+            return self._full_revision_graph(self.get_scheme())
 
-        (path, revnum) = self.lookup_revision_id(revision_id)
+        (path, revnum, scheme) = self.lookup_revision_id(revision_id)
 
         _previous = revision_id
         self._ancestry = {}
         
         if revnum > 0:
-            for (branch, rev) in self.follow_branch(path, revnum - 1):
-                revid = self.generate_revision_id(rev, branch)
+            for (branch, rev) in self.follow_branch(path, revnum - 1, scheme):
+                revid = self.generate_revision_id(rev, branch, str(scheme))
                 self._ancestry[_previous] = [revid]
                 _previous = revid
 
@@ -674,54 +900,105 @@ class SvnRepository(Repository):
 
         return self._ancestry
 
-    def find_branches(self, revnum=None, pb=None):
-        """Find all branches that were changed in the specified revision number.
+    def find_branches(self, using=False):
+        """Find branches underneath this repository.
+
+        This will include branches inside other branches.
+
+        :param using: If True, list only branches using this repository.
+        """
+        # All branches use this repository, so the using argument can be 
+        # ignored.
+        scheme = self.get_scheme()
+
+        existing_branches = [bp for (bp, revnum, _) in 
+                filter(lambda (bp, rev, exists): exists,
+                       self.find_branchpaths(scheme))]
+
+        branches = []
+        for bp in existing_branches:
+            try:
+                branches.append(Branch.open(urlutils.join(self.base, bp)))
+            except NotBranchError: # Skip non-directories
+                pass
+        return branches
+
+    def find_branchpaths(self, scheme, from_revnum=0, to_revnum=None):
+        """Find all branch paths that were changed in the specified revision 
+        range.
 
         :param revnum: Revision to search for branches.
-        :return: iterator that returns tuples with (path, revision number, still exists)
+        :return: iterator that returns tuples with (path, revision number, still exists). The revision number is the revision in which the branch last existed.
         """
-        if revnum is None:
-            revnum = self.transport.get_latest_revnum()
+        assert scheme is not None
+        if to_revnum is None:
+            to_revnum = self.transport.get_latest_revnum()
 
         created_branches = {}
 
-        for i in range(revnum+1):
-            if pb is not None:
-                pb.update("finding branches", i, revnum+1)
-            paths = self._log.get_revision_paths(i)
-            names = paths.keys()
-            names.sort()
-            for p in names:
-                if self.scheme.is_branch(p) or self.scheme.is_tag(p):
-                    if paths[p][0] in ('R', 'D'):
-                        del created_branches[p]
-                        yield (p, i, False)
-
-                    if paths[p][0] in ('A', 'R'): 
-                        created_branches[p] = i
-                elif self.scheme.is_branch_parent(p) or self.scheme.is_tag_parent(p):
-                    if paths[p][0] in ('R', 'D'):
-                        k = created_branches.keys()
-                        for c in k:
-                            if c.startswith(p+"/"):
-                                del created_branches[c] 
-                                yield (c, i, False)
-                    if paths[p][0] in ('A', 'R'):
-                        parents = [p]
-                        while parents:
-                            p = parents.pop()
-                            for c in self.transport.get_dir(p, i)[0].keys():
-                                n = p+"/"+c
-                                if self.scheme.is_branch(n) or self.scheme.is_tag(n):
-                                    created_branches[n] = i
-                                elif self.scheme.is_branch_parent(n) or self.scheme.is_tag_parent(n):
-                                    parents.append(n)
+        ret = []
+
+        pb = ui.ui_factory.nested_progress_bar()
+        try:
+            for i in range(from_revnum, to_revnum+1):
+                pb.update("finding branches", i, to_revnum+1)
+                paths = self._log.get_revision_paths(i)
+                for p in sorted(paths.keys()):
+                    if scheme.is_branch(p) or scheme.is_tag(p):
+                        if paths[p][0] in ('R', 'D') and p in created_branches:
+                            del created_branches[p]
+                            if paths[p][1]:
+                                prev_path = paths[p][1]
+                                prev_rev = paths[p][2]
+                            else:
+                                prev_path = p
+                                prev_rev = self._log.find_latest_change(p, 
+                                    i-1, include_parents=True, 
+                                    include_children=True)
+                            assert isinstance(prev_rev, int)
+                            ret.append((prev_path, prev_rev, False))
+
+                        if paths[p][0] in ('A', 'R'): 
+                            created_branches[p] = i
+                    elif scheme.is_branch_parent(p) or \
+                            scheme.is_tag_parent(p):
+                        if paths[p][0] in ('R', 'D'):
+                            k = created_branches.keys()
+                            for c in k:
+                                if c.startswith(p+"/") and c in created_branches:
+                                    del created_branches[c] 
+                                    j = self._log.find_latest_change(c, i-1, 
+                                            include_parents=True, 
+                                            include_children=True)
+                                    assert isinstance(j, int)
+                                    ret.append((c, j, False))
+                        if paths[p][0] in ('A', 'R'):
+                            parents = [p]
+                            while parents:
+                                p = parents.pop()
+                                try:
+                                    for c in self.transport.get_dir(p, i)[0].keys():
+                                        n = p+"/"+c
+                                        if scheme.is_branch(n) or scheme.is_tag(n):
+                                            created_branches[n] = i
+                                        elif (scheme.is_branch_parent(n) or 
+                                              scheme.is_tag_parent(n)):
+                                            parents.append(n)
+                                except SubversionException, (_, svn.core.SVN_ERR_FS_NOT_DIRECTORY):
+                                    pass
+        finally:
+            pb.finished()
 
         for p in created_branches:
-            j = self._log.find_latest_change(p, revnum, recurse=True)
+            j = self._log.find_latest_change(p, to_revnum, 
+                                             include_parents=True,
+                                             include_children=True)
             if j is None:
                 j = created_branches[p]
-            yield (p, j, True)
+            assert isinstance(j, int)
+            ret.append((p, j, True))
+
+        return ret
 
     def is_shared(self):
         """Return True if this repository is flagged as a shared repository."""
@@ -738,3 +1015,4 @@ class SvnRepository(Repository):
                 timezone, committer, revprops, revision_id)
 
 
+