Edgewall Software

Ticket #8417: backend.py.patch

File backend.py.patch, 13.4 KB (added by me@…, 12 years ago)

And this is the diff.

  • .py

    old new  
    3131from trac.versioncontrol.api import Changeset, Node, Repository, \
    3232                                    IRepositoryConnector, RepositoryManager, \
    3333                                    NoSuchChangeset, NoSuchNode
     34from trac.versioncontrol.cache import CachedRepository, CachedChangeset, \
     35                                      CACHE_METADATA_KEYS, CACHE_REPOSITORY_DIR, CACHE_YOUNGEST_REV, \
     36                                      _kindmap, _actionmap
    3437from trac.versioncontrol.web_ui import IPropertyRenderer, RenderedProperty
     38from trac.util.datefmt import from_utimestamp, to_utimestamp
    3539from trac.wiki import IWikiSyntaxProvider
    3640
    3741# -- plugin i18n
     
    381385            self._setup_ui(self.hgrc)
    382386        repos = MercurialRepository(dir, params, self.log, self)
    383387        repos.version_info = self._version_info
    384         return repos
     388        cached_repos = MercurialCachedRepository(self.env, repos, self.log)
     389        cached_repos.has_linear_changesets = False
     390        return cached_repos
    385391
    386392    # IWikiSyntaxProvider methods
    387393   
     
    438444                     title=errmsg, rel="nofollow")
    439445
    440446
     447class MercurialCachedRepository(CachedRepository):
     448
     449    def display_rev(self, rev):
     450        return self.repos.display_rev(rev)
     451
     452    def normalize_rev(self, rev):
     453        if rev is None or isinstance(rev, basestring) and \
     454               rev.lower() in ('', 'head', 'latest', 'youngest'):
     455            return self.rev_db(self.youngest_rev or nullrev)
     456        else:
     457            return self.repos.normalize_rev(rev)
     458
     459    def db_rev(self, rev):
     460        return self.repos.short_rev(rev)
     461
     462    def rev_db(self, rev):
     463        return self.repos.normalize_rev(rev)
     464
     465    def get_changeset(self, rev):
     466        return MercurialCachedChangeset(self, self.normalize_rev(rev), self.env)
     467
     468    def sync(self, feedback=None, clean=False):
     469        if clean:
     470            self.log.info('Cleaning cache')
     471            @self.env.with_transaction()
     472            def do_clean(db):
     473                cursor = db.cursor()
     474                cursor.execute("DELETE FROM revision WHERE repos=%s",
     475                               (self.id,))
     476                cursor.execute("DELETE FROM node_change WHERE repos=%s",
     477                               (self.id,))
     478                cursor.executemany("""
     479                    DELETE FROM repository WHERE id=%s AND name=%s
     480                    """, [(self.id, k) for k in CACHE_METADATA_KEYS])
     481                cursor.executemany("""
     482                    INSERT INTO repository (id,name,value) VALUES (%s,%s,%s)
     483                    """, [(self.id, k, '') for k in CACHE_METADATA_KEYS])
     484                del self.metadata
     485
     486        metadata = self.metadata
     487       
     488        @self.env.with_transaction()
     489        def do_transaction(db):
     490            cursor = db.cursor()
     491            invalidate = False
     492   
     493            # -- check that we're populating the cache for the correct
     494            #    repository
     495            repository_dir = metadata.get(CACHE_REPOSITORY_DIR)
     496            if repository_dir:
     497                # directory part of the repo name can vary on case insensitive
     498                # fs
     499                if os.path.normcase(repository_dir) \
     500                        != os.path.normcase(self.name):
     501                    self.log.info("'repository_dir' has changed from %r to %r",
     502                                  repository_dir, self.name)
     503                    raise TracError(_("The repository directory has changed, "
     504                                      "you should resynchronize the "
     505                                      "repository with: trac-admin $ENV "
     506                                      "repository resync '%(reponame)s'",
     507                                      reponame=self.reponame or '(default)'))
     508            elif repository_dir is None: #
     509                self.log.info('Storing initial "repository_dir": %s',
     510                              self.name)
     511                cursor.execute("""
     512                    INSERT INTO repository (id,name,value) VALUES (%s,%s,%s)
     513                    """, (self.id, CACHE_REPOSITORY_DIR, self.name))
     514                invalidate = True
     515            else: # 'repository_dir' cleared by a resync
     516                self.log.info('Resetting "repository_dir": %s', self.name)
     517                cursor.execute("""
     518                    UPDATE repository SET value=%s WHERE id=%s AND name=%s
     519                    """, (self.name, self.id, CACHE_REPOSITORY_DIR))
     520                invalidate = True
     521   
     522            # -- insert a 'youngeset_rev' for the repository if necessary
     523            if metadata.get(CACHE_YOUNGEST_REV) is None:
     524                cursor.execute("""
     525                    INSERT INTO repository (id,name,value) VALUES (%s,%s,%s)
     526                    """, (self.id, CACHE_YOUNGEST_REV, ''))
     527                invalidate = True
     528   
     529            if invalidate:
     530                del self.metadata
     531
     532        # -- retrieve the youngest revision in the repository and the youngest
     533        #    revision cached so far
     534        self.repos.clear()
     535
     536        # -- compare them and try to resync if different
     537        next_youngest = None
     538        seen = {}
     539        for bt_name, youngest in self.repos.repo.branchtags().iteritems():
     540            print 'synchronizing branch/tag %s (head %s)...' % (bt_name, self.display_rev(youngest))
     541
     542            # 1. prepare for resyncing
     543            #    (there still might be a race condition at this point)
     544
     545            next_youngest = youngest
     546            kindmap = dict(zip(_kindmap.values(), _kindmap.keys()))
     547            actionmap = dict(zip(_actionmap.values(), _actionmap.keys()))
     548
     549            while next_youngest is not None:
     550                srev = self.db_rev(next_youngest)
     551                exit = [False]
     552                if not seen.has_key(srev):
     553               
     554                    @self.env.with_transaction()
     555                    def do_transaction(db):
     556                        cursor = db.cursor()
     557                       
     558                        # 1.1 Attempt to resync the 'revision' table
     559                        self.log.info("Trying to sync revision [%s]",
     560                                      next_youngest)
     561                        cset = self.repos.get_changeset(next_youngest)
     562                        try:
     563                            cursor.execute("""
     564                                INSERT INTO revision
     565                                    (repos,rev,time,author,message)
     566                                VALUES (%s,%s,%s,%s,%s)
     567                                """, (self.id, srev, to_utimestamp(cset.date),
     568                                      cset.author, cset.message))
     569                            seen[srev] = True
     570                        except Exception, e: # *another* 1.1. resync attempt won
     571                            self.log.warning('Revision %s already cached: %r',
     572                                             next_youngest, e)
     573                            # also potentially in progress, so keep ''previous''
     574                            # notion of 'youngest'
     575                            self.repos.clear(youngest_rev=youngest)
     576                            # FIXME: This aborts a containing transaction
     577                            db.rollback()
     578                            exit[0] = True
     579                            return
     580       
     581                        # 1.2. now *only* one process was able to get there
     582                        #      (i.e. there *shouldn't* be any race condition here)
     583       
     584                        for path, kind, action, bpath, brev in cset.get_changes():
     585                            self.log.debug("Caching node change in [%s]: %r",
     586                                           next_youngest,
     587                                           (path, kind, action, bpath, brev))
     588                            kind = kindmap[kind]
     589                            action = actionmap[action]
     590                            cursor.execute("""
     591                                INSERT INTO node_change
     592                                    (repos,rev,path,node_type,
     593                                     change_type,base_path,base_rev)
     594                                VALUES (%s,%s,%s,%s,%s,%s,%s)
     595                                """, (self.id, srev, path, kind, action, bpath,
     596                                      brev))
     597
     598                        # FIXME: it is not necessary to update everytime in the loop.
     599                        cursor.execute("""
     600                            UPDATE repository SET value=%s WHERE id=%s AND name=%s
     601                            """, (str(self.repos.changectx().hex()), self.id, CACHE_YOUNGEST_REV))
     602
     603                if exit[0]:
     604                    return
     605               
     606                # 1.4. iterate (1.1 should always succeed now)
     607                youngest = next_youngest
     608                next_youngest = self.repos.previous_rev(next_youngest)
     609
     610                # 1.5. provide some feedback
     611                if feedback:
     612                    feedback(youngest)
     613
     614    def get_node(self, path, rev=None):
     615        return self.repos.get_node(path, self.normalize_rev(rev))
     616
     617    def _get_node_revs(self, path, last=None, first=None):
     618        """Return the revisions affecting `path` between `first` and `last`
     619        revisions.
     620        """
     621        last = self.normalize_rev(last)
     622        slast = self.db_rev(last)
     623        node = self.get_node(path, last)    # Check node existence
     624        db = self.env.get_db_cnx()
     625        cursor = db.cursor()
     626        if first is None:
     627            cursor.execute("SELECT rev FROM node_change "
     628                           "WHERE repos=%s AND rev<=%s "
     629                           "  AND path=%s "
     630                           "  AND change_type IN ('A', 'C', 'M') "
     631                           "ORDER BY rev DESC LIMIT 1",
     632                           (self.id, slast, path))
     633            first = 0
     634            for row in cursor:
     635                first = int(row[0])
     636        sfirst = self.db_rev(first)
     637        cursor.execute("SELECT DISTINCT rev FROM node_change "
     638                       "WHERE repos=%%s AND rev>=%%s AND rev<=%%s "
     639                       " AND (path=%%s OR path %s)" % db.like(),
     640                       (self.id, sfirst, slast, path,
     641                        db.like_escape(path + '/') + '%'))
     642        return [int(row[0]) for row in cursor]
     643
     644class MercurialCachedChangeset(CachedChangeset):
     645    pass
    441646
    442647### Version Control API
    443648   
     
    683888                             self.changectx(rev))
    684889
    685890    def get_oldest_rev(self):
    686         return 0
     891        return nullrev
    687892
    688893    def get_youngest_rev(self):
    689894        return self.changectx().hex()
    690895   
    691896    def previous_rev(self, rev, path=''): # FIXME: path ignored for now
    692         for p in self.changectx(rev).parents():
    693             if p:
    694                 return p.hex() # always follow first parent
     897        for parent_rev in self.changectx(rev).ancestors():
     898            return parent_rev.hex()
     899        return None
     900        #for p in self.changectx(rev).parents():
     901        #    if p:
     902        #        return p.hex() # always follow first parent
    695903   
    696904    def next_rev(self, rev, path=''):
    697         ctx = self.changectx(rev)
    698         if path: # might be a file
    699             fc = filectx(self.repo, self.to_s(path), ctx.node())
    700             # Note: the simpler form below raises an HgLookupError for a dir
    701             # fc = ctx.filectx(self.to_s(path))
    702             if fc: # it is a file
    703                 for c in fc.children():
    704                     return c.hex()
    705                 else:
    706                     return None
    707         # it might be a directory (not supported for now) FIXME
    708         for c in ctx.children():
    709             return c.hex() # always follow first child
     905        for following_rev in self.changectx(rev).descendants():
     906            return following_rev.hex()
     907        return None
     908       
     909        #ctx = self.changectx(rev)
     910        #if path: # might be a file
     911        #    fc = filectx(self.repo, self.to_s(path), ctx.node())
     912        #    # Note: the simpler form below raises an HgLookupError for a dir
     913        #    # fc = ctx.filectx(self.to_s(path))
     914        #    if fc: # it is a file
     915        #        for c in fc.children():
     916        #            return c.hex()
     917        #        else:
     918        #            return None
     919        ## it might be a directory (not supported for now) FIXME
     920        #for c in ctx.children():
     921        #    return c.hex() # always follow first child
    710922   
    711923    def rev_older_than(self, rev1, rev2):
    712924        # FIXME use == and ancestors?
     
    779991            if old_node.manifest[old_node.str_path] != \
    780992                   new_node.manifest[new_node.str_path]:
    781993                yield(old_node, new_node, Node.FILE, Changeset.EDIT)
    782            
     994
     995    def clear(self, youngest_rev=None):
     996        self.youngest = None
     997        if youngest_rev is not None:
     998            self.youngest = self.normalize_rev(youngest_rev)
     999        self.oldest = None
    7831000
    7841001class MercurialNode(Node):
    7851002    """A path in the repository, at a given revision.
     
    11891406            # TODO: find a way to detect conflicts and show how they were
    11901407            #       solved (kind of 3-way diff - theirs/mine/merged)
    11911408            edits = [p for p in parents if str_file in p.manifest()]
     1409            edits = edits[:1]
    11921410
    11931411            if str_file not in manifest:
    11941412                str_deletions[str_file] = edits[0]