Fix bug in revid caching.
[jelmer/subvertpy.git] / logwalker.py
index 04ff1e2cb95a7c2747a51473ee1127223a26ddbd..e85bcbf5fc639b5ef7d8e8818958c05c077ec778 100644 (file)
 # 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
+"""Cache of the Subversion history log."""
 
-from bzrlib.errors import NoSuchRevision, BzrError, NotBranchError
-from bzrlib.progress import ProgressBar, DummyProgress
-from bzrlib.trace import mutter
+from bzrlib.errors import NoSuchRevision
+import bzrlib.ui as ui
 
 import os
 
@@ -26,12 +26,7 @@ import svn.core
 
 import base64
 
-try:
-    import sqlite3
-except ImportError:
-    from pysqlite2 import dbapi2 as sqlite3
-
-shelves = {}
+from cache import sqlite3
 
 def _escape_commit_message(message):
     """Replace xml-incompatible control characters."""
@@ -72,7 +67,7 @@ class LogWalker(object):
 
         self.last_revnum = last_revnum
 
-        self.transport = SvnRaTransport(transport.get_repos_root())
+        self.transport = SvnRaTransport(transport.base)
 
         if cache_db is None:
             self.db = sqlite3.connect(":memory:")
@@ -91,16 +86,18 @@ class LogWalker(object):
         if self.saved_revnum is None:
             self.saved_revnum = 0
 
-    def fetch_revisions(self, to_revnum, pb=None):
+    def fetch_revisions(self, to_revnum):
         """Fetch information about all revisions in the remote repository
         until to_revnum.
 
         :param to_revnum: End of range to fetch information for
-        :param pb: Optional progress bar to use
         """
+        to_revnum = max(self.last_revnum, to_revnum)
+
+        pb = ui.ui_factory.nested_progress_bar()
+
         def rcvr(orig_paths, rev, author, date, message, pool):
             pb.update('fetching svn revision info', rev, to_revnum)
-            paths = {}
             if orig_paths is None:
                 orig_paths = {}
             for p in orig_paths:
@@ -118,23 +115,16 @@ class LogWalker(object):
             self.db.execute("replace into revision (revno, author, date, message) values (?,?,?,?)", (rev, author, date, message))
 
             self.saved_revnum = rev
-
-        to_revnum = max(self.last_revnum, to_revnum)
-
-        # Don't bother for only a few revisions
-        if abs(self.saved_revnum-to_revnum) < 10:
-            pb = DummyProgress()
-        else:
-            pb = ProgressBar()
+            if self.saved_revnum % 1000 == 0:
+                self.db.commit()
 
         pool = Pool()
         try:
             try:
-                mutter('getting log %r:%r' % (self.saved_revnum, to_revnum))
-                self.transport.get_log(["/"], self.saved_revnum, to_revnum, 
+                self.transport.get_log("/", self.saved_revnum, to_revnum, 
                                0, True, True, rcvr, pool)
             finally:
-                pb.clear()
+                pb.finished()
         except SubversionException, (_, num):
             if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
                 raise NoSuchRevision(branch=self, 
@@ -161,26 +151,24 @@ class LogWalker(object):
 
         path = path.strip("/")
 
-        i = revnum
-        while i > 0:
-            revpaths = self.get_revision_paths(i, path)
-            yield (path, revpaths, i)
-
-            if (revpaths.has_key(path) and
-                revpaths[path][0] in ('A', 'R') and
-                revpaths[path][1] is None):
-               # this path didn't exist before this revision
-               return
-
-            if (not path is None and 
-                path in revpaths and 
-                not revpaths[path][1] is None):
-                # In this revision, this path was copied from 
-                # somewhere else
-                i = revpaths[path][2]
-                path = revpaths[path][1]
-            else:
-                i-=1
+        while revnum >= 0:
+            revpaths = self.get_revision_paths(revnum, path)
+
+            if revpaths != {}:
+                yield (path, revpaths, revnum)
+
+            if revpaths.has_key(path):
+                if revpaths[path][1] is None:
+                    if revpaths[path][0] in ('A', 'R'):
+                        # this path didn't exist before this revision
+                        return
+                else:
+                    # In this revision, this path was copied from 
+                    # somewhere else
+                    revnum = revpaths[path][2]
+                    path = revpaths[path][1]
+                    continue
+            revnum -= 1
 
     def get_revision_paths(self, revnum, path=None):
         """Obtain dictionary with all the changes in a particular revision.
@@ -206,35 +194,45 @@ class LogWalker(object):
             paths[p] = (act, cf, cr)
         return paths
 
-    def get_revision_info(self, revnum, pb=None):
+    def get_revision_info(self, revnum):
         """Obtain basic information for a specific revision.
 
         :param revnum: Revision number.
         :returns: Tuple with author, log message and date of the revision.
         """
-        assert revnum >= 1
+        assert revnum >= 0
+        if revnum == 0:
+            return (None, None, None)
         if revnum > self.saved_revnum:
-            self.fetch_revisions(revnum, pb)
+            self.fetch_revisions(revnum)
         (author, message, date) = self.db.execute("select author, message, date from revision where revno="+ str(revnum)).fetchone()
-        if author is None:
-            author = None
-        return (author, _escape_commit_message(base64.b64decode(message)), date)
+        if message is not None:
+            message = _escape_commit_message(base64.b64decode(message))
+        return (author, message, date)
 
-    def find_latest_change(self, path, revnum):
+    def find_latest_change(self, path, revnum, recurse=False):
         """Find latest revision that touched path.
 
         :param path: Path to check for changes
         :param revnum: First revision to check
         """
+        assert isinstance(path, basestring)
+        assert isinstance(revnum, int) and revnum >= 0
         if revnum > self.saved_revnum:
             self.fetch_revisions(revnum)
 
-        row = self.db.execute(
-             "select rev from changed_path where path='%s' and rev <= %d order by rev desc limit 1" % (path.strip("/"), revnum)).fetchone()
+        if recurse:
+            extra = " or path like '%s/%%'" % path.strip("/")
+        else:
+            extra = ""
+        query = "select rev from changed_path where (path='%s' or ('%s' like (path || '/%%') and (action = 'R' or action = 'A'))%s) and rev <= %d order by rev desc limit 1" % (path.strip("/"), path.strip("/"), extra, revnum)
+
+        row = self.db.execute(query).fetchone()
         if row is None and path == "":
             return 0
 
-        assert row is not None, "no latest change for %r:%d" % (path, revnum)
+        if row is None:
+            return None
 
         return row[0]
 
@@ -252,26 +250,58 @@ class LogWalker(object):
 
     def find_children(self, path, revnum):
         """Find all children of path in revnum."""
-        # TODO: Find children by walking history, or use 
-        # cache?
+        path = path.strip("/")
+        if self.transport.check_path(path, revnum) == svn.core.svn_node_file:
+            return []
+        class TreeLister(svn.delta.Editor):
+            def __init__(self, base):
+                self.files = []
+                self.base = base
 
-        try:
-            (dirents, _, _) = self.transport.get_dir(
-                path.lstrip("/").encode('utf8'), revnum, kind=True)
-        except SubversionException, (_, num):
-            if num == svn.core.SVN_ERR_FS_NOT_DIRECTORY:
-                return
-            raise
+            def set_target_revision(self, revnum):
+                pass
+
+            def open_root(self, revnum, baton):
+                return path
+
+            def add_directory(self, path, parent_baton, copyfrom_path, copyfrom_revnum, pool):
+                self.files.append(os.path.join(self.base, path))
+                return path
+
+            def change_dir_prop(self, id, name, value, pool):
+                pass
+
+            def change_file_prop(self, id, name, value, pool):
+                pass
 
-        for p in dirents:
-            yield os.path.join(path, p)
-            # This needs to be != svn.core.svn_node_file because 
-            # some ra backends seem to return negative values for .kind.
-            # This if statement is just an optimization to make use of this 
-            # property when possible.
-            if dirents[p].kind != svn.core.svn_node_file:
-                for c in self.find_children(os.path.join(path, p), revnum):
-                    yield c
+            def add_file(self, path, parent_id, copyfrom_path, copyfrom_revnum, baton):
+                self.files.append(os.path.join(self.base, path))
+                return path
+
+            def close_dir(self, id):
+                pass
+
+            def close_file(self, path, checksum):
+                pass
+
+            def close_edit(self):
+                pass
+
+            def abort_edit(self):
+                pass
+
+            def apply_textdelta(self, file_id, base_checksum):
+                pass
+        pool = Pool()
+        editor = TreeLister(path)
+        edit, baton = svn.delta.make_editor(editor, pool)
+        root_repos = self.transport.get_repos_root()
+        self.transport.reparent(os.path.join(root_repos, path))
+        reporter = self.transport.do_update(
+                        revnum, "", True, edit, baton, pool)
+        reporter.set_path("", revnum, True, None, pool)
+        reporter.finish_report(pool)
+        return editor.files
 
     def get_previous(self, path, revnum):
         """Return path,revnum pair specified pair was derived from.