Release 0.4.3
[jelmer/subvertpy.git] / tree.py
diff --git a/tree.py b/tree.py
index ea9dcff8266818c88d9978b76e797360c886463b..34e0977a54347281dd632112224c6486fee913ef 100644 (file)
--- a/tree.py
+++ b/tree.py
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+"""Access to stored Subversion basis trees."""
 
-from binascii import hexlify
-from bzrlib.bzrdir import BzrDirFormat
-from bzrlib.errors import NotBranchError, NoSuchFile
-from bzrlib.inventory import (Inventory, InventoryDirectory, InventoryFile,
-                              ROOT_ID)
-from bzrlib.lockable_files import TransportLock, LockableFiles
-from bzrlib.lockdir import LockDir
-import bzrlib.osutils as osutils
-from bzrlib.progress import DummyProgress
+from bzrlib.inventory import Inventory
+
+from bzrlib import osutils, urlutils
 from bzrlib.trace import mutter
-from bzrlib.tree import RevisionTree, EmptyTree
+from bzrlib.revisiontree import RevisionTree
 
 import os
+import md5
 from cStringIO import StringIO
+import urllib
 
 import svn.core, svn.wc, svn.delta
-from svn.core import SubversionException, Pool
+from svn.core import Pool
+
+# Deal with Subversion 1.5 and the patched Subversion 1.4 (which are 
+# slightly different).
 
-_global_pool = Pool()
+if hasattr(svn.delta, 'tx_invoke_window_handler'):
+    def apply_txdelta_handler(src_stream, target_stream, pool):
+        assert hasattr(src_stream, 'read')
+        assert hasattr(target_stream, 'write')
+        window_handler, baton = svn.delta.tx_apply(src_stream, target_stream, 
+                                                   None, pool)
 
-def apply_txdelta_handler(src_stream, target_stream):
-    ret = svn.delta.svn_txdelta_apply(
-            src_stream, 
-            target_stream,
-            _global_pool)
+        def wrapper(window):
+            window_handler(window, baton)
 
-    print ret
-    def wrapper(window):
-        ret[1](window)
+        return wrapper
+else:
+    def apply_txdelta_handler(src_stream, target_stream, pool):
+        assert hasattr(src_stream, 'read')
+        assert hasattr(target_stream, 'write')
+        ret = svn.delta.svn_txdelta_apply(src_stream, target_stream, None, pool)
 
-    return wrapper
+        def wrapper(window):
+            svn.delta.invoke_txdelta_window_handler(
+                ret[1], window, ret[2])
+
+        return wrapper
 
 class SvnRevisionTree(RevisionTree):
-     def __init__(self, repository, revision_id, inventory=None):
+    """A tree that existed in a historical Subversion revision."""
+    def __init__(self, repository, revision_id):
         self._repository = repository
         self._revision_id = revision_id
-        (self.branch_path, self.revnum) = repository.parse_revision_id(revision_id)
+        pool = Pool()
+        (self.branch_path, self.revnum, scheme) = repository.lookup_revision_id(revision_id)
         self._inventory = Inventory()
-        self.editor = TreeBuildEditor(self)
+        self.id_map = repository.get_fileid_map(self.revnum, self.branch_path, 
+                                                scheme)
+        self.editor = TreeBuildEditor(self, pool)
         self.file_data = {}
+        editor, baton = svn.delta.make_editor(self.editor, pool)
+        root_repos = repository.transport.get_repos_root()
+        reporter = repository.transport.do_switch(
+                self.revnum, True, 
+                urlutils.join(root_repos, self.branch_path), editor, baton, pool)
+        reporter.set_path("", 0, True, None, pool)
+        reporter.finish_report(pool)
+        pool.destroy()
 
-        editor, baton = svn.delta.make_editor(self.editor)
-
-        mutter('do update: %r, %r' % (self.revnum, self.branch_path))
-        reporter, reporter_baton = svn.ra.do_update(repository.ra, self.revnum, self.branch_path, True, editor, baton)
-
-        svn.ra.reporter2_invoke_set_path(reporter, reporter_baton, "", 0, True, None)
-
-        svn.ra.reporter2_invoke_finish_report(reporter, reporter_baton)
-
-     def get_file_lines(self, file_id):
-        return self.file_data[file_id].splitlines(True)
+    def get_file_lines(self, file_id):
+        return osutils.split_lines(self.file_data[file_id])
 
 
 class TreeBuildEditor(svn.delta.Editor):
-    def __init__(self, tree):
+    """Builds a tree given Subversion tree transform calls."""
+    def __init__(self, tree, pool):
         self.tree = tree
         self.repository = tree._repository
         self.last_revnum = {}
         self.dir_revnum = {}
         self.dir_ignores = {}
+        self.pool = pool
 
     def set_target_revision(self, revnum):
         self.revnum = revnum
 
     def open_root(self, revnum, baton):
-        return ROOT_ID
-
-    def relpath(self, path):
-        bp, rp = self.tree._repository.scheme.unprefix(path)
-        if bp == self.tree.branch_path:
-            return rp
-        return None
-
-    def get_file_id(self, path, revnum):
-        return self.tree._repository.path_to_file_id(revnum, path)
+        file_id, revision_id = self.tree.id_map[""]
+        ie = self.tree._inventory.add_path("", 'directory', file_id)
+        ie.revision = revision_id
+        self.tree._inventory.revision_id = revision_id
+        return file_id
 
     def add_directory(self, path, parent_baton, copyfrom_path, copyfrom_revnum, pool):
-        relpath = self.relpath(path)
-        if relpath is None:
-            return ROOT_ID
-        file_id, revision_id = self.get_file_id(path, self.revnum)
-        ie = self.tree._inventory.add_path(relpath, 'directory', file_id)
-        if ie is None:
-            self.tree._inventory.revision_id = revision_id
-            return ROOT_ID
-
+        path = path.decode("utf-8")
+        file_id, revision_id = self.tree.id_map[path]
+        ie = self.tree._inventory.add_path(path, 'directory', file_id)
         ie.revision = revision_id
         return file_id
 
     def change_dir_prop(self, id, name, value, pool):
+        from repository import (SVN_PROP_BZR_ANCESTRY, 
+                        SVN_PROP_BZR_PREFIX, SVN_PROP_BZR_REVISION_INFO, 
+                        SVN_PROP_BZR_FILEIDS, SVN_PROP_BZR_REVISION_ID,
+                        SVN_PROP_BZR_BRANCHING_SCHEME, SVN_PROP_BZR_MERGE)
+
         if name == svn.core.SVN_PROP_ENTRY_COMMITTED_REV:
             self.dir_revnum[id] = int(value)
         elif name == svn.core.SVN_PROP_IGNORE:
             self.dir_ignores[id] = value
+        elif name.startswith(SVN_PROP_BZR_ANCESTRY):
+            if id != self.tree._inventory.root.file_id:
+                mutter('%r set on non-root dir!' % name)
+                return
+        elif name in (SVN_PROP_BZR_FILEIDS, SVN_PROP_BZR_BRANCHING_SCHEME):
+            if id != self.tree._inventory.root.file_id:
+                mutter('%r set on non-root dir!' % name)
+                return
         elif name in (svn.core.SVN_PROP_ENTRY_COMMITTED_DATE,
                       svn.core.SVN_PROP_ENTRY_LAST_AUTHOR,
                       svn.core.SVN_PROP_ENTRY_LOCK_TOKEN,
                       svn.core.SVN_PROP_ENTRY_UUID,
                       svn.core.SVN_PROP_EXECUTABLE):
             pass
-        else:
-            mutter('unsupported file property %r' % name)
+        elif name.startswith(svn.core.SVN_PROP_WC_PREFIX):
+            pass
+        elif (name == SVN_PROP_BZR_REVISION_INFO or 
+              name.startswith(SVN_PROP_BZR_REVISION_ID)):
+            pass
+        elif name == SVN_PROP_BZR_MERGE:
+            pass
+        elif (name.startswith(svn.core.SVN_PROP_PREFIX) or
+              name.startswith(SVN_PROP_BZR_PREFIX)):
+            mutter('unsupported dir property %r' % name)
 
     def change_file_prop(self, id, name, value, pool):
-        if (name == svn.core.SVN_PROP_EXECUTABLE and 
-            value == svn.core.SVN_PROP_EXECUTABLE_VALUE):
-            self.is_executable = True
-        elif (name == svn.core.SVN_PROP_SPECIAL and 
-            value == svn.core.SVN_PROP_SPECIAL_VALUE):
-            self.is_symlink = True
+        from repository import SVN_PROP_BZR_PREFIX
+
+        if name == svn.core.SVN_PROP_EXECUTABLE:
+            self.is_executable = (value != None)
+        elif name == svn.core.SVN_PROP_SPECIAL:
+            self.is_symlink = (value != None)
         elif name == svn.core.SVN_PROP_ENTRY_COMMITTED_REV:
             self.last_file_rev = int(value)
         elif name in (svn.core.SVN_PROP_ENTRY_COMMITTED_DATE,
@@ -133,10 +157,14 @@ class TreeBuildEditor(svn.delta.Editor):
                       svn.core.SVN_PROP_ENTRY_UUID,
                       svn.core.SVN_PROP_MIME_TYPE):
             pass
-        else:
+        elif name.startswith(svn.core.SVN_PROP_WC_PREFIX):
+            pass
+        elif (name.startswith(svn.core.SVN_PROP_PREFIX) or
+              name.startswith(SVN_PROP_BZR_PREFIX)):
             mutter('unsupported file property %r' % name)
 
     def add_file(self, path, parent_id, copyfrom_path, copyfrom_revnum, baton):
+        path = path.decode("utf-8")
         self.is_symlink = False
         self.is_executable = False
         return path
@@ -146,13 +174,11 @@ class TreeBuildEditor(svn.delta.Editor):
             self.tree._inventory[id].ignores = self.dir_ignores[id]
 
     def close_file(self, path, checksum):
-        relpath = self.relpath(path)
-        if relpath is None:
-            return 
-
-        file_id, revision_id = self.get_file_id(path, self.revnum)
-
-        ie = self.tree._inventory.add_path(relpath, 'file', file_id)
+        file_id, revision_id = self.tree.id_map[path]
+        if self.is_symlink:
+            ie = self.tree._inventory.add_path(path, 'symlink', file_id)
+        else:
+            ie = self.tree._inventory.add_path(path, 'file', file_id)
         ie.revision = revision_id
 
         if self.file_stream:
@@ -161,9 +187,16 @@ class TreeBuildEditor(svn.delta.Editor):
         else:
             file_data = ""
 
+        actual_checksum = md5.new(file_data).hexdigest()
+        assert(checksum is None or checksum == actual_checksum,
+                "checksum mismatch: %r != %r" % (checksum, actual_checksum))
+
         if self.is_symlink:
-            ie.kind = 'symlink'
             ie.symlink_target = file_data[len("link "):]
+            ie.text_sha1 = None
+            ie.text_size = None
+            ie.text_id = None
+            ie.executable = False
         else:
             ie.text_sha1 = osutils.sha_string(file_data)
             ie.text_size = len(file_data)
@@ -180,141 +213,95 @@ class TreeBuildEditor(svn.delta.Editor):
 
     def apply_textdelta(self, file_id, base_checksum):
         self.file_stream = StringIO()
-        return apply_txdelta_handler(StringIO(""), self.file_stream)
-
-
-class SvnInventoryFile(InventoryFile):
-    """Inventory entry that can either be a plain file or a 
-    symbolic link. Avoids fetching data until necessary. """
-    def __init__(self, file_id, name, parent_id, repository, path, revnum, 
-                 has_props):
-        self.repository = repository
-        self.path = path
-        self.has_props = has_props
-        self.revnum = revnum
-        InventoryFile.__init__(self, file_id, name, parent_id)
-
-    def _get_sha1(self):
-        text = self.repository._get_file(self.path, self.revnum).read()
-        return osutils.sha_string(text)
-
-    def _get_executable(self):
-        if not self.has_props:
-            return False
-
-        value = self.repository._get_file_prop(self.path, self.revnum, 
-                    svn.core.SVN_PROP_EXECUTABLE)
-        if value and value == svn.core.SVN_PROP_EXECUTABLE_VALUE:
-            return True
-        return False 
-
-    def _is_special(self):
-        if not self.has_props:
-            return False
-
-        value = self.repository._get_file_prop(self.path, self.revnum, 
-                    svn.core.SVN_PROP_SPECIAL)
-        if value and value == svn.core.SVN_PROP_SPECIAL_VALUE:
-            return True
-        return False 
-
-    def _get_symlink_target(self):
-        if not self._is_special():
-            return None
-        data = self.repository._get_file(self.path, self.revnum).read()
-        if not data.startswith("link "):
-            raise BzrError("Improperly formatted symlink file")
-        return data[len("link "):]
-
-    def _get_kind(self):
-        if self._is_special():
-            return 'symlink'
-        return 'file'
-
-    # FIXME: we need a set function here because of InventoryEntry.__init__
-    def _phony_set(self, data):
-        pass
-   
-    text_sha1 = property(_get_sha1, _phony_set)
-    executable = property(_get_executable, _phony_set)
-    symlink_target = property(_get_symlink_target, _phony_set)
-    kind = property(_get_kind, _phony_set)
-
-
-class SlowSvnRevisionTree(RevisionTree):
-    """Original implementation of SvnRevisionTree.
-    
-    More roundtrip intensive than SvnRevisionTree, but more 
-    efficient on bandwidth usage if the full tree isn't used.
-    """
-    def __init__(self, repository, revision_id, inventory=None):
-        self._repository = repository
-        self._revision_id = revision_id
-        if inventory:
-            self._inventory = inventory
-        else:
-            self._inventory = repository.get_inventory(revision_id)
-        (self._branch_path, self._revnum) = repository.parse_revision_id(revision_id)
-
-        self.fetch_inventory()
-
-    def fetch_inventory(self):
-        mutter('getting inventory %r for branch %r' % (self._revnum, self._branch_path))
-
-        def read_directory(inv, id, path, revnum):
-            (props, dirents) = self._cache_get_dir(path, revnum)
-
-            recurse = {}
+        return apply_txdelta_handler(StringIO(""), self.file_stream, self.pool)
 
-            for child_name in dirents:
-                dirent = dirents[child_name]
-
-                child_path = os.path.join(path, child_name)
-
-                (child_id, revid) = self.path_to_file_id(dirent.created_rev, 
-                    child_path)
-                if dirent.kind == svn.core.svn_node_dir:
-                    inventry = InventoryDirectory(child_id, child_name, id)
-                    recurse[child_path] = dirent.created_rev
-                elif dirent.kind == svn.core.svn_node_file:
-                    inventry = SvnInventoryFile(child_id, child_name, id, self, 
-                        child_path, dirent.created_rev, dirent.has_props)
 
+class SvnBasisTree(RevisionTree):
+    """Optimized version of SvnRevisionTree."""
+    def __init__(self, workingtree):
+        self.workingtree = workingtree
+        self._revision_id = workingtree.branch.generate_revision_id(
+                                      workingtree.base_revnum)
+        self.id_map = workingtree.branch.repository.get_fileid_map(
+                workingtree.base_revnum, 
+                workingtree.branch.get_branch_path(workingtree.base_revnum), 
+                workingtree.branch.scheme)
+        self._inventory = Inventory(root_id=None)
+        self._repository = workingtree.branch.repository
+
+        def add_file_to_inv(relpath, id, revid, wc):
+            props = svn.wc.get_prop_diffs(self.workingtree.abspath(relpath), wc)
+            if isinstance(props, list): # Subversion 1.5
+                props = props[1]
+            if props.has_key(svn.core.SVN_PROP_SPECIAL):
+                ie = self._inventory.add_path(relpath, 'symlink', id)
+                ie.symlink_target = open(self._abspath(relpath)).read()[len("link "):]
+                ie.text_sha1 = None
+                ie.text_size = None
+                ie.text_id = None
+                ie.executable = False
+            else:
+                ie = self._inventory.add_path(relpath, 'file', id)
+                data = osutils.fingerprint_file(open(self._abspath(relpath)))
+                ie.text_sha1 = data['sha1']
+                ie.text_size = data['size']
+                ie.executable = props.has_key(svn.core.SVN_PROP_EXECUTABLE)
+            ie.revision = revid
+            return ie
+
+        def find_ids(entry):
+            relpath = urllib.unquote(entry.url[len(entry.repos):].strip("/"))
+            if entry.schedule in (svn.wc.schedule_normal, 
+                                  svn.wc.schedule_delete, 
+                                  svn.wc.schedule_replace):
+                return self.id_map[workingtree.branch.scheme.unprefix(relpath)[1]]
+            return (None, None)
+
+        def add_dir_to_inv(relpath, wc, parent_id):
+            entries = svn.wc.entries_read(wc, False)
+            entry = entries[""]
+            (id, revid) = find_ids(entry)
+            if id == None:
+                return
+
+            # First handle directory itself
+            ie = self._inventory.add_path(relpath, 'directory', id)
+            ie.revision = revid
+            if relpath == "":
+                self._inventory.revision_id = revid
+
+            for name in entries:
+                if name == "":
+                    continue
+
+                subrelpath = os.path.join(relpath, name)
+
+                entry = entries[name]
+                assert entry
+                
+                if entry.kind == svn.core.svn_node_dir:
+                    subwc = svn.wc.adm_open3(wc, 
+                            self.workingtree.abspath(subrelpath), 
+                                             False, 0, None)
+                    try:
+                        add_dir_to_inv(subrelpath, subwc, id)
+                    finally:
+                        svn.wc.adm_close(subwc)
                 else:
-                    raise BzrError("Unknown entry kind for '%s': %s" % 
-                        (child_path, dirent.kind))
-
-                inventry.revision = revid
-                inv.add(inventry)
-
-            for child_path in recurse:
-                (child_id, _) = self.path_to_file_id(recurse[child_path], 
-                    child_path)
-                read_directory(inv, child_id, child_path, recurse[child_path])
-    
-        inv = Inventory(revision_id=self._revision_id, root_id=ROOT_ID)
-        inv[ROOT_ID].revision = self._revision_id
+                    (subid, subrevid) = find_ids(entry)
+                    if subid is not None:
+                        add_file_to_inv(subrelpath, subid, subrevid, wc)
 
-        assert path != None
-        read_directory(inv, ROOT_ID, self._branch_path, self._revnum)
+        wc = workingtree._get_wc() 
+        try:
+            add_dir_to_inv("", wc, None)
+        finally:
+            svn.wc.adm_close(wc)
 
-        return inv
-
-    def get_file_lines(self, file_id):
-        path = "%s/%s" % (self._branch_path, self.id2path(file_id))
-        stream = self._repository._get_file(path, self._revnum)
-        return stream.readlines()
-
-
-class SvnBasisTree(SvnRevisionTree):
-    """Optimized version of SvnRevisionTree."""
-    def __init__(self, workingtree, revid):
-        super(SvnBasisTree, self).__init__(workingtree.branch.repository,
-                                           revid)
-        self.workingtree = workingtree
+    def _abspath(self, relpath):
+        return svn.wc.get_pristine_copy_path(self.workingtree.abspath(relpath))
 
     def get_file_lines(self, file_id):
-        path = self.id2path(file_id)
-        base_copy = svn.wc.get_pristine_copy_path(self.workingtree.abspath(path))
-        return open(base_copy).readlines()
+        base_copy = self._abspath(self.id2path(file_id))
+        return osutils.split_lines(open(base_copy).read())