MultiRepos: added initial support for multiple cached repositories.

 - introduction of a new ''repository'' command in TracAdmin
   - `resync` subcommand is now `repository resync`
   - new subcommand `changeset` for new changeset notification (the name of this subcommand is likely to change in follow-up revisions)
 - the `RepositoryManager` now makes a distinction between "real" repositories and mere aliases to real repositories. Only real repositories get notified of new changesets and get resynced.
 - a `Repository` can now report its actual base, if it's a scoped repository. This enable to identify different scoped repositories sharing the same base. Note that a scoped repository is a real repository and if its type requires it (e.g. svn), it will get cached on its own.


Note also that '''a schema upgrade is needed''' (database_version = 22).
The `node_change` and `revision` tables gain a new `repos` column of type text.


Patch written by Remy Blank on #7723.
Review, nit-picking and summary by me.

diff --git a/trac/admin/console.py b/trac/admin/console.py
--- a/trac/admin/console.py
+++ b/trac/admin/console.py
@@ -50,7 +50,7 @@
         try:
             import readline
             delims = readline.get_completer_delims()
-            for c in '-/':
+            for c in '-/()':
                 delims = delims.replace(c, '')
             readline.set_completer_delims(delims)
         except ImportError:
diff --git a/trac/admin/tests/console-tests.txt b/trac/admin/tests/console-tests.txt
--- a/trac/admin/tests/console-tests.txt
+++ b/trac/admin/tests/console-tests.txt
@@ -5,62 +5,63 @@
 
 Invoking trac-admin without command starts interactive mode.
 
-help                 Show documentation
-initenv              Create and initialize a new environment
-component add        Add a new component
-component chown      Change component ownership
-component list       Show available components
-component remove     Remove/uninstall a component
-component rename     Rename a component
-config get           Get the value of the given option in "trac.ini"
-config remove        Remove the specified option from "trac.ini"
-config set           Set the value for the given option in "trac.ini"
-deploy               Extract static resources from Trac and all plugins
-hotcopy              Make a hot backup copy of an environment
-milestone add        Add milestone
-milestone completed  Set milestone complete date
-milestone due        Set milestone due date
-milestone list       Show milestones
-milestone remove     Remove milestone
-milestone rename     Rename milestone
-permission add       Add a new permission rule
-permission list      List permission rules
-permission remove    Remove a permission rule
-priority add         Add a priority value option
-priority change      Change a priority value
-priority list        Show possible ticket priorities
-priority order       Move a priority value up or down in the list
-priority remove      Remove a priority value
-resolution add       Add a resolution value option
-resolution change    Change a resolution value
-resolution list      Show possible ticket resolutions
-resolution order     Move a resolution value up or down in the list
-resolution remove    Remove a resolution value
-resync               Re-synchronize trac with the repository
-severity add         Add a severity value option
-severity change      Change a severity value
-severity list        Show possible ticket severities
-severity order       Move a severity value up or down in the list
-severity remove      Remove a severity value
-ticket remove        Remove ticket
-ticket_type add      Add a ticket type
-ticket_type change   Change a ticket type
-ticket_type list     Show possible ticket types
-ticket_type order    Move a ticket type up or down in the list
-ticket_type remove   Remove a ticket type
-upgrade              Upgrade database to current version
-version add          Add version
-version list         Show versions
-version remove       Remove version
-version rename       Rename version
-version time         Set version date
-wiki dump            Export all wiki pages to files named by title
-wiki export          Export wiki page to file or stdout
-wiki import          Import wiki page from file or stdin
-wiki list            List wiki pages
-wiki load            Import all wiki pages from directory
-wiki remove          Remove wiki page
-wiki upgrade         Upgrade default wiki pages to current version
+help                  Show documentation
+initenv               Create and initialize a new environment
+component add         Add a new component
+component chown       Change component ownership
+component list        Show available components
+component remove      Remove/uninstall a component
+component rename      Rename a component
+config get            Get the value of the given option in "trac.ini"
+config remove         Remove the specified option from "trac.ini"
+config set            Set the value for the given option in "trac.ini"
+deploy                Extract static resources from Trac and all plugins
+hotcopy               Make a hot backup copy of an environment
+milestone add         Add milestone
+milestone completed   Set milestone complete date
+milestone due         Set milestone due date
+milestone list        Show milestones
+milestone remove      Remove milestone
+milestone rename      Rename milestone
+permission add        Add a new permission rule
+permission list       List permission rules
+permission remove     Remove a permission rule
+priority add          Add a priority value option
+priority change       Change a priority value
+priority list         Show possible ticket priorities
+priority order        Move a priority value up or down in the list
+priority remove       Remove a priority value
+repository changeset  Notify trac about new changesets
+repository resync     Re-synchronize trac with repositories
+resolution add        Add a resolution value option
+resolution change     Change a resolution value
+resolution list       Show possible ticket resolutions
+resolution order      Move a resolution value up or down in the list
+resolution remove     Remove a resolution value
+severity add          Add a severity value option
+severity change       Change a severity value
+severity list         Show possible ticket severities
+severity order        Move a severity value up or down in the list
+severity remove       Remove a severity value
+ticket remove         Remove ticket
+ticket_type add       Add a ticket type
+ticket_type change    Change a ticket type
+ticket_type list      Show possible ticket types
+ticket_type order     Move a ticket type up or down in the list
+ticket_type remove    Remove a ticket type
+upgrade               Upgrade database to current version
+version add           Add version
+version list          Show versions
+version remove        Remove version
+version rename        Rename version
+version time          Set version date
+wiki dump             Export all wiki pages to files named by title
+wiki export           Export wiki page to file or stdout
+wiki import           Import wiki page from file or stdin
+wiki list             List wiki pages
+wiki load             Import all wiki pages from directory
+wiki remove           Remove wiki page
+wiki upgrade          Upgrade default wiki pages to current version
 ===== test_config_get =====
 Test project
 ===== test_config_set =====
diff --git a/trac/db_default.py b/trac/db_default.py
--- a/trac/db_default.py
+++ b/trac/db_default.py
@@ -17,7 +17,7 @@
 from trac.db import Table, Column, Index
 
 # Database version identifier. Used for automatic upgrades.
-db_version = 21
+db_version = 22
 
 def __mkreports(reports):
     """Utility function used to create report data in same syntax as the
@@ -82,20 +82,26 @@
         Index(['time'])],
 
     # Version control cache
-    Table('revision', key='rev')[
+    Table('repository', key=('id', 'name'))[
+        Column('id'),
+        Column('name'),
+        Column('value')],
+    Table('revision', key=('repos', 'rev'))[
+        Column('repos'),
         Column('rev'),
         Column('time', type='int'),
         Column('author'),
         Column('message'),
-        Index(['time'])],
-    Table('node_change', key=('rev', 'path', 'change_type'))[
+        Index(['repos', 'time'])],
+    Table('node_change', key=('repos', 'rev', 'path', 'change_type'))[
+        Column('repos'),
         Column('rev'),
         Column('path'),
         Column('node_type', size=1),
         Column('change_type', size=1),
         Column('base_path'),
         Column('base_rev'),
-        Index(['rev'])],
+        Index(['repos', 'rev'])],
 
     # Ticket system
     Table('ticket', key='id')[
@@ -384,8 +390,7 @@
            ('system',
              ('name', 'value'),
                (('database_version', str(db_version)),
-                ('initial_database_version', str(db_version)),
-                ('youngest_rev', ''))),
+                ('initial_database_version', str(db_version)))),
            ('report',
              ('author', 'title', 'query', 'description'),
                __mkreports(get_reports(db))))
diff --git a/trac/versioncontrol/admin.py b/trac/versioncontrol/admin.py
--- a/trac/versioncontrol/admin.py
+++ b/trac/versioncontrol/admin.py
@@ -15,8 +15,9 @@
 
 from trac.admin import IAdminCommandProvider
 from trac.core import *
-from trac.util.text import printout
+from trac.util.text import printerr, printout
 from trac.util.translation import _, ngettext
+from trac.versioncontrol import RepositoryManager
 
 
 class VersionControlAdmin(Component):
@@ -27,36 +28,74 @@
     # IAdminCommandProvider methods
     
     def get_admin_commands(self):
-        yield ('resync', '[rev]',
-               """Re-synchronize trac with the repository
+        yield ('repository changeset', '<repos> <rev> [rev] [...]',
+               'Notify trac about new changesets',
+               self._complete_repos, self._do_changeset)
+        yield ('repository resync', '<repos> [rev]',
+               """Re-synchronize trac with repositories
                
                When [rev] is specified, only that revision is synchronized.
                Otherwise, the complete revision history is synchronized. Note
                that this operation can take a long time to complete.
+               
+               To synchronize all repositories, specify "*" as the repository.
                """,
-               None, self._do_resync)
+               self._complete_repos, self._do_resync)
     
-    def _do_resync(self, rev=None):
-        if rev:
-            self.env.get_repository().sync_changeset(rev)
-            printout(_('%(rev)s resynced.', rev=rev))
-            return
+    def _complete_repos(self, args):
+        if len(args) == 1:
+            rm = RepositoryManager(self.env)
+            return [reponame or '(default)' for reponame
+                    in rm.get_all_repositories()]
+    
+    def _do_changeset(self, reponame, *revs):
+        rm = RepositoryManager(self.env)
+        rm.notify_changesets_added(reponame, revs, None)
+    
+    def _do_resync(self, reponame, rev=None):
+        rm = RepositoryManager(self.env)
+        if reponame == '*':
+            if rev is not None:
+                raise TracError(_('Cannot synchronize a single revision '
+                                  'on multiple repositories'))
+            repositories = rm.get_real_repositories(None)
+        else:
+            if reponame == '(default)':
+                reponame = ''
+            repos = rm.get_repository(reponame, None)
+            if repos is None:
+                raise TracError(_("Unknown repository '%(reponame)s'",
+                                  reponame=reponame or '(default)'))
+            if rev is not None:
+                repos.sync_changeset(rev)
+                printout(_('%(rev)s resynced on %(reponame)s.', rev=rev,
+                           reponame=repos.reponame or '(default)'))
+                return
+            repositories = [repos]
+        
         from trac.versioncontrol.cache import CACHE_METADATA_KEYS
-        printout(_('Resyncing repository history... '))
         db = self.env.get_db_cnx()
         cursor = db.cursor()
-        cursor.execute("DELETE FROM revision")
-        cursor.execute("DELETE FROM node_change")
-        cursor.executemany("DELETE FROM system WHERE name=%s",
-                           [(k,) for k in CACHE_METADATA_KEYS])
-        cursor.executemany("INSERT INTO system (name, value) VALUES (%s, %s)",
-                           [(k, '') for k in CACHE_METADATA_KEYS])
-        db.commit()
-        repos = self.env.get_repository().sync(self._resync_feedback)
-        cursor.execute("SELECT count(rev) FROM revision")
-        for cnt, in cursor:
-            printout(ngettext('%(num)s revision cached.',
-                              '%(num)s revisions cached.', num=cnt))
+        for repos in sorted(repositories, key=lambda r: r.reponame):
+            reponame = repos.reponame
+            printout(_('Resyncing repository history for %(reponame)s... ',
+                       reponame=reponame or '(default)'))
+            cursor.execute("DELETE FROM revision WHERE repos=%s", (reponame,))
+            cursor.execute("DELETE FROM node_change "
+                           "WHERE repos=%s", (reponame,))
+            cursor.executemany("DELETE FROM repository "
+                               "WHERE id=%s AND name=%s",
+                               [(reponame, k) for k in CACHE_METADATA_KEYS])
+            cursor.executemany("INSERT INTO repository (id, name, value) "
+                               "VALUES (%s, %s, %s)", [(reponame, k, '') 
+                                                for k in CACHE_METADATA_KEYS])
+            db.commit()
+            repos.sync(self._resync_feedback)
+            cursor.execute("SELECT count(rev) FROM revision WHERE repos=%s",
+                           (reponame,))
+            for cnt, in cursor:
+                printout(ngettext('%(num)s revision cached.',
+                                  '%(num)s revisions cached.', num=cnt))
         printout(_('Done.'))
 
     def _resync_feedback(self, rev):
diff --git a/trac/versioncontrol/api.py b/trac/versioncontrol/api.py
--- a/trac/versioncontrol/api.py
+++ b/trac/versioncontrol/api.py
@@ -55,6 +55,7 @@
         """Return a Repository instance for the given repository type and dir.
         """
 
+
 class IRepositoryProvider(Interface):
     """Provide known named instances of Repository."""
 
@@ -73,8 +74,15 @@
         """
 
 
+class IRepositoryChangeListener(Interface):
+    """Listen for changes in repositories."""
+    
+    def changeset_added(self, repos, changeset):
+        """Called after a changeset has been added to a repository."""
+
+
 class RepositoryManager(Component):
-    """Component registering the supported version control systems,
+    """Component registering the supported version control systems.
 
     It provides easy access to the configured implementation.
     """
@@ -83,6 +91,7 @@
 
     connectors = ExtensionPoint(IRepositoryConnector)
     providers = ExtensionPoint(IRepositoryProvider)
+    change_listeners = ExtensionPoint(IRepositoryChangeListener)
 
     repository_type = Option('trac', 'repository_type', 'svn',
         """Default repository connector type. (''since 0.10'')""")
@@ -184,7 +193,7 @@
     # Public API methods
 
     def get_repository(self, reponame, authname):
-        """Retrieve the appropriate Repository for the given name
+        """Retrieve the appropriate Repository for the given name.
 
            :param reponame: the key for specifying the repository.
                             If no name is given, take the the default 
@@ -195,7 +204,8 @@
         """
         repoinfo = self.get_all_repositories().get(reponame, {})
         if repoinfo and 'alias' in repoinfo:
-            repoinfo = self.get_all_repositories().get(repoinfo['alias'])
+            reponame = repoinfo['alias']
+            repoinfo = self.get_all_repositories().get(reponame)
         if repoinfo:
             rdir = repoinfo.get('dir')
             rtype = repoinfo.get('type', self.repository_type)
@@ -251,7 +261,7 @@
         return (reponame, self.get_repository(reponame, authname), path or '/')
 
     def get_default_repository(self, context):
-        """Recover the appropriatet repository from the current context.
+        """Recover the appropriate repository from the current context.
 
         Lookup the closest source or changeset resource in the context 
         hierarchy and return the name of its associated repository.
@@ -273,7 +283,45 @@
                     else:
                         self._all_repositories[reponame] = info
         return self._all_repositories
+    
+    def get_real_repositories(self, authname):
+        """Return a list of all real repositories (i.e. excluding aliases)."""
+        repositories = set()
+        for reponame in self.get_all_repositories():
+            try:
+                repos = self.get_repository(reponame, authname)
+                if repos is not None:
+                    repositories.add(repos)
+            except TracError:
+                "Skip invalid repositories"
+        return repositories
 
+    def notify_changesets_added(self, reponame, revs, authname):
+        """Notify repositories and change listeners about added changesets."""
+        self.log.debug('Notification on %s for changesets %r'
+                       % (reponame, revs))
+        
+        # Notify a repository by name, and all repositories with the same
+        # base, or all repositories by base
+        repos = self.get_repository(reponame, None)
+        if repos:
+            base = repos.get_base()
+        else:
+            base = reponame
+        repositories = [each for each in self.get_real_repositories(authname)
+                        if each.get_base() == base]
+        for repos in sorted(repositories, key=lambda r: r.reponame):
+            repos.sync()
+            for rev in revs:
+                try:
+                    changeset = repos.get_changeset(rev)
+                except NoSuchChangeset:
+                    continue
+                self.log.debug('Notifying %s for revision %s'
+                               % (repos.reponame, rev))
+                for listener in self.change_listeners:
+                    listener.changeset_added(repos, changeset)
+    
     def shutdown(self, tid=None):
         if tid:
             assert tid == threading._get_ident()
@@ -345,6 +393,15 @@
         """Close the connection to the repository."""
         raise NotImplementedError
 
+    def get_base(self):
+        """Return the name of the base repository for this repository.
+        
+        This function returns the name of the base repository to which scoped
+        repositories belong. For non-scoped repositories, it returns the 
+        repository name.
+        """
+        return self.name
+        
     def clear(self, youngest_rev=None):
         """Clear any data that may have been cached in instance properties.
 
@@ -457,7 +514,7 @@
         return row and row[0] or None
 
     def get_path_history(self, path, rev=None, limit=None):
-        """Retrieve all the revisions containing this path
+        """Retrieve all the revisions containing this path.
 
         If given, `rev` is used as a starting point (i.e. no revision
         ''newer'' than `rev` should be returned).
@@ -636,7 +693,7 @@
         return []
         
     def get_changes(self):
-        """Generator that produces a tuple for every change in the changeset
+        """Generator that produces a tuple for every change in the changeset.
 
         The tuple will contain `(path, kind, change, base_path, base_rev)`,
         where `change` can be one of Changeset.ADD, Changeset.COPY,
diff --git a/trac/versioncontrol/cache.py b/trac/versioncontrol/cache.py
--- a/trac/versioncontrol/cache.py
+++ b/trac/versioncontrol/cache.py
@@ -41,16 +41,25 @@
     has_linear_changesets = False
 
     def __init__(self, getdb, repos, authz, log):
+        self.repos = repos
         Repository.__init__(self, repos.name, authz, log)
         if callable(getdb):
             self.getdb = getdb
         else:
             self.getdb = lambda: getdb
-        self.repos = repos
 
+    def _set_reponame(self, value):
+        self.repos.reponame = value
+    
+    reponame = property(fget=lambda self: self.repos.reponame,
+                        fset=_set_reponame)
+    
     def close(self):
         self.repos.close()
 
+    def get_base(self):
+        return self.repos.get_base()
+        
     def get_quickjump_entries(self, rev):
         for category, name, path, rev in self.repos.get_quickjump_entries(rev):
             yield category, name, path, rev
@@ -63,9 +72,10 @@
         db = self.getdb()
         cursor = db.cursor()
         cursor.execute("SELECT rev FROM revision "
-                       "WHERE time >= %s AND time < %s "
+                       "WHERE repos=%s AND time >= %s AND time < %s"
                        "ORDER BY time DESC, rev DESC",
-                       (to_timestamp(start), to_timestamp(stop)))
+                       (self.reponame, to_timestamp(start),
+                        to_timestamp(stop)))
         for rev, in cursor:
             try:
                 if self.authz.has_permission_for_changeset(rev):
@@ -78,16 +88,18 @@
         db = self.getdb()
         cursor = db.cursor()
         cursor.execute("UPDATE revision SET time=%s, author=%s, message=%s "
-                       "WHERE rev=%s", (to_timestamp(cset.date),
-                                        cset.author, cset.message,
-                                        (str(cset.rev))))
+                       "WHERE repos=%s AND rev=%s",
+                       (to_timestamp(cset.date), cset.author, cset.message,
+                        self.reponame, str(cset.rev)))
         db.commit()
         
     def sync(self, feedback=None):
         db = self.getdb()
         cursor = db.cursor()
-        cursor.execute("SELECT name, value FROM system WHERE name IN (%s)" %
-                       ','.join(["'%s'" % key for key in CACHE_METADATA_KEYS]))
+        cursor.execute("SELECT name, value FROM repository "
+                       "WHERE id=%%s AND name IN (%s)" % 
+                       ','.join(['%s'] * len(CACHE_METADATA_KEYS)),
+                       (self.reponame,) + CACHE_METADATA_KEYS)
         metadata = {}
         for name, value in cursor:
             metadata[name] = value
@@ -103,24 +115,27 @@
                                   "'trac-admin resync' operation is needed."))
         elif repository_dir is None: # 
             self.log.info('Storing initial "repository_dir": %s' % self.name)
-            cursor.execute("INSERT INTO system (name,value) VALUES (%s,%s)",
-                           (CACHE_REPOSITORY_DIR, self.name,))
+            cursor.execute("INSERT INTO repository (id,name,value) "
+                           "VALUES (%s,%s,%s)",
+                           (self.reponame, CACHE_REPOSITORY_DIR, self.name))
         else: # 'repository_dir' cleared by a resync
             self.log.info('Resetting "repository_dir": %s' % self.name)
-            cursor.execute("UPDATE system SET value=%s WHERE name=%s",
-                           (self.name, CACHE_REPOSITORY_DIR))
-
-        db.commit() # save metadata changes made up to now
+            cursor.execute("UPDATE repository SET value=%s "
+                           "WHERE id=%s AND name=%s",
+                           (self.name, self.reponame, CACHE_REPOSITORY_DIR))
 
         # -- retrieve the youngest revision in the repository
         self.repos.clear()
         repos_youngest = self.repos.youngest_rev
 
         # -- retrieve the youngest revision cached so far
-        if CACHE_YOUNGEST_REV not in metadata:
-            raise TracError(_('Missing "youngest_rev" in cache metadata'))
-        
-        self.youngest = metadata[CACHE_YOUNGEST_REV]
+        self.youngest = metadata.get(CACHE_YOUNGEST_REV)
+        if self.youngest is None:
+            cursor.execute("INSERT INTO repository (id,name,value) "
+                           "VALUES (%s,%s,%s)",
+                           (self.reponame, CACHE_YOUNGEST_REV, ''))
+
+        db.commit() # save metadata changes made up to now
 
         if self.youngest:
             self.youngest = self.repos.normalize_rev(self.youngest)
@@ -156,8 +171,9 @@
                 return
 
             # 0. first check if there's no (obvious) resync in progress
-            cursor.execute("SELECT rev FROM revision WHERE rev=%s",
-                           (str(next_youngest),))
+            cursor.execute("SELECT rev FROM revision "
+                           "WHERE repos=%s AND rev=%s",
+                           (self.reponame, str(next_youngest)))
             for rev, in cursor:
                 # already there, but in progress, so keep ''previous''
                 # notion of 'youngest'
@@ -182,9 +198,9 @@
                     cset = self.repos.get_changeset(next_youngest)
                     try:
                         cursor.execute("INSERT INTO revision "
-                                       " (rev,time,author,message) "
-                                       "VALUES (%s,%s,%s,%s)",
-                                       (str(next_youngest),
+                                       " (repos,rev,time,author,message) "
+                                       "VALUES (%s,%s,%s,%s,%s)",
+                                       (self.reponame, str(next_youngest),
                                         to_timestamp(cset.date),
                                         cset.author, cset.message))
                     except Exception, e: # *another* 1.1. resync attempt won 
@@ -206,10 +222,10 @@
                         kind = kindmap[kind]
                         action = actionmap[action]
                         cursor.execute("INSERT INTO node_change "
-                                       " (rev,path,node_type,change_type, "
-                                       "  base_path,base_rev) "
-                                       "VALUES (%s,%s,%s,%s,%s,%s)",
-                                       (str(next_youngest),
+                                       " (repos,rev,path,node_type,"
+                                       "  change_type,base_path,base_rev) "
+                                       "VALUES (%s,%s,%s,%s,%s,%s,%s)",
+                                       (self.reponame, str(next_youngest),
                                         path, kind, action, bpath, brev))
 
                     # 1.3. iterate (1.1 should always succeed now)
@@ -218,8 +234,10 @@
 
                     # 1.4. update 'youngest_rev' metadata 
                     #      (minimize possibility of failures at point 0.)
-                    cursor.execute("UPDATE system SET value=%s WHERE name=%s",
-                                   (str(self.youngest), CACHE_YOUNGEST_REV))
+                    cursor.execute("UPDATE repository SET value=%s "
+                                   "WHERE id=%s AND name=%s",
+                                   (str(self.youngest), self.reponame,
+                                    CACHE_YOUNGEST_REV))
                     db.commit()
 
                     # 1.5. provide some feedback
@@ -258,9 +276,9 @@
     def _next_prev_rev(self, direction, rev, path=''):
         db = self.getdb()
         # the changeset revs are sequence of ints:
-        sql = "SELECT rev FROM node_change WHERE " + \
+        sql = "SELECT rev FROM node_change WHERE repos=%s AND " + \
               db.cast('rev', 'int') + " " + direction + " %s"
-        args = [rev]
+        args = [self.reponame, rev]
 
         if path:
             path = path.lstrip('/')
@@ -316,7 +334,8 @@
         db = self.getdb()
         cursor = db.cursor()
         cursor.execute("SELECT time,author,message FROM revision "
-                       "WHERE rev=%s", (str(rev),))
+                       "WHERE repos=%s AND rev=%s",
+                       (self.repos.reponame, str(rev)))
         row = cursor.fetchone()
         if row:
             _date, author, message = row
@@ -330,8 +349,8 @@
         db = self.getdb()
         cursor = db.cursor()
         cursor.execute("SELECT path,node_type,change_type,base_path,base_rev "
-                       "FROM node_change WHERE rev=%s "
-                       "ORDER BY path", (str(self.rev),))
+                       "FROM node_change WHERE repos=%s AND rev=%s "
+                       "ORDER BY path", (self.repos.reponame, str(self.rev)))
         for path, kind, change, base_path, base_rev in cursor:
             if not self.authz.has_permission(posixpath.join(self.scope,
                                                             path.strip('/'))):
diff --git a/trac/versioncontrol/svn_fs.py b/trac/versioncontrol/svn_fs.py
--- a/trac/versioncontrol/svn_fs.py
+++ b/trac/versioncontrol/svn_fs.py
@@ -431,6 +431,7 @@
         self.fs_ptr = repos.svn_repos_fs(self.repos)
         
         uuid = fs.get_uuid(self.fs_ptr, self.pool())
+        self.base = 'svn:%s:%s' % (uuid, _from_svn(root_path_utf8))
         name = 'svn:%s:%s' % (uuid, _from_svn(path_utf8))
 
         Repository.__init__(self, name, authz, log)
@@ -484,6 +485,9 @@
     def close(self):
         self.repos = self.fs_ptr = self.pool = None
 
+    def get_base(self):
+        return self.base
+        
     def _get_tags_or_branches(self, paths):
         """Retrieve known branches or tags."""
         for path in self.options.get(paths, []):
diff --git a/trac/versioncontrol/tests/cache.py b/trac/versioncontrol/tests/cache.py
--- a/trac/versioncontrol/tests/cache.py
+++ b/trac/versioncontrol/tests/cache.py
@@ -32,8 +32,9 @@
         self.db = InMemoryDatabase()
         self.log = logger_factory('test')
         cursor = self.db.cursor()
-        cursor.execute("INSERT INTO system (name, value) VALUES (%s,%s)",
-                       ('youngest_rev', ''))
+        cursor.execute("INSERT INTO repository (id, name, value) "
+                       "VALUES (%s,%s,%s)",
+                       ('test-repos', 'youngest_rev', ''))
 
     def test_initial_sync_with_empty_repos(self):
         t = datetime(2001, 1, 1, 1, 1, 1, 0, utc)
@@ -91,16 +92,19 @@
         t2 = datetime(2002, 1, 1, 1, 1, 1, 0, utc)
         t3 = datetime(2003, 1, 1, 1, 1, 1, 0, utc)
         cursor = self.db.cursor()
-        cursor.execute("INSERT INTO revision (rev,time,author,message) "
-                       "VALUES (0,%s,'','')", (to_timestamp(t1),))
-        cursor.execute("INSERT INTO revision (rev,time,author,message) "
-                       "VALUES (1,%s,'joe','Import')", (to_timestamp(t2),))
-        cursor.executemany("INSERT INTO node_change (rev,path,node_type,"
-                           "change_type,base_path,base_rev) "
-                           "VALUES ('1',%s,%s,%s,%s,%s)",
+        cursor.execute("INSERT INTO revision (repos,rev,time,author,message) "
+                       "VALUES ('test-repos',0,%s,'','')",
+                       (to_timestamp(t1),))
+        cursor.execute("INSERT INTO revision (repos,rev,time,author,message) "
+                       "VALUES ('test-repos',1,%s,'joe','Import')",
+                       (to_timestamp(t2),))
+        cursor.executemany("INSERT INTO node_change (repos,rev,path,"
+                           "node_type,change_type,base_path,base_rev) "
+                           "VALUES ('test-repos','1',%s,%s,%s,%s,%s)",
                            [('trunk', 'D', 'A', None, None),
                             ('trunk/README', 'F', 'A', None, None)])
-        cursor.execute("UPDATE system SET value='1' WHERE name='youngest_rev'")
+        cursor.execute("UPDATE repository SET value='1' "
+                       "WHERE id='test-repos' AND name='youngest_rev'")
 
         changes = [('trunk/README', Node.FILE, Changeset.EDIT, 'trunk/README', 1)]
         changeset = Mock(Changeset, 2, 'Update', 'joe', t3,
@@ -128,16 +132,19 @@
         t1 = datetime(2001, 1, 1, 1, 1, 1, 0, utc)
         t2 = datetime(2002, 1, 1, 1, 1, 1, 0, utc)
         cursor = self.db.cursor()
-        cursor.execute("INSERT INTO revision (rev,time,author,message) "
-                       "VALUES (0,%s,'','')", (to_timestamp(t1),))
-        cursor.execute("INSERT INTO revision (rev,time,author,message) "
-                       "VALUES (1,%s,'joe','Import')", (to_timestamp(t2),))
-        cursor.executemany("INSERT INTO node_change (rev,path,node_type,"
-                           "change_type,base_path,base_rev) "
-                           "VALUES ('1',%s,%s,%s,%s,%s)",
+        cursor.execute("INSERT INTO revision (repos,rev,time,author,message) "
+                       "VALUES ('test-repos',0,%s,'','')",
+                       (to_timestamp(t1),))
+        cursor.execute("INSERT INTO revision (repos,rev,time,author,message) "
+                       "VALUES ('test-repos',1,%s,'joe','Import')",
+                       (to_timestamp(t2),))
+        cursor.executemany("INSERT INTO node_change (repos,rev,path,"
+                           "node_type,change_type,base_path,base_rev) "
+                           "VALUES ('test-repos','1',%s,%s,%s,%s,%s)",
                            [('trunk', 'D', 'A', None, None),
                             ('trunk/README', 'F', 'A', None, None)])
-        cursor.execute("UPDATE system SET value='1' WHERE name='youngest_rev'")
+        cursor.execute("UPDATE repository SET value='1' "
+                       "WHERE id='test-repos' AND name='youngest_rev'")
 
         repos = Mock(Repository, 'test-repos', None, self.log,
                      get_changeset=lambda x: None,
diff --git a/trac/versioncontrol/web_ui/changeset.py b/trac/versioncontrol/web_ui/changeset.py
--- a/trac/versioncontrol/web_ui/changeset.py
+++ b/trac/versioncontrol/web_ui/changeset.py
@@ -908,14 +908,15 @@
 
         single = rev_a == rev_b
         if reponame:
-            title = ngettext('Changeset in %(repo)s', 'Changesets in %(repo)s',
+            title = ngettext('Changeset in %(repo)s ',
+                             'Changesets in %(repo)s ',
                              single and 1 or 2, repo=reponame)
         else:
-            title = ngettext('Changeset', 'Changesets', single and 1 or 2)
+            title = ngettext('Changeset ', 'Changesets ', single and 1 or 2)
         if single:
-            title = tag(title, tag.em(' [%s]' % rev_a))
+            title = tag(title, tag.em('[%s]' % rev_a))
         else:
-            title = tag(title, tag.em(' [%s-%s]' % (rev_a, rev_b)))
+            title = tag(title, tag.em('[%s-%s]' % (rev_a, rev_b)))
         if field == 'title':
             return title
         elif field == 'summary':

