--- backend-r10657.py	2011-04-15 20:50:23.000000000 +0900
+++ backend.py	2011-04-16 13:23:29.000000000 +0900
@@ -31,7 +31,11 @@
 from trac.versioncontrol.api import Changeset, Node, Repository, \
                                     IRepositoryConnector, RepositoryManager, \
                                     NoSuchChangeset, NoSuchNode
+from trac.versioncontrol.cache import CachedRepository, CachedChangeset, \
+                                      CACHE_METADATA_KEYS, CACHE_REPOSITORY_DIR, CACHE_YOUNGEST_REV, \
+                                      _kindmap, _actionmap
 from trac.versioncontrol.web_ui import IPropertyRenderer, RenderedProperty
+from trac.util.datefmt import from_utimestamp, to_utimestamp
 from trac.wiki import IWikiSyntaxProvider
 
 # -- plugin i18n
@@ -381,7 +385,9 @@
             self._setup_ui(self.hgrc)
         repos = MercurialRepository(dir, params, self.log, self)
         repos.version_info = self._version_info
-        return repos
+        cached_repos = MercurialCachedRepository(self.env, repos, self.log)
+        cached_repos.has_linear_changesets = False
+        return cached_repos
 
     # IWikiSyntaxProvider methods
     
@@ -438,6 +444,181 @@
                      title=errmsg, rel="nofollow")
 
 
+class MercurialCachedRepository(CachedRepository):
+
+    def display_rev(self, rev):
+        return self.repos.display_rev(rev)
+
+    def normalize_rev(self, rev):
+        if rev is None or isinstance(rev, basestring) and \
+               rev.lower() in ('', 'head', 'latest', 'youngest'):
+            return self.rev_db(self.youngest_rev or nullrev)
+        else:
+            return self.repos.normalize_rev(rev)
+
+    def db_rev(self, rev):
+        return self.repos.short_rev(rev)
+
+    def rev_db(self, rev):
+        return self.repos.normalize_rev(rev)
+
+    def get_changeset(self, rev):
+        return MercurialCachedChangeset(self, self.normalize_rev(rev), self.env)
+
+    def sync(self, feedback=None, clean=False):
+        if clean:
+            self.log.info('Cleaning cache')
+            @self.env.with_transaction()
+            def do_clean(db):
+                cursor = db.cursor()
+                cursor.execute("DELETE FROM revision WHERE repos=%s",
+                               (self.id,))
+                cursor.execute("DELETE FROM node_change WHERE repos=%s",
+                               (self.id,))
+                cursor.executemany("""
+                    DELETE FROM repository WHERE id=%s AND name=%s
+                    """, [(self.id, k) for k in CACHE_METADATA_KEYS])
+                cursor.executemany("""
+                    INSERT INTO repository (id,name,value) VALUES (%s,%s,%s)
+                    """, [(self.id, k, '') for k in CACHE_METADATA_KEYS])
+                del self.metadata
+
+        metadata = self.metadata
+        
+        @self.env.with_transaction()
+        def do_transaction(db):
+            cursor = db.cursor()
+            invalidate = False
+    
+            # -- check that we're populating the cache for the correct
+            #    repository
+            repository_dir = metadata.get(CACHE_REPOSITORY_DIR)
+            if repository_dir:
+                # directory part of the repo name can vary on case insensitive
+                # fs
+                if os.path.normcase(repository_dir) \
+                        != os.path.normcase(self.name):
+                    self.log.info("'repository_dir' has changed from %r to %r",
+                                  repository_dir, self.name)
+                    raise TracError(_("The repository directory has changed, "
+                                      "you should resynchronize the "
+                                      "repository with: trac-admin $ENV "
+                                      "repository resync '%(reponame)s'",
+                                      reponame=self.reponame or '(default)'))
+            elif repository_dir is None: # 
+                self.log.info('Storing initial "repository_dir": %s',
+                              self.name)
+                cursor.execute("""
+                    INSERT INTO repository (id,name,value) VALUES (%s,%s,%s)
+                    """, (self.id, CACHE_REPOSITORY_DIR, self.name))
+                invalidate = True
+            else: # 'repository_dir' cleared by a resync
+                self.log.info('Resetting "repository_dir": %s', self.name)
+                cursor.execute("""
+                    UPDATE repository SET value=%s WHERE id=%s AND name=%s
+                    """, (self.name, self.id, CACHE_REPOSITORY_DIR))
+                invalidate = True
+    
+            # -- insert a 'youngeset_rev' for the repository if necessary
+            if metadata.get(CACHE_YOUNGEST_REV) is None:
+                cursor.execute("""
+                    INSERT INTO repository (id,name,value) VALUES (%s,%s,%s)
+                    """, (self.id, CACHE_YOUNGEST_REV, ''))
+                invalidate = True
+    
+            if invalidate:
+                del self.metadata
+
+        # -- retrieve the youngest revision in the repository and the youngest
+        #    revision cached so far
+        self.repos.clear()
+
+        kindmap = dict(zip(_kindmap.values(), _kindmap.keys()))
+        actionmap = dict(zip(_actionmap.values(), _actionmap.keys()))
+
+        # Retrieve all revisions.
+        repo_revs = set(self.db_rev(rev) for rev in self.repos.repo.changelog)
+        
+        @self.env.with_transaction()
+        def do_transaction(db):
+            cursor = db.cursor()
+
+            # Note: The idea using sets for add_revs/del_revs is borrowed from
+            # https://github.com/maraujop/TracMercurialChangesetPlugin/blob/master/mercurialchangeset/admin.py
+            # The term "nodes" are change to "revs" for consistency of terminology.
+            def _cache_and_get_revision_info(srev):
+                cset = self.repos.get_changeset(self.rev_db(srev))
+
+                # This is the time-consuming part, so we feedback here.
+                for path, kind, action, bpath, brev in cset.get_changes():
+                    kind = kindmap[kind]
+                    action = actionmap[action]
+                    try:
+                        cursor.execute("""
+                            INSERT INTO node_change
+                                (repos,rev,path,node_type,
+                                 change_type,base_path,base_rev)
+                            VALUES (%s,%s,%s,%s,%s,%s,%s)
+                            """, (self.id, srev, path, kind, action, bpath,
+                                  brev))
+                    except Exception, e:
+                        self.log.error('Error %s while inserting into node_change: %s',
+                                       e, (self.id, srev, path, kind, action, bpath, brev))
+                if feedback:
+                    feedback(self.rev_db(srev))
+
+                return (self.id, srev, to_utimestamp(cset.date), cset.author, cset.message)
+
+            # sql_revs: all nodes already synced in revision the table
+            sql_string = """
+                         SELECT rev FROM revision
+                         WHERE repos = %s
+                         """
+            cursor.execute(sql_string, (self.id, ))
+            sql_revs = set(int(row[0]) for row in cursor.fetchall())
+            
+            # add_revs: new revisions to be synced
+            # del_revs: There might be revisions synced that are outdated.
+            add_revs = [ _cache_and_get_revision_info(srev) for srev in repo_revs - sql_revs ]
+            del_revs = [ (self.id, srev) for srev in sql_revs - repo_revs ]
+
+            sql_string = """
+                         INSERT INTO revision (repos, rev, time, author, message)
+                         VALUES (%s, %s, %s, %s, %s)
+                         """
+            # We insert the new revisions' information into Trac's revision table
+            # trac.db.utils.executemany can not be passed an iterator
+            # Constructing a list here slow things down, but it is the only way at the moment
+            cursor.executemany(sql_string, list(add_revs))
+
+            sql_string = """
+                         DELETE FROM revision
+                         WHERE repos = %s AND rev = %s
+                         """
+            cursor.executemany(sql_string, list(del_revs))
+
+            # Update the most recent revision for the browser.
+            cursor.execute("""
+                UPDATE repository SET value=%s WHERE id=%s AND name=%s
+                """, (str(self.repos.changectx().hex()), self.id, CACHE_YOUNGEST_REV))
+            del self.metadata
+
+
+class MercurialCachedChangeset(CachedChangeset):
+
+    hg_properties = [
+        N_("Parents:"), N_("Children:"), N_("Branch:"), N_("Tags:")
+    ]
+
+    def __init__(self, repos, rev, env):
+        super(MercurialCachedChangeset, self).__init__(repos, rev, env)
+        self.ctx = repos.repos.changectx(rev)
+        self.branch = repos.repos.to_u(self.ctx.branch())
+
+    def get_branches(self):
+        """Yield branch names to which this changeset belong."""
+        return self.branch and [(self.branch, 
+                                len(self.ctx.children()) == 0)] or []
 
 ### Version Control API
     
@@ -560,6 +741,7 @@
         try:
             return repo[repo.lookup(self.to_s(rev))].rev()
         except (HgLookupError, RepoError):
+            import pdb; pdb.set_trace()
             raise NoSuchChangeset(rev)
 
     def display_rev(self, rev):
@@ -683,30 +865,20 @@
                              self.changectx(rev))
 
     def get_oldest_rev(self):
-        return 0
+        return nullrev
 
     def get_youngest_rev(self):
         return self.changectx().hex()
     
     def previous_rev(self, rev, path=''): # FIXME: path ignored for now
-        for p in self.changectx(rev).parents():
-            if p:
-                return p.hex() # always follow first parent
+        for parent_rev in self.changectx(rev).ancestors():
+            return parent_rev.hex()
+        return None
     
     def next_rev(self, rev, path=''):
-        ctx = self.changectx(rev)
-        if path: # might be a file
-            fc = filectx(self.repo, self.to_s(path), ctx.node())
-            # Note: the simpler form below raises an HgLookupError for a dir
-            # fc = ctx.filectx(self.to_s(path))
-            if fc: # it is a file
-                for c in fc.children():
-                    return c.hex()
-                else:
-                    return None
-        # it might be a directory (not supported for now) FIXME
-        for c in ctx.children():
-            return c.hex() # always follow first child
+        for following_rev in self.changectx(rev).descendants():
+            return following_rev.hex()
+        return None
    
     def rev_older_than(self, rev1, rev2):
         # FIXME use == and ancestors?
@@ -779,7 +951,7 @@
             if old_node.manifest[old_node.str_path] != \
                    new_node.manifest[new_node.str_path]:
                 yield(old_node, new_node, Node.FILE, Changeset.EDIT)
-            
+
 
 class MercurialNode(Node):
     """A path in the repository, at a given revision.
@@ -1189,6 +1361,7 @@
             # TODO: find a way to detect conflicts and show how they were 
             #       solved (kind of 3-way diff - theirs/mine/merged)
             edits = [p for p in parents if str_file in p.manifest()]
+            edits = edits[:1]
 
             if str_file not in manifest:
                 str_deletions[str_file] = edits[0]

