--- backend-r10657.py	2011-04-15 20:50:23.000000000 +0900
+++ backend.py	2011-04-15 20:46:00.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,205 @@
                      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()
+
+        # -- compare them and try to resync if different
+        next_youngest = None
+        seen = {}
+        for bt_name, youngest in self.repos.repo.branchtags().iteritems():
+            print 'synchronizing branch/tag %s (head %s)...' % (bt_name, self.display_rev(youngest))
+
+            # 1. prepare for resyncing
+            #    (there still might be a race condition at this point)
+
+            next_youngest = youngest
+            kindmap = dict(zip(_kindmap.values(), _kindmap.keys()))
+            actionmap = dict(zip(_actionmap.values(), _actionmap.keys()))
+
+            while next_youngest is not None:
+                srev = self.db_rev(next_youngest)
+                exit = [False]
+                if not seen.has_key(srev):
+                
+                    @self.env.with_transaction()
+                    def do_transaction(db):
+                        cursor = db.cursor()
+                        
+                        # 1.1 Attempt to resync the 'revision' table
+                        self.log.info("Trying to sync revision [%s]",
+                                      next_youngest)
+                        cset = self.repos.get_changeset(next_youngest)
+                        try:
+                            cursor.execute("""
+                                INSERT INTO revision
+                                    (repos,rev,time,author,message)
+                                VALUES (%s,%s,%s,%s,%s)
+                                """, (self.id, srev, to_utimestamp(cset.date),
+                                      cset.author, cset.message))
+                            seen[srev] = True
+                        except Exception, e: # *another* 1.1. resync attempt won 
+                            self.log.warning('Revision %s already cached: %r',
+                                             next_youngest, e)
+                            # also potentially in progress, so keep ''previous''
+                            # notion of 'youngest'
+                            self.repos.clear(youngest_rev=youngest)
+                            # FIXME: This aborts a containing transaction
+                            db.rollback()
+                            exit[0] = True
+                            return
+        
+                        # 1.2. now *only* one process was able to get there
+                        #      (i.e. there *shouldn't* be any race condition here)
+        
+                        for path, kind, action, bpath, brev in cset.get_changes():
+                            self.log.debug("Caching node change in [%s]: %r",
+                                           next_youngest,
+                                           (path, kind, action, bpath, brev))
+                            kind = kindmap[kind]
+                            action = actionmap[action]
+                            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))
+
+                        # FIXME: it is not necessary to update everytime in the loop.
+                        cursor.execute("""
+                            UPDATE repository SET value=%s WHERE id=%s AND name=%s
+                            """, (str(self.repos.changectx().hex()), self.id, CACHE_YOUNGEST_REV))
+
+                if exit[0]:
+                    return
+                
+                # 1.4. iterate (1.1 should always succeed now)
+                youngest = next_youngest
+                next_youngest = self.repos.previous_rev(next_youngest)
+
+                # 1.5. provide some feedback
+                if feedback:
+                    feedback(youngest)
+
+    def get_node(self, path, rev=None):
+        return self.repos.get_node(path, self.normalize_rev(rev))
+
+    def _get_node_revs(self, path, last=None, first=None):
+        """Return the revisions affecting `path` between `first` and `last`
+        revisions.
+        """
+        last = self.normalize_rev(last)
+        slast = self.db_rev(last)
+        node = self.get_node(path, last)    # Check node existence
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        if first is None:
+            cursor.execute("SELECT rev FROM node_change "
+                           "WHERE repos=%s AND rev<=%s "
+                           "  AND path=%s "
+                           "  AND change_type IN ('A', 'C', 'M') "
+                           "ORDER BY rev DESC LIMIT 1",
+                           (self.id, slast, path))
+            first = 0
+            for row in cursor:
+                first = int(row[0])
+        sfirst = self.db_rev(first)
+        cursor.execute("SELECT DISTINCT rev FROM node_change "
+                       "WHERE repos=%%s AND rev>=%%s AND rev<=%%s "
+                       " AND (path=%%s OR path %s)" % db.like(),
+                       (self.id, sfirst, slast, path,
+                        db.like_escape(path + '/') + '%'))
+        return [int(row[0]) for row in cursor]
+
+class MercurialCachedChangeset(CachedChangeset):
+    pass
 
 ### Version Control API
     
@@ -683,30 +888,37 @@
                              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
+        #for p in self.changectx(rev).parents():
+        #    if p:
+        #        return p.hex() # always follow first parent
     
     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
+        
+        #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
    
     def rev_older_than(self, rev1, rev2):
         # FIXME use == and ancestors?
@@ -779,7 +991,12 @@
             if old_node.manifest[old_node.str_path] != \
                    new_node.manifest[new_node.str_path]:
                 yield(old_node, new_node, Node.FILE, Changeset.EDIT)
-            
+
+    def clear(self, youngest_rev=None):
+        self.youngest = None
+        if youngest_rev is not None:
+            self.youngest = self.normalize_rev(youngest_rev)
+        self.oldest = None
 
 class MercurialNode(Node):
     """A path in the repository, at a given revision.
@@ -1189,6 +1406,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]

