Edgewall Software

Ticket #7723: 7723-multirepos-cache-6-r7928.patch

File 7723-multirepos-cache-6-r7928.patch, 36.9 KB (added by rblank, 3 years ago)

Invalidate youngest_rev cache when updating revision cache.

  • trac/admin/console.py

    diff --git a/trac/admin/console.py b/trac/admin/console.py
    a b  
    5050        try: 
    5151            import readline 
    5252            delims = readline.get_completer_delims() 
    53             for c in '-/': 
     53            for c in '-/()': 
    5454                delims = delims.replace(c, '') 
    5555            readline.set_completer_delims(delims) 
    5656        except ImportError: 
  • trac/admin/tests/console-tests.txt

    diff --git a/trac/admin/tests/console-tests.txt b/trac/admin/tests/console-tests.txt
    a b  
    3131priority list        Show possible ticket priorities 
    3232priority order       Move a priority value up or down in the list 
    3333priority remove      Remove a priority value 
     34repository notify    Notify trac about repository events 
     35repository resync    Re-synchronize trac with repositories 
    3436resolution add       Add a resolution value option 
    3537resolution change    Change a resolution value 
    3638resolution list      Show possible ticket resolutions 
    3739resolution order     Move a resolution value up or down in the list 
    3840resolution remove    Remove a resolution value 
    39 resync               Re-synchronize trac with the repository 
    4041severity add         Add a severity value option 
    4142severity change      Change a severity value 
    4243severity list        Show possible ticket severities 
  • trac/db_default.py

    diff --git a/trac/db_default.py b/trac/db_default.py
    a b  
    1717from trac.db import Table, Column, Index 
    1818 
    1919# Database version identifier. Used for automatic upgrades. 
    20 db_version = 21 
     20db_version = 22 
    2121 
    2222def __mkreports(reports): 
    2323    """Utility function used to create report data in same syntax as the 
     
    8282        Index(['time'])], 
    8383 
    8484    # Version control cache 
    85     Table('revision', key='rev')[ 
     85    Table('repository', key=('id', 'name'))[ 
     86        Column('id'), 
     87        Column('name'), 
     88        Column('value')], 
     89    Table('revision', key=('repos', 'rev'))[ 
     90        Column('repos'), 
    8691        Column('rev'), 
    8792        Column('time', type='int'), 
    8893        Column('author'), 
    8994        Column('message'), 
    90         Index(['time'])], 
    91     Table('node_change', key=('rev', 'path', 'change_type'))[ 
     95        Index(['repos', 'time'])], 
     96    Table('node_change', key=('repos', 'rev', 'path', 'change_type'))[ 
     97        Column('repos'), 
    9298        Column('rev'), 
    9399        Column('path'), 
    94100        Column('node_type', size=1), 
    95101        Column('change_type', size=1), 
    96102        Column('base_path'), 
    97103        Column('base_rev'), 
    98         Index(['rev'])], 
     104        Index(['repos', 'rev'])], 
    99105 
    100106    # Ticket system 
    101107    Table('ticket', key='id')[ 
     
    384390           ('system', 
    385391             ('name', 'value'), 
    386392               (('database_version', str(db_version)), 
    387                 ('initial_database_version', str(db_version)), 
    388                 ('youngest_rev', ''))), 
     393                ('initial_database_version', str(db_version)))), 
    389394           ('report', 
    390395             ('author', 'title', 'query', 'description'), 
    391396               __mkreports(get_reports(db)))) 
  • new file trac/upgrades/db22.py

    diff --git a/trac/upgrades/db22.py b/trac/upgrades/db22.py
    new file mode 100644
    - +  
     1from trac.db import Table, Column, Index, DatabaseManager 
     2 
     3def do_upgrade(env, ver, cursor): 
     4    # Make changeset cache multi-repository aware 
     5    cursor.execute("CREATE TEMPORARY TABLE rev_old " 
     6                   "AS SELECT * FROM revision") 
     7    cursor.execute("DROP TABLE revision") 
     8    cursor.execute("CREATE TEMPORARY TABLE nc_old " 
     9                   "AS SELECT * FROM node_change") 
     10    cursor.execute("DROP TABLE node_change") 
     11     
     12    tables = [Table('repository', key=('id', 'name'))[ 
     13                Column('id'), 
     14                Column('name'), 
     15                Column('value')], 
     16              Table('revision', key=('repos', 'rev'))[ 
     17                Column('repos'), 
     18                Column('rev'), 
     19                Column('time', type='int'), 
     20                Column('author'), 
     21                Column('message'), 
     22                Index(['repos', 'time'])], 
     23              Table('node_change', key=('repos', 'rev', 'path', 'change_type'))[ 
     24                Column('repos'), 
     25                Column('rev'), 
     26                Column('path'), 
     27                Column('node_type', size=1), 
     28                Column('change_type', size=1), 
     29                Column('base_path'), 
     30                Column('base_rev'), 
     31                Index(['repos', 'rev'])]] 
     32     
     33    db_connector, _ = DatabaseManager(env)._get_connector() 
     34    for table in tables: 
     35        for stmt in db_connector.to_sql(table): 
     36            cursor.execute(stmt) 
     37     
     38    cursor.execute("INSERT INTO revision (repos,rev,time,author,message) " 
     39                   "SELECT '',rev,time,author,message FROM rev_old") 
     40    cursor.execute("DROP TABLE rev_old") 
     41    cursor.execute("INSERT INTO node_change (repos,rev,path,node_type," 
     42                   "change_type,base_path,base_rev) " 
     43                   "SELECT '',rev,path,node_type,change_type,base_path," 
     44                   "base_rev FROM nc_old") 
     45    cursor.execute("DROP TABLE nc_old") 
     46     
     47    cursor.execute("INSERT INTO repository (id,name,value) " 
     48                   "SELECT '',name,value FROM system " 
     49                   "WHERE name IN ('repository_dir', 'youngest_rev')") 
     50    cursor.execute("DELETE FROM system " 
     51                   "WHERE name IN ('repository_dir', 'youngest_rev')") 
  • trac/versioncontrol/admin.py

    diff --git a/trac/versioncontrol/admin.py b/trac/versioncontrol/admin.py
    a b  
    1515 
    1616from trac.admin import IAdminCommandProvider 
    1717from trac.core import * 
    18 from trac.util.text import printout 
     18from trac.util.text import printerr, printout 
    1919from trac.util.translation import _, ngettext 
     20from trac.versioncontrol import IRepositoryChangeListener, RepositoryManager 
    2021 
    2122 
    2223class VersionControlAdmin(Component): 
     
    2728    # IAdminCommandProvider methods 
    2829     
    2930    def get_admin_commands(self): 
    30         yield ('resync', '[rev]', 
    31                """Re-synchronize trac with the repository 
     31        yield ('repository notify', '<event> <repos> <rev> [rev] [...]', 
     32               """Notify trac about repository events 
     33                
     34               The event "changeset_added" notifies that new changesets have 
     35               been added to a repository. 
     36                
     37               The event "changeset_modified" notifies that existing changesets 
     38               have been modified in a repository. 
     39               """, 
     40               self._complete_notify, self._do_notify) 
     41        yield ('repository resync', '<repos> [rev]', 
     42               """Re-synchronize trac with repositories 
    3243                
    3344               When [rev] is specified, only that revision is synchronized. 
    3445               Otherwise, the complete revision history is synchronized. Note 
    3546               that this operation can take a long time to complete. 
     47                
     48               To synchronize all repositories, specify "*" as the repository. 
    3649               """, 
    37                None, self._do_resync) 
     50               self._complete_repos, self._do_resync) 
    3851     
    39     def _do_resync(self, rev=None): 
    40         if rev: 
    41             self.env.get_repository().sync_changeset(rev) 
    42             printout(_('%(rev)s resynced.', rev=rev)) 
    43             return 
     52    _notify_events = [each for each in IRepositoryChangeListener.__dict__ 
     53                      if not each.startswith('_')] 
     54     
     55    def _complete_notify(self, args): 
     56        if len(args) == 1: 
     57            return self._notify_events 
     58        elif len(args) == 2: 
     59            rm = RepositoryManager(self.env) 
     60            return [reponame or '(default)' for reponame 
     61                    in rm.get_all_repositories()] 
     62     
     63    def _complete_repos(self, args): 
     64        if len(args) == 1: 
     65            rm = RepositoryManager(self.env) 
     66            return [reponame or '(default)' for reponame 
     67                    in rm.get_all_repositories()] 
     68     
     69    def _do_notify(self, event, reponame, *revs): 
     70        if event not in self._notify_events: 
     71            raise TracError(_('Unknown notify event "%s"' % event)) 
     72        rm = RepositoryManager(self.env) 
     73        rm.notify(event, reponame, revs, None) 
     74     
     75    def _do_resync(self, reponame, rev=None): 
     76        rm = RepositoryManager(self.env) 
     77        if reponame == '*': 
     78            if rev is not None: 
     79                raise TracError(_('Cannot synchronize a single revision ' 
     80                                  'on multiple repositories')) 
     81            repositories = rm.get_real_repositories(None) 
     82        else: 
     83            if reponame == '(default)': 
     84                reponame = '' 
     85            repos = rm.get_repository(reponame, None) 
     86            if repos is None: 
     87                raise TracError(_("Unknown repository '%(reponame)s'", 
     88                                  reponame=reponame or '(default)')) 
     89            if rev is not None: 
     90                repos.sync_changeset(rev) 
     91                printout(_('%(rev)s resynced on %(reponame)s.', rev=rev, 
     92                           reponame=repos.reponame or '(default)')) 
     93                return 
     94            repositories = [repos] 
     95         
    4496        from trac.versioncontrol.cache import CACHE_METADATA_KEYS 
    45         printout(_('Resyncing repository history... ')) 
    4697        db = self.env.get_db_cnx() 
    4798        cursor = db.cursor() 
    48         cursor.execute("DELETE FROM revision") 
    49         cursor.execute("DELETE FROM node_change") 
    50         cursor.executemany("DELETE FROM system WHERE name=%s", 
    51                            [(k,) for k in CACHE_METADATA_KEYS]) 
    52         cursor.executemany("INSERT INTO system (name, value) VALUES (%s, %s)", 
    53                            [(k, '') for k in CACHE_METADATA_KEYS]) 
    54         db.commit() 
    55         repos = self.env.get_repository().sync(self._resync_feedback) 
    56         cursor.execute("SELECT count(rev) FROM revision") 
    57         for cnt, in cursor: 
    58             printout(ngettext('%(num)s revision cached.', 
    59                               '%(num)s revisions cached.', num=cnt)) 
     99        inval = False 
     100        for repos in sorted(repositories, key=lambda r: r.reponame): 
     101            reponame = repos.reponame 
     102            printout(_('Resyncing repository history for %(reponame)s... ', 
     103                       reponame=reponame or '(default)')) 
     104            cursor.execute("DELETE FROM revision WHERE repos=%s", (reponame,)) 
     105            cursor.execute("DELETE FROM node_change " 
     106                           "WHERE repos=%s", (reponame,)) 
     107            cursor.executemany("DELETE FROM repository " 
     108                               "WHERE id=%s AND name=%s", 
     109                               [(reponame, k) for k in CACHE_METADATA_KEYS]) 
     110            cursor.executemany("INSERT INTO repository (id, name, value) " 
     111                               "VALUES (%s, %s, %s)", [(reponame, k, '')  
     112                                                for k in CACHE_METADATA_KEYS]) 
     113            db.commit() 
     114            if repos.sync(self._resync_feedback): 
     115                inval = True 
     116            cursor.execute("SELECT count(rev) FROM revision WHERE repos=%s", 
     117                           (reponame,)) 
     118            for cnt, in cursor: 
     119                printout(ngettext('%(num)s revision cached.', 
     120                                  '%(num)s revisions cached.', num=cnt)) 
     121        if inval: 
     122            self.config.touch()     # FIXME: Brute force 
    60123        printout(_('Done.')) 
    61124 
    62125    def _resync_feedback(self, rev): 
  • trac/versioncontrol/api.py

    diff --git a/trac/versioncontrol/api.py b/trac/versioncontrol/api.py
    a b  
    5555        """Return a Repository instance for the given repository type and dir. 
    5656        """ 
    5757 
     58 
    5859class IRepositoryProvider(Interface): 
    5960    """Provide known named instances of Repository.""" 
    6061 
     
    7374        """ 
    7475 
    7576 
     77class IRepositoryChangeListener(Interface): 
     78    """Listen for changes in repositories.""" 
     79     
     80    def changeset_added(self, repos, changeset): 
     81        """Called after a changeset has been added to a repository.""" 
     82 
     83    def changeset_modified(self, repos, changeset): 
     84        """Called after a changeset has been modified in a repository.""" 
     85 
     86 
    7687class RepositoryManager(Component): 
    77     """Component registering the supported version control systems, 
     88    """Component registering the supported version control systems. 
    7889 
    7990    It provides easy access to the configured implementation. 
    8091    """ 
     
    8394 
    8495    connectors = ExtensionPoint(IRepositoryConnector) 
    8596    providers = ExtensionPoint(IRepositoryProvider) 
     97    change_listeners = ExtensionPoint(IRepositoryChangeListener) 
    8698 
    8799    repository_type = Option('trac', 'repository_type', 'svn', 
    88100        """Default repository connector type. (''since 0.10'')""") 
     
    103115        if handler is not Chrome(self.env): 
    104116            try: 
    105117                # FIXME: only sync the default repository - this is bogus 
    106                 self.get_repository('', req.authname).sync() 
     118                if self.get_repository('', req.authname).sync(): 
     119                    self.config.touch()     # FIXME: Brute force 
    107120            except TracError, e: 
    108121                add_warning(req, _("Can't synchronize with the repository " 
    109122                              "(%(error)s). Look in the Trac log for more " 
     
    184197    # Public API methods 
    185198 
    186199    def get_repository(self, reponame, authname): 
    187         """Retrieve the appropriate Repository for the given name 
     200        """Retrieve the appropriate Repository for the given name. 
    188201 
    189202           :param reponame: the key for specifying the repository. 
    190203                            If no name is given, take the the default  
     
    195208        """ 
    196209        repoinfo = self.get_all_repositories().get(reponame, {}) 
    197210        if repoinfo and 'alias' in repoinfo: 
    198             repoinfo = self.get_all_repositories().get(repoinfo['alias']) 
     211            reponame = repoinfo['alias'] 
     212            repoinfo = self.get_all_repositories().get(reponame) 
    199213        if repoinfo: 
    200214            rdir = repoinfo.get('dir') 
    201215            rtype = repoinfo.get('type', self.repository_type) 
     
    251265        return (reponame, self.get_repository(reponame, authname), path or '/') 
    252266 
    253267    def get_default_repository(self, context): 
    254         """Recover the appropriatet repository from the current context. 
     268        """Recover the appropriate repository from the current context. 
    255269 
    256270        Lookup the closest source or changeset resource in the context  
    257271        hierarchy and return the name of its associated repository. 
     
    273287                    else: 
    274288                        self._all_repositories[reponame] = info 
    275289        return self._all_repositories 
     290     
     291    def get_real_repositories(self, authname): 
     292        """Return a list of all real repositories (i.e. excluding aliases).""" 
     293        repositories = set() 
     294        for reponame in self.get_all_repositories(): 
     295            try: 
     296                repos = self.get_repository(reponame, authname) 
     297                if repos is not None: 
     298                    repositories.add(repos) 
     299            except TracError: 
     300                "Skip invalid repositories" 
     301        return repositories 
    276302 
     303    def notify(self, event, reponame, revs, authname): 
     304        """Notify repositories and change listeners about repository events. 
     305         
     306        The supported events are the names of the methods defined in the 
     307        `IRepositoryChangeListener` interface. 
     308        """ 
     309        self.log.debug('Event %s on %s for changesets %r' 
     310                       % (event, reponame, revs)) 
     311         
     312        # Notify a repository by name, and all repositories with the same 
     313        # base, or all repositories by base 
     314        repos = self.get_repository(reponame, None) 
     315        if repos: 
     316            base = repos.get_base() 
     317        else: 
     318            base = reponame 
     319        repositories = [each for each in self.get_real_repositories(authname) 
     320                        if each.get_base() == base] 
     321        inval = False 
     322        for repos in sorted(repositories, key=lambda r: r.reponame): 
     323            if repos.sync(): 
     324                inval = True 
     325            for rev in revs: 
     326                if event == 'changeset_modified': 
     327                    repos.sync_changeset(rev) 
     328                try: 
     329                    changeset = repos.get_changeset(rev) 
     330                except NoSuchChangeset: 
     331                    continue 
     332                inval = inval or (event == 'changeset_modified') 
     333                self.log.debug('Event %s on %s for revision %s' 
     334                               % (event, repos.reponame, rev)) 
     335                for listener in self.change_listeners: 
     336                    getattr(listener, event)(repos, changeset) 
     337         
     338        if inval: 
     339            self.log.debug('Invalidating youngest_rev cache') 
     340            self.config.touch()     # FIXME: Brute force method 
     341     
    277342    def shutdown(self, tid=None): 
    278343        if tid: 
    279344            assert tid == threading._get_ident() 
     
    345410        """Close the connection to the repository.""" 
    346411        raise NotImplementedError 
    347412 
     413    def get_base(self): 
     414        """Return the name of the base repository for this repository. 
     415         
     416        This function returns the name of the base repository to which scoped 
     417        repositories belong. For non-scoped repositories, it returns the  
     418        repository name. 
     419        """ 
     420        return self.name 
     421         
    348422    def clear(self, youngest_rev=None): 
    349423        """Clear any data that may have been cached in instance properties. 
    350424 
     
    360434        The backend will call this function for each `rev` it decided to 
    361435        synchronize, once the synchronization changes are committed to the  
    362436        cache. 
     437         
     438        Return True if any changes have been committed to the cache. 
    363439        """ 
    364         pass 
     440        return False 
    365441 
    366442    def sync_changeset(self, rev): 
    367443        """Resync the repository cache for the given `rev`, if relevant.""" 
    368         raise NotImplementedError 
     444        pass 
    369445 
    370446    def get_quickjump_entries(self, rev): 
    371447        """Generate a list of interesting places in the repository. 
     
    378454        return [] 
    379455     
    380456    def get_changeset(self, rev): 
    381         """Retrieve a Changeset corresponding to the  given revision `rev`.""" 
     457        """Retrieve a Changeset corresponding to the given revision `rev`.""" 
    382458        raise NotImplementedError 
    383459 
    384460    def get_changesets(self, start, stop): 
     
    457533        return row and row[0] or None 
    458534 
    459535    def get_path_history(self, path, rev=None, limit=None): 
    460         """Retrieve all the revisions containing this path 
     536        """Retrieve all the revisions containing this path. 
    461537 
    462538        If given, `rev` is used as a starting point (i.e. no revision 
    463539        ''newer'' than `rev` should be returned). 
     
    636712        return [] 
    637713         
    638714    def get_changes(self): 
    639         """Generator that produces a tuple for every change in the changeset 
     715        """Generator that produces a tuple for every change in the changeset. 
    640716 
    641717        The tuple will contain `(path, kind, change, base_path, base_rev)`, 
    642718        where `change` can be one of Changeset.ADD, Changeset.COPY, 
  • trac/versioncontrol/cache.py

    diff --git a/trac/versioncontrol/cache.py b/trac/versioncontrol/cache.py
    a b  
    4141    has_linear_changesets = False 
    4242 
    4343    def __init__(self, getdb, repos, authz, log): 
     44        self.repos = repos 
    4445        Repository.__init__(self, repos.name, authz, log) 
    4546        if callable(getdb): 
    4647            self.getdb = getdb 
    4748        else: 
    4849            self.getdb = lambda: getdb 
    49         self.repos = repos 
    5050 
     51    def _set_reponame(self, value): 
     52        self.repos.reponame = value 
     53     
     54    reponame = property(fget=lambda self: self.repos.reponame, 
     55                        fset=_set_reponame) 
     56     
    5157    def close(self): 
    5258        self.repos.close() 
    5359 
     60    def get_base(self): 
     61        return self.repos.get_base() 
     62         
    5463    def get_quickjump_entries(self, rev): 
    5564        for category, name, path, rev in self.repos.get_quickjump_entries(rev): 
    5665            yield category, name, path, rev 
     
    6372        db = self.getdb() 
    6473        cursor = db.cursor() 
    6574        cursor.execute("SELECT rev FROM revision " 
    66                        "WHERE time >= %s AND time < %s " 
     75                       "WHERE repos=%s AND time >= %s AND time < %s" 
    6776                       "ORDER BY time DESC, rev DESC", 
    68                        (to_timestamp(start), to_timestamp(stop))) 
     77                       (self.reponame, to_timestamp(start), 
     78                        to_timestamp(stop))) 
    6979        for rev, in cursor: 
    7080            try: 
    7181                if self.authz.has_permission_for_changeset(rev): 
     
    7888        db = self.getdb() 
    7989        cursor = db.cursor() 
    8090        cursor.execute("UPDATE revision SET time=%s, author=%s, message=%s " 
    81                        "WHERE rev=%s", (to_timestamp(cset.date), 
    82                                        cset.author, cset.message, 
    83                                         (str(cset.rev)))) 
     91                       "WHERE repos=%s AND rev=%s", 
     92                       (to_timestamp(cset.date), cset.author, cset.message, 
     93                        self.reponame, str(cset.rev))) 
    8494        db.commit() 
    8595         
    8696    def sync(self, feedback=None): 
    8797        db = self.getdb() 
    8898        cursor = db.cursor() 
    89         cursor.execute("SELECT name, value FROM system WHERE name IN (%s)" % 
    90                        ','.join(["'%s'" % key for key in CACHE_METADATA_KEYS])) 
     99        cursor.execute("SELECT name, value FROM repository " 
     100                       "WHERE id=%%s AND name IN (%s)" %  
     101                       ','.join(['%s'] * len(CACHE_METADATA_KEYS)), 
     102                       (self.reponame,) + CACHE_METADATA_KEYS) 
    91103        metadata = {} 
    92104        for name, value in cursor: 
    93105            metadata[name] = value 
     
    103115                                  "'trac-admin resync' operation is needed.")) 
    104116        elif repository_dir is None: #  
    105117            self.log.info('Storing initial "repository_dir": %s' % self.name) 
    106             cursor.execute("INSERT INTO system (name,value) VALUES (%s,%s)", 
    107                            (CACHE_REPOSITORY_DIR, self.name,)) 
     118            cursor.execute("INSERT INTO repository (id,name,value) " 
     119                           "VALUES (%s,%s,%s)", 
     120                           (self.reponame, CACHE_REPOSITORY_DIR, self.name)) 
    108121        else: # 'repository_dir' cleared by a resync 
    109122            self.log.info('Resetting "repository_dir": %s' % self.name) 
    110             cursor.execute("UPDATE system SET value=%s WHERE name=%s", 
    111                            (self.name, CACHE_REPOSITORY_DIR)) 
    112  
    113         db.commit() # save metadata changes made up to now 
     123            cursor.execute("UPDATE repository SET value=%s " 
     124                           "WHERE id=%s AND name=%s", 
     125                           (self.name, self.reponame, CACHE_REPOSITORY_DIR)) 
    114126 
    115127        # -- retrieve the youngest revision in the repository 
    116128        self.repos.clear() 
    117129        repos_youngest = self.repos.youngest_rev 
    118130 
    119131        # -- retrieve the youngest revision cached so far 
    120         if CACHE_YOUNGEST_REV not in metadata: 
    121             raise TracError(_('Missing "youngest_rev" in cache metadata')) 
    122          
    123         self.youngest = metadata[CACHE_YOUNGEST_REV] 
     132        self.youngest = metadata.get(CACHE_YOUNGEST_REV) 
     133        if self.youngest is None: 
     134            cursor.execute("INSERT INTO repository (id,name,value) " 
     135                           "VALUES (%s,%s,%s)", 
     136                           (self.reponame, CACHE_YOUNGEST_REV, '')) 
     137 
     138        db.commit() # save metadata changes made up to now 
    124139 
    125140        if self.youngest: 
    126141            self.youngest = self.repos.normalize_rev(self.youngest) 
     
    150165                                    find_initial_rev=True) 
    151166                    next_youngest = self.repos.normalize_rev(next_youngest) 
    152167                except TracError: 
    153                     return # can't normalize oldest_rev: repository was empty 
     168                    # can't normalize oldest_rev: repository was empty 
     169                    return False 
    154170 
    155171            if next_youngest is None: # nothing to cache yet 
    156                 return 
     172                return False 
    157173 
    158174            # 0. first check if there's no (obvious) resync in progress 
    159             cursor.execute("SELECT rev FROM revision WHERE rev=%s", 
    160                            (str(next_youngest),)) 
     175            cursor.execute("SELECT rev FROM revision " 
     176                           "WHERE repos=%s AND rev=%s", 
     177                           (self.reponame, str(next_youngest))) 
    161178            for rev, in cursor: 
    162179                # already there, but in progress, so keep ''previous'' 
    163180                # notion of 'youngest' 
    164181                self.repos.clear(youngest_rev=self.youngest) 
    165                 return 
     182                return False 
    166183 
    167184            # 1. prepare for resyncing 
    168185            #    (there still might be a race condition at this point) 
     
    182199                    cset = self.repos.get_changeset(next_youngest) 
    183200                    try: 
    184201                        cursor.execute("INSERT INTO revision " 
    185                                        " (rev,time,author,message) " 
    186                                        "VALUES (%s,%s,%s,%s)", 
    187                                        (str(next_youngest), 
     202                                       " (repos,rev,time,author,message) " 
     203                                       "VALUES (%s,%s,%s,%s,%s)", 
     204                                       (self.reponame, str(next_youngest), 
    188205                                        to_timestamp(cset.date), 
    189206                                        cset.author, cset.message)) 
    190207                    except Exception, e: # *another* 1.1. resync attempt won  
     
    194211                        # notion of 'youngest' 
    195212                        self.repos.clear(youngest_rev=self.youngest) 
    196213                        db.rollback() 
    197                         return 
     214                        return False 
    198215 
    199216                    # 1.2. now *only* one process was able to get there 
    200217                    #      (i.e. there *shouldn't* be any race condition here) 
     
    206223                        kind = kindmap[kind] 
    207224                        action = actionmap[action] 
    208225                        cursor.execute("INSERT INTO node_change " 
    209                                        " (rev,path,node_type,change_type, " 
    210                                        "  base_path,base_rev) " 
    211                                        "VALUES (%s,%s,%s,%s,%s,%s)", 
    212                                        (str(next_youngest), 
     226                                       " (repos,rev,path,node_type," 
     227                                       "  change_type,base_path,base_rev) " 
     228                                       "VALUES (%s,%s,%s,%s,%s,%s,%s)", 
     229                                       (self.reponame, str(next_youngest), 
    213230                                        path, kind, action, bpath, brev)) 
    214231 
    215232                    # 1.3. iterate (1.1 should always succeed now) 
     
    218235 
    219236                    # 1.4. update 'youngest_rev' metadata  
    220237                    #      (minimize possibility of failures at point 0.) 
    221                     cursor.execute("UPDATE system SET value=%s WHERE name=%s", 
    222                                    (str(self.youngest), CACHE_YOUNGEST_REV)) 
     238                    cursor.execute("UPDATE repository SET value=%s " 
     239                                   "WHERE id=%s AND name=%s", 
     240                                   (str(self.youngest), self.reponame, 
     241                                    CACHE_YOUNGEST_REV)) 
    223242                    db.commit() 
    224243 
    225244                    # 1.5. provide some feedback 
    226245                    if feedback: 
    227246                        feedback(self.youngest) 
     247                 
     248                return True 
    228249            finally: 
    229250                # 3. restore permission checking (after 1.) 
    230251                self.repos.authz = authz 
     
    240261 
    241262    def get_youngest_rev(self): 
    242263        if not hasattr(self, 'youngest'): 
    243             self.sync() 
     264            db = self.getdb() 
     265            cursor = db.cursor() 
     266            cursor.execute("SELECT MAX(" + db.cast('rev', 'int') + ") " 
     267                           "FROM revision WHERE repos=%s", (self.reponame,)) 
     268            for rev, in cursor: 
     269                self.youngest = str(rev) 
    244270        return self.youngest 
    245271 
    246272    def previous_rev(self, rev, path=''): 
     
    258284    def _next_prev_rev(self, direction, rev, path=''): 
    259285        db = self.getdb() 
    260286        # the changeset revs are sequence of ints: 
    261         sql = "SELECT rev FROM node_change WHERE " + \ 
     287        sql = "SELECT rev FROM node_change WHERE repos=%s AND " + \ 
    262288              db.cast('rev', 'int') + " " + direction + " %s" 
    263         args = [rev] 
     289        args = [self.reponame, rev] 
    264290 
    265291        if path: 
    266292            path = path.lstrip('/') 
     
    316342        db = self.getdb() 
    317343        cursor = db.cursor() 
    318344        cursor.execute("SELECT time,author,message FROM revision " 
    319                        "WHERE rev=%s", (str(rev),)) 
     345                       "WHERE repos=%s AND rev=%s", 
     346                       (self.repos.reponame, str(rev))) 
    320347        row = cursor.fetchone() 
    321348        if row: 
    322349            _date, author, message = row 
     
    330357        db = self.getdb() 
    331358        cursor = db.cursor() 
    332359        cursor.execute("SELECT path,node_type,change_type,base_path,base_rev " 
    333                        "FROM node_change WHERE rev=%s " 
    334                        "ORDER BY path", (str(self.rev),)) 
     360                       "FROM node_change WHERE repos=%s AND rev=%s " 
     361                       "ORDER BY path", (self.repos.reponame, str(self.rev))) 
    335362        for path, kind, change, base_path, base_rev in cursor: 
    336363            if not self.authz.has_permission(posixpath.join(self.scope, 
    337364                                                            path.strip('/'))): 
  • trac/versioncontrol/svn_fs.py

    diff --git a/trac/versioncontrol/svn_fs.py b/trac/versioncontrol/svn_fs.py
    a b  
    411411         
    412412        # Remove any trailing slash or else subversion might abort 
    413413        if isinstance(path, unicode): 
    414             self.path = path 
    415414            path_utf8 = path.encode('utf-8') 
    416415        else: # note that this should usually not happen (unicode arg expected) 
    417             self.path = to_unicode(path) 
    418             path_utf8 = self.path.encode('utf-8') 
     416            path_utf8 = to_unicode(path).encode('utf-8') 
     417 
    419418        path_utf8 = os.path.normpath(path_utf8).replace('\\', '/') 
     419        self.path = path_utf8.decode('utf-8') 
     420         
    420421        root_path_utf8 = repos.svn_repos_find_root_path(path_utf8, self.pool()) 
    421422        if root_path_utf8 is None: 
    422423            raise TracError(_("%(path)s does not appear to be a Subversion " 
     
    431432        self.fs_ptr = repos.svn_repos_fs(self.repos) 
    432433         
    433434        uuid = fs.get_uuid(self.fs_ptr, self.pool()) 
    434         name = 'svn:%s:%s' % (uuid, _from_svn(path_utf8)) 
     435        self.base = 'svn:%s:%s' % (uuid, _from_svn(root_path_utf8)) 
     436        name = 'svn:%s:%s' % (uuid, self.path) 
    435437 
    436438        Repository.__init__(self, name, authz, log) 
    437439 
     
    484486    def close(self): 
    485487        self.repos = self.fs_ptr = self.pool = None 
    486488 
     489    def get_base(self): 
     490        return self.base 
     491         
    487492    def _get_tags_or_branches(self, paths): 
    488493        """Retrieve known branches or tags.""" 
    489494        for path in self.options.get(paths, []): 
  • trac/versioncontrol/tests/api.py

    diff --git a/trac/versioncontrol/tests/api.py b/trac/versioncontrol/tests/api.py
    a b  
    2626    def test_raise_NotImplementedError_close(self): 
    2727        self.failUnlessRaises(NotImplementedError, self.repo_base.close) 
    2828 
    29     def test_raise_NotImplementedError_sync_changeset(self): 
    30         self.failUnlessRaises(NotImplementedError, self.repo_base.sync_changeset, 1) 
    31  
    3229    def test_raise_NotImplementedError_get_changeset(self): 
    3330        self.failUnlessRaises(NotImplementedError, self.repo_base.get_changeset, 1) 
    3431 
  • trac/versioncontrol/tests/cache.py

    diff --git a/trac/versioncontrol/tests/cache.py b/trac/versioncontrol/tests/cache.py
    a b  
    3232        self.db = InMemoryDatabase() 
    3333        self.log = logger_factory('test') 
    3434        cursor = self.db.cursor() 
    35         cursor.execute("INSERT INTO system (name, value) VALUES (%s,%s)", 
    36                        ('youngest_rev', '')) 
     35        cursor.execute("INSERT INTO repository (id, name, value) " 
     36                       "VALUES (%s,%s,%s)", 
     37                       ('test-repos', 'youngest_rev', '')) 
    3738 
    3839    def test_initial_sync_with_empty_repos(self): 
    3940        t = datetime(2001, 1, 1, 1, 1, 1, 0, utc) 
     
    9192        t2 = datetime(2002, 1, 1, 1, 1, 1, 0, utc) 
    9293        t3 = datetime(2003, 1, 1, 1, 1, 1, 0, utc) 
    9394        cursor = self.db.cursor() 
    94         cursor.execute("INSERT INTO revision (rev,time,author,message) " 
    95                        "VALUES (0,%s,'','')", (to_timestamp(t1),)) 
    96         cursor.execute("INSERT INTO revision (rev,time,author,message) " 
    97                        "VALUES (1,%s,'joe','Import')", (to_timestamp(t2),)) 
    98         cursor.executemany("INSERT INTO node_change (rev,path,node_type," 
    99                            "change_type,base_path,base_rev) " 
    100                            "VALUES ('1',%s,%s,%s,%s,%s)", 
     95        cursor.execute("INSERT INTO revision (repos,rev,time,author,message) " 
     96                       "VALUES ('test-repos',0,%s,'','')", 
     97                       (to_timestamp(t1),)) 
     98        cursor.execute("INSERT INTO revision (repos,rev,time,author,message) " 
     99                       "VALUES ('test-repos',1,%s,'joe','Import')", 
     100                       (to_timestamp(t2),)) 
     101        cursor.executemany("INSERT INTO node_change (repos,rev,path," 
     102                           "node_type,change_type,base_path,base_rev) " 
     103                           "VALUES ('test-repos','1',%s,%s,%s,%s,%s)", 
    101104                           [('trunk', 'D', 'A', None, None), 
    102105                            ('trunk/README', 'F', 'A', None, None)]) 
    103         cursor.execute("UPDATE system SET value='1' WHERE name='youngest_rev'") 
     106        cursor.execute("UPDATE repository SET value='1' " 
     107                       "WHERE id='test-repos' AND name='youngest_rev'") 
    104108 
    105109        changes = [('trunk/README', Node.FILE, Changeset.EDIT, 'trunk/README', 1)] 
    106110        changeset = Mock(Changeset, 2, 'Update', 'joe', t3, 
     
    128132        t1 = datetime(2001, 1, 1, 1, 1, 1, 0, utc) 
    129133        t2 = datetime(2002, 1, 1, 1, 1, 1, 0, utc) 
    130134        cursor = self.db.cursor() 
    131         cursor.execute("INSERT INTO revision (rev,time,author,message) " 
    132                        "VALUES (0,%s,'','')", (to_timestamp(t1),)) 
    133         cursor.execute("INSERT INTO revision (rev,time,author,message) " 
    134                        "VALUES (1,%s,'joe','Import')", (to_timestamp(t2),)) 
    135         cursor.executemany("INSERT INTO node_change (rev,path,node_type," 
    136                            "change_type,base_path,base_rev) " 
    137                            "VALUES ('1',%s,%s,%s,%s,%s)", 
     135        cursor.execute("INSERT INTO revision (repos,rev,time,author,message) " 
     136                       "VALUES ('test-repos',0,%s,'','')", 
     137                       (to_timestamp(t1),)) 
     138        cursor.execute("INSERT INTO revision (repos,rev,time,author,message) " 
     139                       "VALUES ('test-repos',1,%s,'joe','Import')", 
     140                       (to_timestamp(t2),)) 
     141        cursor.executemany("INSERT INTO node_change (repos,rev,path," 
     142                           "node_type,change_type,base_path,base_rev) " 
     143                           "VALUES ('test-repos','1',%s,%s,%s,%s,%s)", 
    138144                           [('trunk', 'D', 'A', None, None), 
    139145                            ('trunk/README', 'F', 'A', None, None)]) 
    140         cursor.execute("UPDATE system SET value='1' WHERE name='youngest_rev'") 
     146        cursor.execute("UPDATE repository SET value='1' " 
     147                       "WHERE id='test-repos' AND name='youngest_rev'") 
    141148 
    142149        repos = Mock(Repository, 'test-repos', None, self.log, 
    143150                     get_changeset=lambda x: None, 
  • trac/versioncontrol/web_ui/changeset.py

    diff --git a/trac/versioncontrol/web_ui/changeset.py b/trac/versioncontrol/web_ui/changeset.py
    a b  
    908908 
    909909        single = rev_a == rev_b 
    910910        if reponame: 
    911             title = ngettext('Changeset in %(repo)s', 'Changesets in %(repo)s', 
     911            title = ngettext('Changeset in %(repo)s ', 
     912                             'Changesets in %(repo)s ', 
    912913                             single and 1 or 2, repo=reponame) 
    913914        else: 
    914             title = ngettext('Changeset', 'Changesets', single and 1 or 2) 
     915            title = ngettext('Changeset ', 'Changesets ', single and 1 or 2) 
    915916        if single: 
    916             title = tag(title, tag.em(' [%s]' % rev_a)) 
     917            title = tag(title, tag.em('[%s]' % rev_a)) 
    917918        else: 
    918             title = tag(title, tag.em(' [%s-%s]' % (rev_a, rev_b))) 
     919            title = tag(title, tag.em('[%s-%s]' % (rev_a, rev_b))) 
    919920        if field == 'title': 
    920921            return title 
    921922        elif field == 'summary':