Ticket #7723: 7723-multirepos-cache-6-r7928.patch
| File 7723-multirepos-cache-6-r7928.patch, 36.9 KB (added by rblank, 3 years ago) |
|---|
-
trac/admin/console.py
diff --git a/trac/admin/console.py b/trac/admin/console.py
a b 50 50 try: 51 51 import readline 52 52 delims = readline.get_completer_delims() 53 for c in '-/ ':53 for c in '-/()': 54 54 delims = delims.replace(c, '') 55 55 readline.set_completer_delims(delims) 56 56 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 31 31 priority list Show possible ticket priorities 32 32 priority order Move a priority value up or down in the list 33 33 priority remove Remove a priority value 34 repository notify Notify trac about repository events 35 repository resync Re-synchronize trac with repositories 34 36 resolution add Add a resolution value option 35 37 resolution change Change a resolution value 36 38 resolution list Show possible ticket resolutions 37 39 resolution order Move a resolution value up or down in the list 38 40 resolution remove Remove a resolution value 39 resync Re-synchronize trac with the repository40 41 severity add Add a severity value option 41 42 severity change Change a severity value 42 43 severity list Show possible ticket severities -
trac/db_default.py
diff --git a/trac/db_default.py b/trac/db_default.py
a b 17 17 from trac.db import Table, Column, Index 18 18 19 19 # Database version identifier. Used for automatic upgrades. 20 db_version = 2 120 db_version = 22 21 21 22 22 def __mkreports(reports): 23 23 """Utility function used to create report data in same syntax as the … … 82 82 Index(['time'])], 83 83 84 84 # 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'), 86 91 Column('rev'), 87 92 Column('time', type='int'), 88 93 Column('author'), 89 94 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'), 92 98 Column('rev'), 93 99 Column('path'), 94 100 Column('node_type', size=1), 95 101 Column('change_type', size=1), 96 102 Column('base_path'), 97 103 Column('base_rev'), 98 Index(['re v'])],104 Index(['repos', 'rev'])], 99 105 100 106 # Ticket system 101 107 Table('ticket', key='id')[ … … 384 390 ('system', 385 391 ('name', 'value'), 386 392 (('database_version', str(db_version)), 387 ('initial_database_version', str(db_version)), 388 ('youngest_rev', ''))), 393 ('initial_database_version', str(db_version)))), 389 394 ('report', 390 395 ('author', 'title', 'query', 'description'), 391 396 __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
- + 1 from trac.db import Table, Column, Index, DatabaseManager 2 3 def 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 15 15 16 16 from trac.admin import IAdminCommandProvider 17 17 from trac.core import * 18 from trac.util.text import print out18 from trac.util.text import printerr, printout 19 19 from trac.util.translation import _, ngettext 20 from trac.versioncontrol import IRepositoryChangeListener, RepositoryManager 20 21 21 22 22 23 class VersionControlAdmin(Component): … … 27 28 # IAdminCommandProvider methods 28 29 29 30 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 32 43 33 44 When [rev] is specified, only that revision is synchronized. 34 45 Otherwise, the complete revision history is synchronized. Note 35 46 that this operation can take a long time to complete. 47 48 To synchronize all repositories, specify "*" as the repository. 36 49 """, 37 None, self._do_resync)50 self._complete_repos, self._do_resync) 38 51 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 44 96 from trac.versioncontrol.cache import CACHE_METADATA_KEYS 45 printout(_('Resyncing repository history... '))46 97 db = self.env.get_db_cnx() 47 98 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 60 123 printout(_('Done.')) 61 124 62 125 def _resync_feedback(self, rev): -
trac/versioncontrol/api.py
diff --git a/trac/versioncontrol/api.py b/trac/versioncontrol/api.py
a b 55 55 """Return a Repository instance for the given repository type and dir. 56 56 """ 57 57 58 58 59 class IRepositoryProvider(Interface): 59 60 """Provide known named instances of Repository.""" 60 61 … … 73 74 """ 74 75 75 76 77 class 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 76 87 class RepositoryManager(Component): 77 """Component registering the supported version control systems ,88 """Component registering the supported version control systems. 78 89 79 90 It provides easy access to the configured implementation. 80 91 """ … … 83 94 84 95 connectors = ExtensionPoint(IRepositoryConnector) 85 96 providers = ExtensionPoint(IRepositoryProvider) 97 change_listeners = ExtensionPoint(IRepositoryChangeListener) 86 98 87 99 repository_type = Option('trac', 'repository_type', 'svn', 88 100 """Default repository connector type. (''since 0.10'')""") … … 103 115 if handler is not Chrome(self.env): 104 116 try: 105 117 # 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 107 120 except TracError, e: 108 121 add_warning(req, _("Can't synchronize with the repository " 109 122 "(%(error)s). Look in the Trac log for more " … … 184 197 # Public API methods 185 198 186 199 def get_repository(self, reponame, authname): 187 """Retrieve the appropriate Repository for the given name 200 """Retrieve the appropriate Repository for the given name. 188 201 189 202 :param reponame: the key for specifying the repository. 190 203 If no name is given, take the the default … … 195 208 """ 196 209 repoinfo = self.get_all_repositories().get(reponame, {}) 197 210 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) 199 213 if repoinfo: 200 214 rdir = repoinfo.get('dir') 201 215 rtype = repoinfo.get('type', self.repository_type) … … 251 265 return (reponame, self.get_repository(reponame, authname), path or '/') 252 266 253 267 def get_default_repository(self, context): 254 """Recover the appropriate trepository from the current context.268 """Recover the appropriate repository from the current context. 255 269 256 270 Lookup the closest source or changeset resource in the context 257 271 hierarchy and return the name of its associated repository. … … 273 287 else: 274 288 self._all_repositories[reponame] = info 275 289 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 276 302 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 277 342 def shutdown(self, tid=None): 278 343 if tid: 279 344 assert tid == threading._get_ident() … … 345 410 """Close the connection to the repository.""" 346 411 raise NotImplementedError 347 412 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 348 422 def clear(self, youngest_rev=None): 349 423 """Clear any data that may have been cached in instance properties. 350 424 … … 360 434 The backend will call this function for each `rev` it decided to 361 435 synchronize, once the synchronization changes are committed to the 362 436 cache. 437 438 Return True if any changes have been committed to the cache. 363 439 """ 364 pass440 return False 365 441 366 442 def sync_changeset(self, rev): 367 443 """Resync the repository cache for the given `rev`, if relevant.""" 368 raise NotImplementedError444 pass 369 445 370 446 def get_quickjump_entries(self, rev): 371 447 """Generate a list of interesting places in the repository. … … 378 454 return [] 379 455 380 456 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`.""" 382 458 raise NotImplementedError 383 459 384 460 def get_changesets(self, start, stop): … … 457 533 return row and row[0] or None 458 534 459 535 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. 461 537 462 538 If given, `rev` is used as a starting point (i.e. no revision 463 539 ''newer'' than `rev` should be returned). … … 636 712 return [] 637 713 638 714 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. 640 716 641 717 The tuple will contain `(path, kind, change, base_path, base_rev)`, 642 718 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 41 41 has_linear_changesets = False 42 42 43 43 def __init__(self, getdb, repos, authz, log): 44 self.repos = repos 44 45 Repository.__init__(self, repos.name, authz, log) 45 46 if callable(getdb): 46 47 self.getdb = getdb 47 48 else: 48 49 self.getdb = lambda: getdb 49 self.repos = repos50 50 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 51 57 def close(self): 52 58 self.repos.close() 53 59 60 def get_base(self): 61 return self.repos.get_base() 62 54 63 def get_quickjump_entries(self, rev): 55 64 for category, name, path, rev in self.repos.get_quickjump_entries(rev): 56 65 yield category, name, path, rev … … 63 72 db = self.getdb() 64 73 cursor = db.cursor() 65 74 cursor.execute("SELECT rev FROM revision " 66 "WHERE time >= %s AND time < %s"75 "WHERE repos=%s AND time >= %s AND time < %s" 67 76 "ORDER BY time DESC, rev DESC", 68 (to_timestamp(start), to_timestamp(stop))) 77 (self.reponame, to_timestamp(start), 78 to_timestamp(stop))) 69 79 for rev, in cursor: 70 80 try: 71 81 if self.authz.has_permission_for_changeset(rev): … … 78 88 db = self.getdb() 79 89 cursor = db.cursor() 80 90 cursor.execute("UPDATE revision SET time=%s, author=%s, message=%s " 81 "WHERE re v=%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))) 84 94 db.commit() 85 95 86 96 def sync(self, feedback=None): 87 97 db = self.getdb() 88 98 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) 91 103 metadata = {} 92 104 for name, value in cursor: 93 105 metadata[name] = value … … 103 115 "'trac-admin resync' operation is needed.")) 104 116 elif repository_dir is None: # 105 117 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)) 108 121 else: # 'repository_dir' cleared by a resync 109 122 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)) 114 126 115 127 # -- retrieve the youngest revision in the repository 116 128 self.repos.clear() 117 129 repos_youngest = self.repos.youngest_rev 118 130 119 131 # -- 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 124 139 125 140 if self.youngest: 126 141 self.youngest = self.repos.normalize_rev(self.youngest) … … 150 165 find_initial_rev=True) 151 166 next_youngest = self.repos.normalize_rev(next_youngest) 152 167 except TracError: 153 return # can't normalize oldest_rev: repository was empty 168 # can't normalize oldest_rev: repository was empty 169 return False 154 170 155 171 if next_youngest is None: # nothing to cache yet 156 return 172 return False 157 173 158 174 # 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))) 161 178 for rev, in cursor: 162 179 # already there, but in progress, so keep ''previous'' 163 180 # notion of 'youngest' 164 181 self.repos.clear(youngest_rev=self.youngest) 165 return 182 return False 166 183 167 184 # 1. prepare for resyncing 168 185 # (there still might be a race condition at this point) … … 182 199 cset = self.repos.get_changeset(next_youngest) 183 200 try: 184 201 cursor.execute("INSERT INTO revision " 185 " (re v,time,author,message) "186 "VALUES (%s,%s,%s,%s )",187 (s tr(next_youngest),202 " (repos,rev,time,author,message) " 203 "VALUES (%s,%s,%s,%s,%s)", 204 (self.reponame, str(next_youngest), 188 205 to_timestamp(cset.date), 189 206 cset.author, cset.message)) 190 207 except Exception, e: # *another* 1.1. resync attempt won … … 194 211 # notion of 'youngest' 195 212 self.repos.clear(youngest_rev=self.youngest) 196 213 db.rollback() 197 return 214 return False 198 215 199 216 # 1.2. now *only* one process was able to get there 200 217 # (i.e. there *shouldn't* be any race condition here) … … 206 223 kind = kindmap[kind] 207 224 action = actionmap[action] 208 225 cursor.execute("INSERT INTO node_change " 209 " (re v,path,node_type,change_type,"210 " base_path,base_rev) "211 "VALUES (%s,%s,%s,%s,%s,%s )",212 (s tr(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), 213 230 path, kind, action, bpath, brev)) 214 231 215 232 # 1.3. iterate (1.1 should always succeed now) … … 218 235 219 236 # 1.4. update 'youngest_rev' metadata 220 237 # (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)) 223 242 db.commit() 224 243 225 244 # 1.5. provide some feedback 226 245 if feedback: 227 246 feedback(self.youngest) 247 248 return True 228 249 finally: 229 250 # 3. restore permission checking (after 1.) 230 251 self.repos.authz = authz … … 240 261 241 262 def get_youngest_rev(self): 242 263 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) 244 270 return self.youngest 245 271 246 272 def previous_rev(self, rev, path=''): … … 258 284 def _next_prev_rev(self, direction, rev, path=''): 259 285 db = self.getdb() 260 286 # 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 " + \ 262 288 db.cast('rev', 'int') + " " + direction + " %s" 263 args = [ rev]289 args = [self.reponame, rev] 264 290 265 291 if path: 266 292 path = path.lstrip('/') … … 316 342 db = self.getdb() 317 343 cursor = db.cursor() 318 344 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))) 320 347 row = cursor.fetchone() 321 348 if row: 322 349 _date, author, message = row … … 330 357 db = self.getdb() 331 358 cursor = db.cursor() 332 359 cursor.execute("SELECT path,node_type,change_type,base_path,base_rev " 333 "FROM node_change WHERE re v=%s "334 "ORDER BY path", (s tr(self.rev),))360 "FROM node_change WHERE repos=%s AND rev=%s " 361 "ORDER BY path", (self.repos.reponame, str(self.rev))) 335 362 for path, kind, change, base_path, base_rev in cursor: 336 363 if not self.authz.has_permission(posixpath.join(self.scope, 337 364 path.strip('/'))): -
trac/versioncontrol/svn_fs.py
diff --git a/trac/versioncontrol/svn_fs.py b/trac/versioncontrol/svn_fs.py
a b 411 411 412 412 # Remove any trailing slash or else subversion might abort 413 413 if isinstance(path, unicode): 414 self.path = path415 414 path_utf8 = path.encode('utf-8') 416 415 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 419 418 path_utf8 = os.path.normpath(path_utf8).replace('\\', '/') 419 self.path = path_utf8.decode('utf-8') 420 420 421 root_path_utf8 = repos.svn_repos_find_root_path(path_utf8, self.pool()) 421 422 if root_path_utf8 is None: 422 423 raise TracError(_("%(path)s does not appear to be a Subversion " … … 431 432 self.fs_ptr = repos.svn_repos_fs(self.repos) 432 433 433 434 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) 435 437 436 438 Repository.__init__(self, name, authz, log) 437 439 … … 484 486 def close(self): 485 487 self.repos = self.fs_ptr = self.pool = None 486 488 489 def get_base(self): 490 return self.base 491 487 492 def _get_tags_or_branches(self, paths): 488 493 """Retrieve known branches or tags.""" 489 494 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 26 26 def test_raise_NotImplementedError_close(self): 27 27 self.failUnlessRaises(NotImplementedError, self.repo_base.close) 28 28 29 def test_raise_NotImplementedError_sync_changeset(self):30 self.failUnlessRaises(NotImplementedError, self.repo_base.sync_changeset, 1)31 32 29 def test_raise_NotImplementedError_get_changeset(self): 33 30 self.failUnlessRaises(NotImplementedError, self.repo_base.get_changeset, 1) 34 31 -
trac/versioncontrol/tests/cache.py
diff --git a/trac/versioncontrol/tests/cache.py b/trac/versioncontrol/tests/cache.py
a b 32 32 self.db = InMemoryDatabase() 33 33 self.log = logger_factory('test') 34 34 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', '')) 37 38 38 39 def test_initial_sync_with_empty_repos(self): 39 40 t = datetime(2001, 1, 1, 1, 1, 1, 0, utc) … … 91 92 t2 = datetime(2002, 1, 1, 1, 1, 1, 0, utc) 92 93 t3 = datetime(2003, 1, 1, 1, 1, 1, 0, utc) 93 94 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)", 101 104 [('trunk', 'D', 'A', None, None), 102 105 ('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'") 104 108 105 109 changes = [('trunk/README', Node.FILE, Changeset.EDIT, 'trunk/README', 1)] 106 110 changeset = Mock(Changeset, 2, 'Update', 'joe', t3, … … 128 132 t1 = datetime(2001, 1, 1, 1, 1, 1, 0, utc) 129 133 t2 = datetime(2002, 1, 1, 1, 1, 1, 0, utc) 130 134 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)", 138 144 [('trunk', 'D', 'A', None, None), 139 145 ('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'") 141 148 142 149 repos = Mock(Repository, 'test-repos', None, self.log, 143 150 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 908 908 909 909 single = rev_a == rev_b 910 910 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 ', 912 913 single and 1 or 2, repo=reponame) 913 914 else: 914 title = ngettext('Changeset ', 'Changesets', single and 1 or 2)915 title = ngettext('Changeset ', 'Changesets ', single and 1 or 2) 915 916 if single: 916 title = tag(title, tag.em(' [%s]' % rev_a))917 title = tag(title, tag.em('[%s]' % rev_a)) 917 918 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))) 919 920 if field == 'title': 920 921 return title 921 922 elif field == 'summary':
