Edgewall Software

Ticket #8417: backend.py.patch

File backend.py.patch, 13.4 KB (added by me@…, 13 months 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]