| | 447 | class MercurialCachedRepository(CachedRepository): |
| | 448 | |
| | 449 | def display_rev(self, rev): |
| | 450 | return self.repos.display_rev(rev) |
| | 451 | |
| | 452 | def normalize_rev(self, rev): |
| | 453 | if rev is None or isinstance(rev, basestring) and \ |
| | 454 | rev.lower() in ('', 'head', 'latest', 'youngest'): |
| | 455 | return self.rev_db(self.youngest_rev or nullrev) |
| | 456 | else: |
| | 457 | return self.repos.normalize_rev(rev) |
| | 458 | |
| | 459 | def db_rev(self, rev): |
| | 460 | return self.repos.short_rev(rev) |
| | 461 | |
| | 462 | def rev_db(self, rev): |
| | 463 | return self.repos.normalize_rev(rev) |
| | 464 | |
| | 465 | def get_changeset(self, rev): |
| | 466 | return MercurialCachedChangeset(self, self.normalize_rev(rev), self.env) |
| | 467 | |
| | 468 | def sync(self, feedback=None, clean=False): |
| | 469 | if clean: |
| | 470 | self.log.info('Cleaning cache') |
| | 471 | @self.env.with_transaction() |
| | 472 | def do_clean(db): |
| | 473 | cursor = db.cursor() |
| | 474 | cursor.execute("DELETE FROM revision WHERE repos=%s", |
| | 475 | (self.id,)) |
| | 476 | cursor.execute("DELETE FROM node_change WHERE repos=%s", |
| | 477 | (self.id,)) |
| | 478 | cursor.executemany(""" |
| | 479 | DELETE FROM repository WHERE id=%s AND name=%s |
| | 480 | """, [(self.id, k) for k in CACHE_METADATA_KEYS]) |
| | 481 | cursor.executemany(""" |
| | 482 | INSERT INTO repository (id,name,value) VALUES (%s,%s,%s) |
| | 483 | """, [(self.id, k, '') for k in CACHE_METADATA_KEYS]) |
| | 484 | del self.metadata |
| | 485 | |
| | 486 | metadata = self.metadata |
| | 487 | |
| | 488 | @self.env.with_transaction() |
| | 489 | def do_transaction(db): |
| | 490 | cursor = db.cursor() |
| | 491 | invalidate = False |
| | 492 | |
| | 493 | # -- check that we're populating the cache for the correct |
| | 494 | # repository |
| | 495 | repository_dir = metadata.get(CACHE_REPOSITORY_DIR) |
| | 496 | if repository_dir: |
| | 497 | # directory part of the repo name can vary on case insensitive |
| | 498 | # fs |
| | 499 | if os.path.normcase(repository_dir) \ |
| | 500 | != os.path.normcase(self.name): |
| | 501 | self.log.info("'repository_dir' has changed from %r to %r", |
| | 502 | repository_dir, self.name) |
| | 503 | raise TracError(_("The repository directory has changed, " |
| | 504 | "you should resynchronize the " |
| | 505 | "repository with: trac-admin $ENV " |
| | 506 | "repository resync '%(reponame)s'", |
| | 507 | reponame=self.reponame or '(default)')) |
| | 508 | elif repository_dir is None: # |
| | 509 | self.log.info('Storing initial "repository_dir": %s', |
| | 510 | self.name) |
| | 511 | cursor.execute(""" |
| | 512 | INSERT INTO repository (id,name,value) VALUES (%s,%s,%s) |
| | 513 | """, (self.id, CACHE_REPOSITORY_DIR, self.name)) |
| | 514 | invalidate = True |
| | 515 | else: # 'repository_dir' cleared by a resync |
| | 516 | self.log.info('Resetting "repository_dir": %s', self.name) |
| | 517 | cursor.execute(""" |
| | 518 | UPDATE repository SET value=%s WHERE id=%s AND name=%s |
| | 519 | """, (self.name, self.id, CACHE_REPOSITORY_DIR)) |
| | 520 | invalidate = True |
| | 521 | |
| | 522 | # -- insert a 'youngeset_rev' for the repository if necessary |
| | 523 | if metadata.get(CACHE_YOUNGEST_REV) is None: |
| | 524 | cursor.execute(""" |
| | 525 | INSERT INTO repository (id,name,value) VALUES (%s,%s,%s) |
| | 526 | """, (self.id, CACHE_YOUNGEST_REV, '')) |
| | 527 | invalidate = True |
| | 528 | |
| | 529 | if invalidate: |
| | 530 | del self.metadata |
| | 531 | |
| | 532 | # -- retrieve the youngest revision in the repository and the youngest |
| | 533 | # revision cached so far |
| | 534 | self.repos.clear() |
| | 535 | |
| | 536 | # -- compare them and try to resync if different |
| | 537 | next_youngest = None |
| | 538 | seen = {} |
| | 539 | for bt_name, youngest in self.repos.repo.branchtags().iteritems(): |
| | 540 | print 'synchronizing branch/tag %s (head %s)...' % (bt_name, self.display_rev(youngest)) |
| | 541 | |
| | 542 | # 1. prepare for resyncing |
| | 543 | # (there still might be a race condition at this point) |
| | 544 | |
| | 545 | next_youngest = youngest |
| | 546 | kindmap = dict(zip(_kindmap.values(), _kindmap.keys())) |
| | 547 | actionmap = dict(zip(_actionmap.values(), _actionmap.keys())) |
| | 548 | |
| | 549 | while next_youngest is not None: |
| | 550 | srev = self.db_rev(next_youngest) |
| | 551 | exit = [False] |
| | 552 | if not seen.has_key(srev): |
| | 553 | |
| | 554 | @self.env.with_transaction() |
| | 555 | def do_transaction(db): |
| | 556 | cursor = db.cursor() |
| | 557 | |
| | 558 | # 1.1 Attempt to resync the 'revision' table |
| | 559 | self.log.info("Trying to sync revision [%s]", |
| | 560 | next_youngest) |
| | 561 | cset = self.repos.get_changeset(next_youngest) |
| | 562 | try: |
| | 563 | cursor.execute(""" |
| | 564 | INSERT INTO revision |
| | 565 | (repos,rev,time,author,message) |
| | 566 | VALUES (%s,%s,%s,%s,%s) |
| | 567 | """, (self.id, srev, to_utimestamp(cset.date), |
| | 568 | cset.author, cset.message)) |
| | 569 | seen[srev] = True |
| | 570 | except Exception, e: # *another* 1.1. resync attempt won |
| | 571 | self.log.warning('Revision %s already cached: %r', |
| | 572 | next_youngest, e) |
| | 573 | # also potentially in progress, so keep ''previous'' |
| | 574 | # notion of 'youngest' |
| | 575 | self.repos.clear(youngest_rev=youngest) |
| | 576 | # FIXME: This aborts a containing transaction |
| | 577 | db.rollback() |
| | 578 | exit[0] = True |
| | 579 | return |
| | 580 | |
| | 581 | # 1.2. now *only* one process was able to get there |
| | 582 | # (i.e. there *shouldn't* be any race condition here) |
| | 583 | |
| | 584 | for path, kind, action, bpath, brev in cset.get_changes(): |
| | 585 | self.log.debug("Caching node change in [%s]: %r", |
| | 586 | next_youngest, |
| | 587 | (path, kind, action, bpath, brev)) |
| | 588 | kind = kindmap[kind] |
| | 589 | action = actionmap[action] |
| | 590 | cursor.execute(""" |
| | 591 | INSERT INTO node_change |
| | 592 | (repos,rev,path,node_type, |
| | 593 | change_type,base_path,base_rev) |
| | 594 | VALUES (%s,%s,%s,%s,%s,%s,%s) |
| | 595 | """, (self.id, srev, path, kind, action, bpath, |
| | 596 | brev)) |
| | 597 | |
| | 598 | # FIXME: it is not necessary to update everytime in the loop. |
| | 599 | cursor.execute(""" |
| | 600 | UPDATE repository SET value=%s WHERE id=%s AND name=%s |
| | 601 | """, (str(self.repos.changectx().hex()), self.id, CACHE_YOUNGEST_REV)) |
| | 602 | |
| | 603 | if exit[0]: |
| | 604 | return |
| | 605 | |
| | 606 | # 1.4. iterate (1.1 should always succeed now) |
| | 607 | youngest = next_youngest |
| | 608 | next_youngest = self.repos.previous_rev(next_youngest) |
| | 609 | |
| | 610 | # 1.5. provide some feedback |
| | 611 | if feedback: |
| | 612 | feedback(youngest) |
| | 613 | |
| | 614 | def get_node(self, path, rev=None): |
| | 615 | return self.repos.get_node(path, self.normalize_rev(rev)) |
| | 616 | |
| | 617 | def _get_node_revs(self, path, last=None, first=None): |
| | 618 | """Return the revisions affecting `path` between `first` and `last` |
| | 619 | revisions. |
| | 620 | """ |
| | 621 | last = self.normalize_rev(last) |
| | 622 | slast = self.db_rev(last) |
| | 623 | node = self.get_node(path, last) # Check node existence |
| | 624 | db = self.env.get_db_cnx() |
| | 625 | cursor = db.cursor() |
| | 626 | if first is None: |
| | 627 | cursor.execute("SELECT rev FROM node_change " |
| | 628 | "WHERE repos=%s AND rev<=%s " |
| | 629 | " AND path=%s " |
| | 630 | " AND change_type IN ('A', 'C', 'M') " |
| | 631 | "ORDER BY rev DESC LIMIT 1", |
| | 632 | (self.id, slast, path)) |
| | 633 | first = 0 |
| | 634 | for row in cursor: |
| | 635 | first = int(row[0]) |
| | 636 | sfirst = self.db_rev(first) |
| | 637 | cursor.execute("SELECT DISTINCT rev FROM node_change " |
| | 638 | "WHERE repos=%%s AND rev>=%%s AND rev<=%%s " |
| | 639 | " AND (path=%%s OR path %s)" % db.like(), |
| | 640 | (self.id, sfirst, slast, path, |
| | 641 | db.like_escape(path + '/') + '%')) |
| | 642 | return [int(row[0]) for row in cursor] |
| | 643 | |
| | 644 | class MercurialCachedChangeset(CachedChangeset): |
| | 645 | pass |