diff --git a/trac/versioncontrol/admin.py b/trac/versioncontrol/admin.py
|
a
|
b
|
|
| 201 | 201 | from trac.versioncontrol.cache import CACHE_METADATA_KEYS |
| 202 | 202 | db = self.env.get_db_cnx() |
| 203 | 203 | cursor = db.cursor() |
| 204 | | inval = False |
| 205 | 204 | for repos in sorted(repositories, key=lambda r: r.reponame): |
| 206 | 205 | reponame = repos.reponame |
| 207 | 206 | printout(_('Resyncing repository history for %(reponame)s... ', |
| … |
… |
|
| 219 | 218 | [(reponame, k, '') |
| 220 | 219 | for k in CACHE_METADATA_KEYS]) |
| 221 | 220 | db.commit() |
| 222 | | if repos.sync(self._sync_feedback): |
| 223 | | inval = True |
| | 221 | repos.sync(self._sync_feedback) |
| 224 | 222 | cursor.execute("SELECT count(rev) FROM revision WHERE repos=%s", |
| 225 | 223 | (reponame,)) |
| 226 | 224 | for cnt, in cursor: |
| 227 | 225 | printout(ngettext('%(num)s revision cached.', |
| 228 | 226 | '%(num)s revisions cached.', num=cnt)) |
| 229 | | if inval: |
| 230 | | self.config.touch() # FIXME: Brute force |
| 231 | 227 | printout(_('Done.')) |
| 232 | 228 | |
| 233 | 229 | def _sync_feedback(self, rev): |
diff --git a/trac/versioncontrol/api.py b/trac/versioncontrol/api.py
|
a
|
b
|
|
| 391 | 391 | self.log.warn("Found no repositories matching '%s' base.", |
| 392 | 392 | base or reponame) |
| 393 | 393 | return |
| 394 | | inval = False |
| 395 | 394 | for repos in sorted(repositories, key=lambda r: r.reponame): |
| 396 | | if repos.sync(): |
| 397 | | inval = True |
| 398 | | else: |
| 399 | | self.log.debug("Repository %s already up-to-date.", repos.name) |
| | 395 | repos.sync() |
| 400 | 396 | for rev in revs: |
| 401 | 397 | args = [] |
| 402 | 398 | if event == 'changeset_modified': |
| … |
… |
|
| 405 | 401 | changeset = repos.get_changeset(rev) |
| 406 | 402 | except NoSuchChangeset: |
| 407 | 403 | continue |
| 408 | | inval = inval or (event == 'changeset_modified') |
| 409 | 404 | self.log.debug("Event %s on %s for revision %s", |
| 410 | | event, repos.reponame, rev) |
| | 405 | event, repos.reponame or '(default)', rev) |
| 411 | 406 | for listener in self.change_listeners: |
| 412 | 407 | getattr(listener, event)(repos, changeset, *args) |
| 413 | | |
| 414 | | if inval: |
| 415 | | self.log.debug("Invalidating youngest_rev cache") |
| 416 | | self.config.touch() # FIXME: Brute force method |
| 417 | 408 | |
| 418 | 409 | def shutdown(self, tid=None): |
| 419 | 410 | if tid: |
| … |
… |
|
| 510 | 501 | The backend will call this function for each `rev` it decided to |
| 511 | 502 | synchronize, once the synchronization changes are committed to the |
| 512 | 503 | cache. |
| 513 | | |
| 514 | | Return True if any changes have been committed to the cache. |
| 515 | 504 | """ |
| 516 | | return False |
| | 505 | pass |
| 517 | 506 | |
| 518 | 507 | def sync_changeset(self, rev): |
| 519 | 508 | """Resync the repository cache for the given `rev`, if relevant. |
diff --git a/trac/versioncontrol/cache.py b/trac/versioncontrol/cache.py
|
a
|
b
|
|
| 18 | 18 | import posixpath |
| 19 | 19 | from datetime import datetime |
| 20 | 20 | |
| | 21 | from trac.cache import CacheProxy |
| 21 | 22 | from trac.core import TracError |
| 22 | 23 | from trac.util.datefmt import utc, to_timestamp |
| 23 | 24 | from trac.util.translation import _ |
| … |
… |
|
| 40 | 41 | |
| 41 | 42 | has_linear_changesets = False |
| 42 | 43 | |
| 43 | | def __init__(self, getdb, repos, authz, log): |
| | 44 | def __init__(self, env, repos, authz, log): |
| | 45 | self.env = env |
| 44 | 46 | self.repos = repos |
| | 47 | self.metadata = CacheProxy(self.__class__.__module__ + '.' |
| | 48 | + self.__class__.__name__ + '.metadata:' |
| | 49 | + self.repos.reponame, self._metadata, |
| | 50 | self.env) |
| 45 | 51 | Repository.__init__(self, repos.name, authz, log) |
| 46 | | if callable(getdb): |
| 47 | | self.getdb = getdb |
| 48 | | else: |
| 49 | | self.getdb = lambda: getdb |
| 50 | 52 | |
| 51 | 53 | def _set_reponame(self, value): |
| 52 | 54 | self.repos.reponame = value |
| | 55 | self.metadata.id = self.__class__.__module__ + '.' \ |
| | 56 | + self.__class__.__name__ + '.metadata:' \ |
| | 57 | + value |
| 53 | 58 | |
| 54 | 59 | reponame = property(fget=lambda self: self.repos.reponame, |
| 55 | 60 | fset=_set_reponame) |
| … |
… |
|
| 66 | 71 | |
| 67 | 72 | def get_changeset(self, rev): |
| 68 | 73 | return CachedChangeset(self.repos, self.repos.normalize_rev(rev), |
| 69 | | self.getdb, self.authz) |
| | 74 | self.env, self.authz) |
| 70 | 75 | |
| 71 | 76 | def get_changesets(self, start, stop): |
| 72 | | db = self.getdb() |
| | 77 | db = self.env.get_db_cnx() |
| 73 | 78 | cursor = db.cursor() |
| 74 | 79 | cursor.execute("SELECT rev FROM revision " |
| 75 | 80 | "WHERE repos=%s AND time >= %s AND time < %s" |
| … |
… |
|
| 85 | 90 | |
| 86 | 91 | def sync_changeset(self, rev): |
| 87 | 92 | cset = self.repos.get_changeset(rev) |
| 88 | | db = self.getdb() |
| | 93 | db = self.env.get_db_cnx() |
| 89 | 94 | cursor = db.cursor() |
| 90 | 95 | cursor.execute("SELECT time,author,message FROM revision " |
| 91 | 96 | "WHERE repos=%s AND rev=%s", |
| … |
… |
|
| 102 | 107 | db.commit() |
| 103 | 108 | return old_changeset |
| 104 | 109 | |
| 105 | | # @cached? => move to RepositoryManager |
| 106 | | def metadata(self, db=None): |
| 107 | | if not db: |
| 108 | | db = self.getdb() |
| | 110 | def _metadata(self, db): |
| | 111 | """Retrieve data for the cached `metadata` attribute.""" |
| 109 | 112 | cursor = db.cursor() |
| 110 | 113 | cursor.execute("SELECT name, value FROM repository " |
| 111 | 114 | "WHERE id=%%s AND name IN (%s)" % |
| 112 | 115 | ','.join(['%s'] * len(CACHE_METADATA_KEYS)), |
| 113 | 116 | (self.reponame,) + CACHE_METADATA_KEYS) |
| 114 | | metadata = {} |
| 115 | | for name, value in cursor: |
| 116 | | metadata[name] = value |
| 117 | | return metadata |
| | 117 | return dict(cursor) |
| 118 | 118 | |
| 119 | 119 | def sync(self, feedback=None): |
| 120 | | db = self.getdb() |
| | 120 | db = self.env.get_db_cnx() |
| 121 | 121 | cursor = db.cursor() |
| 122 | | metadata = self.metadata(db) |
| | 122 | metadata = self.metadata.get(db) |
| | 123 | do_commit = False |
| 123 | 124 | |
| 124 | 125 | # -- check that we're populating the cache for the correct repository |
| 125 | 126 | repository_dir = metadata.get(CACHE_REPOSITORY_DIR) |
| … |
… |
|
| 135 | 136 | cursor.execute("INSERT INTO repository (id,name,value) " |
| 136 | 137 | "VALUES (%s,%s,%s)", |
| 137 | 138 | (self.reponame, CACHE_REPOSITORY_DIR, self.name)) |
| | 139 | do_commit = True |
| 138 | 140 | else: # 'repository_dir' cleared by a resync |
| 139 | 141 | self.log.info('Resetting "repository_dir": %s' % self.name) |
| 140 | 142 | cursor.execute("UPDATE repository SET value=%s " |
| 141 | 143 | "WHERE id=%s AND name=%s", |
| 142 | 144 | (self.name, self.reponame, CACHE_REPOSITORY_DIR)) |
| | 145 | do_commit = True |
| 143 | 146 | |
| 144 | 147 | # -- retrieve the youngest revision in the repository |
| 145 | 148 | self.repos.clear() |
| 146 | 149 | repos_youngest = self.repos.youngest_rev |
| 147 | 150 | |
| 148 | 151 | # -- retrieve the youngest revision cached so far |
| 149 | | self.youngest = metadata.get(CACHE_YOUNGEST_REV) |
| 150 | | if self.youngest is None: |
| | 152 | youngest = metadata.get(CACHE_YOUNGEST_REV) |
| | 153 | if youngest is None: |
| 151 | 154 | cursor.execute("INSERT INTO repository (id,name,value) " |
| 152 | 155 | "VALUES (%s,%s,%s)", |
| 153 | 156 | (self.reponame, CACHE_YOUNGEST_REV, '')) |
| | 157 | do_commit = True |
| 154 | 158 | |
| 155 | | db.commit() # save metadata changes made up to now |
| | 159 | if do_commit: |
| | 160 | self.metadata.invalidate(db) |
| | 161 | db.commit() # save metadata changes made up to now |
| 156 | 162 | |
| 157 | | if self.youngest: |
| 158 | | self.youngest = self.repos.normalize_rev(self.youngest) |
| 159 | | if not self.youngest: |
| | 163 | if youngest: |
| | 164 | youngest = self.repos.normalize_rev(youngest) |
| | 165 | if not youngest: |
| 160 | 166 | self.log.debug('normalize_rev failed (youngest_rev=%r)' % |
| 161 | 167 | self.youngest_rev) |
| 162 | 168 | else: |
| 163 | 169 | self.log.debug('cache metadata undefined (youngest_rev=%r)' % |
| 164 | 170 | self.youngest_rev) |
| 165 | | self.youngest = None |
| | 171 | youngest = None |
| 166 | 172 | |
| 167 | 173 | # -- compare them and try to resync if different |
| 168 | | if self.youngest != repos_youngest: |
| | 174 | if youngest != repos_youngest: |
| 169 | 175 | self.log.info("repos rev [%s] != cached rev [%s]" % |
| 170 | | (repos_youngest, self.youngest)) |
| 171 | | if self.youngest: |
| 172 | | next_youngest = self.repos.next_rev(self.youngest) |
| | 176 | (repos_youngest, youngest)) |
| | 177 | if youngest: |
| | 178 | next_youngest = self.repos.next_rev(youngest) |
| 173 | 179 | else: |
| 174 | 180 | next_youngest = None |
| 175 | 181 | try: |
| … |
… |
|
| 183 | 189 | next_youngest = self.repos.normalize_rev(next_youngest) |
| 184 | 190 | except TracError: |
| 185 | 191 | # can't normalize oldest_rev: repository was empty |
| 186 | | return False |
| | 192 | return |
| 187 | 193 | |
| 188 | 194 | if next_youngest is None: # nothing to cache yet |
| 189 | | return False |
| | 195 | return |
| 190 | 196 | |
| 191 | 197 | # 0. first check if there's no (obvious) resync in progress |
| 192 | 198 | cursor.execute("SELECT rev FROM revision " |
| … |
… |
|
| 195 | 201 | for rev, in cursor: |
| 196 | 202 | # already there, but in progress, so keep ''previous'' |
| 197 | 203 | # notion of 'youngest' |
| 198 | | self.repos.clear(youngest_rev=self.youngest) |
| 199 | | return False |
| | 204 | self.repos.clear(youngest_rev=youngest) |
| | 205 | return |
| 200 | 206 | |
| 201 | 207 | # 1. prepare for resyncing |
| 202 | 208 | # (there still might be a race condition at this point) |
| … |
… |
|
| 226 | 232 | (next_youngest, e)) |
| 227 | 233 | # also potentially in progress, so keep ''previous'' |
| 228 | 234 | # notion of 'youngest' |
| 229 | | self.repos.clear(youngest_rev=self.youngest) |
| | 235 | self.repos.clear(youngest_rev=youngest) |
| 230 | 236 | db.rollback() |
| 231 | | return False |
| | 237 | return |
| 232 | 238 | |
| 233 | 239 | # 1.2. now *only* one process was able to get there |
| 234 | 240 | # (i.e. there *shouldn't* be any race condition here) |
| … |
… |
|
| 247 | 253 | path, kind, action, bpath, brev)) |
| 248 | 254 | |
| 249 | 255 | # 1.3. iterate (1.1 should always succeed now) |
| 250 | | self.youngest = next_youngest |
| | 256 | youngest = next_youngest |
| 251 | 257 | next_youngest = self.repos.next_rev(next_youngest) |
| 252 | 258 | |
| 253 | 259 | # 1.4. update 'youngest_rev' metadata |
| 254 | 260 | # (minimize possibility of failures at point 0.) |
| 255 | 261 | cursor.execute("UPDATE repository SET value=%s " |
| 256 | 262 | "WHERE id=%s AND name=%s", |
| 257 | | (str(self.youngest), self.reponame, |
| | 263 | (str(youngest), self.reponame, |
| 258 | 264 | CACHE_YOUNGEST_REV)) |
| | 265 | self.metadata.invalidate(db) |
| 259 | 266 | db.commit() |
| 260 | 267 | |
| 261 | 268 | # 1.5. provide some feedback |
| 262 | 269 | if feedback: |
| 263 | | feedback(self.youngest) |
| 264 | | |
| 265 | | return True |
| | 270 | feedback(youngest) |
| 266 | 271 | finally: |
| 267 | 272 | # 3. restore permission checking (after 1.) |
| 268 | 273 | self.repos.authz = authz |
| … |
… |
|
| 277 | 282 | return self.repos.oldest_rev |
| 278 | 283 | |
| 279 | 284 | def get_youngest_rev(self): |
| 280 | | if not hasattr(self, 'youngest'): |
| 281 | | metadata = self.metadata() |
| 282 | | self.youngest = metadata.get(CACHE_YOUNGEST_REV) |
| 283 | | return self.youngest |
| | 285 | return self.metadata.get().get(CACHE_YOUNGEST_REV) |
| 284 | 286 | |
| 285 | 287 | def previous_rev(self, rev, path=''): |
| 286 | 288 | if self.has_linear_changesets: |
| … |
… |
|
| 295 | 297 | return self.repos.next_rev(rev, path) |
| 296 | 298 | |
| 297 | 299 | def _next_prev_rev(self, direction, rev, path=''): |
| 298 | | db = self.getdb() |
| | 300 | db = self.env.get_db_cnx() |
| 299 | 301 | # the changeset revs are sequence of ints: |
| 300 | 302 | sql = "SELECT rev FROM node_change WHERE repos=%s AND " + \ |
| 301 | 303 | db.cast('rev', 'int') + " " + direction + " %s" |
| … |
… |
|
| 348 | 350 | |
| 349 | 351 | class CachedChangeset(Changeset): |
| 350 | 352 | |
| 351 | | def __init__(self, repos, rev, getdb, authz): |
| | 353 | def __init__(self, repos, rev, env, authz): |
| 352 | 354 | self.repos = repos |
| 353 | | self.getdb = getdb |
| | 355 | self.env = env |
| 354 | 356 | self.authz = authz |
| 355 | | db = self.getdb() |
| | 357 | db = self.env.get_db_cnx() |
| 356 | 358 | cursor = db.cursor() |
| 357 | 359 | cursor.execute("SELECT time,author,message FROM revision " |
| 358 | 360 | "WHERE repos=%s AND rev=%s", |
| … |
… |
|
| 367 | 369 | self.scope = getattr(repos, 'scope', '') |
| 368 | 370 | |
| 369 | 371 | def get_changes(self): |
| 370 | | db = self.getdb() |
| | 372 | db = self.env.get_db_cnx() |
| 371 | 373 | cursor = db.cursor() |
| 372 | 374 | cursor.execute("SELECT path,node_type,change_type,base_path,base_rev " |
| 373 | 375 | "FROM node_change WHERE repos=%s AND rev=%s " |
diff --git a/trac/versioncontrol/svn_fs.py b/trac/versioncontrol/svn_fs.py
|
a
|
b
|
|
| 289 | 289 | if type == 'direct-svnfs': |
| 290 | 290 | repos = fs_repos |
| 291 | 291 | else: |
| 292 | | repos = CachedRepository(self.env.get_db_cnx, fs_repos, None, |
| 293 | | self.log) |
| | 292 | repos = CachedRepository(self.env, fs_repos, None, self.log) |
| 294 | 293 | repos.has_linear_changesets = True |
| 295 | 294 | # FIXME: convert SubversionAuthorizer to a PermissionPolicy |
| 296 | 295 | if 'authname' in options: |
diff --git a/trac/versioncontrol/tests/cache.py b/trac/versioncontrol/tests/cache.py
|
a
|
b
|
|
| 17 | 17 | from datetime import datetime |
| 18 | 18 | |
| 19 | 19 | from trac.log import logger_factory |
| 20 | | from trac.test import Mock, InMemoryDatabase |
| | 20 | from trac.test import EnvironmentStub, Mock, InMemoryDatabase |
| 21 | 21 | from trac.util.datefmt import to_timestamp, utc |
| 22 | 22 | from trac.versioncontrol import Repository, Changeset, Node, NoSuchChangeset |
| 23 | 23 | from trac.versioncontrol.cache import CachedRepository |
| … |
… |
|
| 29 | 29 | class CacheTestCase(unittest.TestCase): |
| 30 | 30 | |
| 31 | 31 | def setUp(self): |
| 32 | | self.db = InMemoryDatabase() |
| 33 | | self.log = logger_factory('test') |
| | 32 | self.env = EnvironmentStub() |
| | 33 | self.db = self.env.get_db_cnx() |
| | 34 | self.log = self.env.log |
| 34 | 35 | cursor = self.db.cursor() |
| 35 | 36 | cursor.execute("INSERT INTO repository (id, name, value) " |
| 36 | 37 | "VALUES (%s,%s,%s)", |
| … |
… |
|
| 47 | 48 | get_youngest_rev=lambda: 0, |
| 48 | 49 | normalize_rev=no_changeset, |
| 49 | 50 | next_rev=lambda x: None) |
| 50 | | cache = CachedRepository(self.db, repos, None, self.log) |
| | 51 | cache = CachedRepository(self.env, repos, None, self.log) |
| 51 | 52 | cache.sync() |
| 52 | 53 | |
| 53 | 54 | cursor = self.db.cursor() |
| … |
… |
|
| 71 | 72 | get_youngest_rev=lambda: 1, |
| 72 | 73 | normalize_rev=lambda x: x, |
| 73 | 74 | next_rev=lambda x: int(x) == 0 and 1 or None) |
| 74 | | cache = CachedRepository(self.db, repos, None, self.log) |
| | 75 | cache = CachedRepository(self.env, repos, None, self.log) |
| 75 | 76 | cache.sync() |
| 76 | 77 | |
| 77 | 78 | cursor = self.db.cursor() |
| … |
… |
|
| 115 | 116 | get_oldest_rev=lambda: 0, |
| 116 | 117 | normalize_rev=lambda x: x, |
| 117 | 118 | next_rev=lambda x: x and int(x) == 1 and 2 or None) |
| 118 | | cache = CachedRepository(self.db, repos, None, self.log) |
| | 119 | cache = CachedRepository(self.env, repos, None, self.log) |
| 119 | 120 | cache.sync() |
| 120 | 121 | |
| 121 | 122 | cursor = self.db.cursor() |
| … |
… |
|
| 152 | 153 | get_oldest_rev=lambda: 0, |
| 153 | 154 | next_rev=lambda x: None, |
| 154 | 155 | normalize_rev=lambda rev: rev) |
| 155 | | cache = CachedRepository(self.db, repos, None, self.log) |
| | 156 | cache = CachedRepository(self.env, repos, None, self.log) |
| 156 | 157 | self.assertEqual('1', cache.youngest_rev) |
| 157 | 158 | changeset = cache.get_changeset(1) |
| 158 | 159 | self.assertEqual('joe', changeset.author) |