# HG changeset patch # User Patrick Mezard # Date 1336775047 -7200 # Node ID b6081c2c46471ccb11d183e8afeccf78da918b35 # Parent 4ae3ba9e4d7a2672311ced09b52022269f4bab90 phases: introduce phasecache The original motivation was changectx.phase() had special logic to correctly lookup in repo._phaserev, including invalidating it when necessary. And at other places, repo._phaserev was accessed directly. This led to the discovery that phases state including _phaseroots, _phaserev and _dirtyphase was manipulated in localrepository.py, phases.py, repair.py, etc. phasecache helps encapsulating that. This patch replaces all phase state in localrepo with phasecache and adjust related code except for advance/retractboundary() in phases. These still access to phasecache internals directly. This will be addressed in a followup. diff -r 4ae3ba9e4d7a -r b6081c2c4647 hgext/mq.py --- a/hgext/mq.py Sat May 12 00:19:30 2012 +0200 +++ b/hgext/mq.py Sat May 12 00:24:07 2012 +0200 @@ -883,7 +883,7 @@ def finish(self, repo, revs): # Manually trigger phase computation to ensure phasedefaults is # executed before we remove the patches. - repo._phaserev + repo._phasecache patches = self._revpatches(repo, sorted(revs)) qfinished = self._cleanup(patches, len(patches)) if qfinished and repo.ui.configbool('mq', 'secret', False): diff -r 4ae3ba9e4d7a -r b6081c2c4647 mercurial/commands.py --- a/mercurial/commands.py Sat May 12 00:19:30 2012 +0200 +++ b/mercurial/commands.py Sat May 12 00:24:07 2012 +0200 @@ -4378,7 +4378,7 @@ nodes = [ctx.node() for ctx in repo.set('%ld', revs)] if not nodes: raise util.Abort(_('empty revision set')) - olddata = repo._phaserev[:] + olddata = repo._phasecache.getphaserevs(repo)[:] phases.advanceboundary(repo, targetphase, nodes) if opts['force']: phases.retractboundary(repo, targetphase, nodes) @@ -4386,7 +4386,7 @@ lock.release() if olddata is not None: changes = 0 - newdata = repo._phaserev + newdata = repo._phasecache.getphaserevs(repo) changes = sum(o != newdata[i] for i, o in enumerate(olddata)) rejected = [n for n in nodes if newdata[repo[n].rev()] < targetphase] diff -r 4ae3ba9e4d7a -r b6081c2c4647 mercurial/context.py --- a/mercurial/context.py Sat May 12 00:19:30 2012 +0200 +++ b/mercurial/context.py Sat May 12 00:24:07 2012 +0200 @@ -191,12 +191,7 @@ def bookmarks(self): return self._repo.nodebookmarks(self._node) def phase(self): - if self._rev == -1: - return phases.public - if self._rev >= len(self._repo._phaserev): - # outdated cache - del self._repo._phaserev - return self._repo._phaserev[self._rev] + return self._repo._phasecache.phase(self._repo, self._rev) def phasestr(self): return phases.phasenames[self.phase()] def mutable(self): diff -r 4ae3ba9e4d7a -r b6081c2c4647 mercurial/discovery.py --- a/mercurial/discovery.py Sat May 12 00:19:30 2012 +0200 +++ b/mercurial/discovery.py Sat May 12 00:24:07 2012 +0200 @@ -105,7 +105,7 @@ og.commonheads, _any, _hds = commoninc # compute outgoing - if not repo._phaseroots[phases.secret]: + if not repo._phasecache.phaseroots[phases.secret]: og.missingheads = onlyheads or repo.heads() elif onlyheads is None: # use visible heads as it should be cached diff -r 4ae3ba9e4d7a -r b6081c2c4647 mercurial/localrepo.py --- a/mercurial/localrepo.py Sat May 12 00:19:30 2012 +0200 +++ b/mercurial/localrepo.py Sat May 12 00:24:07 2012 +0200 @@ -41,7 +41,6 @@ self.wopener = scmutil.opener(self.root) self.baseui = baseui self.ui = baseui.copy() - self._dirtyphases = False # A list of callback to shape the phase if no data were found. # Callback are in the form: func(repo, roots) --> processed root. # This list it to be filled by extension during repo setup @@ -182,22 +181,8 @@ bookmarks.write(self) @storecache('phaseroots') - def _phaseroots(self): - phaseroots, self._dirtyphases = phases.readroots( - self, self._phasedefaults) - return phaseroots - - @propertycache - def _phaserev(self): - cache = [phases.public] * len(self) - for phase in phases.trackedphases: - roots = map(self.changelog.rev, self._phaseroots[phase]) - if roots: - for rev in roots: - cache[rev] = phase - for rev in self.changelog.descendants(*roots): - cache[rev] = phase - return cache + def _phasecache(self): + return phases.phasecache(self, self._phasedefaults) @storecache('00changelog.i') def changelog(self): @@ -604,10 +589,11 @@ def known(self, nodes): nm = self.changelog.nodemap + pc = self._phasecache result = [] for n in nodes: r = nm.get(n) - resp = not (r is None or self._phaserev[r] >= phases.secret) + resp = not (r is None or pc.phase(self, r) >= phases.secret) result.append(resp) return result @@ -863,7 +849,6 @@ pass delcache('_tagscache') - delcache('_phaserev') self._branchcache = None # in UTF-8 self._branchcachetip = None @@ -931,9 +916,8 @@ def unlock(): self.store.write() - if self._dirtyphases: - phases.writeroots(self, self._phaseroots) - self._dirtyphases = False + if '_phasecache' in vars(self): + self._phasecache.write() for k, ce in self._filecache.items(): if k == 'dirstate': continue diff -r 4ae3ba9e4d7a -r b6081c2c4647 mercurial/phases.py --- a/mercurial/phases.py Sat May 12 00:19:30 2012 +0200 +++ b/mercurial/phases.py Sat May 12 00:24:07 2012 +0200 @@ -99,7 +99,7 @@ """ import errno -from node import nullid, bin, hex, short +from node import nullid, nullrev, bin, hex, short from i18n import _ allphases = public, draft, secret = range(3) @@ -124,7 +124,7 @@ updated = True return updated -def readroots(repo, phasedefaults=None): +def _readroots(repo, phasedefaults=None): """Read phase roots from disk phasedefaults is a list of fn(repo, roots) callable, which are @@ -156,15 +156,51 @@ dirty = True return roots, dirty -def writeroots(repo, phaseroots): - """Write phase roots from disk""" - f = repo.sopener('phaseroots', 'w', atomictemp=True) - try: - for phase, roots in enumerate(phaseroots): - for h in roots: - f.write('%i %s\n' % (phase, hex(h))) - finally: - f.close() +class phasecache(object): + def __init__(self, repo, phasedefaults): + self.phaseroots, self.dirty = _readroots(repo, phasedefaults) + self.opener = repo.sopener + self._phaserevs = None + + def getphaserevs(self, repo, rebuild=False): + if rebuild or self._phaserevs is None: + revs = [public] * len(repo.changelog) + for phase in trackedphases: + roots = map(repo.changelog.rev, self.phaseroots[phase]) + if roots: + for rev in roots: + revs[rev] = phase + for rev in repo.changelog.descendants(*roots): + revs[rev] = phase + self._phaserevs = revs + return self._phaserevs + + def invalidatephaserevs(self): + self._phaserevs = None + + def phase(self, repo, rev): + # We need a repo argument here to be able to build _phaserev + # if necessary. The repository instance is not stored in + # phasecache to avoid reference cycles. The changelog instance + # is not stored because it is a filecache() property and can + # be replaced without us being notified. + if rev == nullrev: + return public + if self._phaserevs is None or rev >= len(self._phaserevs): + self._phaserevs = self.getphaserevs(repo, rebuild=True) + return self._phaserevs[rev] + + def write(self): + if not self.dirty: + return + f = self.opener('phaseroots', 'w', atomictemp=True) + try: + for phase, roots in enumerate(self.phaseroots): + for h in roots: + f.write('%i %s\n' % (phase, hex(h))) + finally: + f.close() + self.dirty = False def advanceboundary(repo, targetphase, nodes): """Add nodes to a phase changing other nodes phases if necessary. @@ -173,6 +209,8 @@ in the target phase or kept in a *lower* phase. Simplify boundary to contains phase roots only.""" + phcache = repo._phasecache + delroots = [] # set of root deleted by this path for phase in xrange(targetphase + 1, len(allphases)): # filter nodes that are not in a compatible phase already @@ -181,16 +219,15 @@ nodes = [n for n in nodes if repo[n].phase() >= phase] if not nodes: break # no roots to move anymore - roots = repo._phaseroots[phase] + roots = phcache.phaseroots[phase] olds = roots.copy() ctxs = list(repo.set('roots((%ln::) - (%ln::%ln))', olds, olds, nodes)) roots.clear() roots.update(ctx.node() for ctx in ctxs) if olds != roots: # invalidate cache (we probably could be smarter here - if '_phaserev' in vars(repo): - del repo._phaserev - repo._dirtyphases = True + phcache.invalidatephaserevs() + phcache.dirty = True # some roots may need to be declared for lower phases delroots.extend(olds - roots) # declare deleted root in the target phase @@ -205,22 +242,23 @@ in the target phase or kept in a *higher* phase. Simplify boundary to contains phase roots only.""" - currentroots = repo._phaseroots[targetphase] + phcache = repo._phasecache + + currentroots = phcache.phaseroots[targetphase] newroots = [n for n in nodes if repo[n].phase() < targetphase] if newroots: currentroots.update(newroots) ctxs = repo.set('roots(%ln::)', currentroots) currentroots.intersection_update(ctx.node() for ctx in ctxs) - if '_phaserev' in vars(repo): - del repo._phaserev - repo._dirtyphases = True + phcache.invalidatephaserevs() + phcache.dirty = True def listphases(repo): """List phases root for serialisation over pushkey""" keys = {} value = '%i' % draft - for root in repo._phaseroots[draft]: + for root in repo._phasecache.phaseroots[draft]: keys[hex(root)] = value if repo.ui.configbool('phases', 'publish', True): @@ -263,7 +301,7 @@ def visibleheads(repo): """return the set of visible head of this repo""" # XXX we want a cache on this - sroots = repo._phaseroots[secret] + sroots = repo._phasecache.phaseroots[secret] if sroots: # XXX very slow revset. storing heads or secret "boundary" would help. revset = repo.set('heads(not (%ln::))', sroots) @@ -279,7 +317,7 @@ """return a branchmap for the visible set""" # XXX Recomputing this data on the fly is very slow. We should build a # XXX cached version while computin the standard branchmap version. - sroots = repo._phaseroots[secret] + sroots = repo._phasecache.phaseroots[secret] if sroots: vbranchmap = {} for branch, nodes in repo.branchmap().iteritems(): diff -r 4ae3ba9e4d7a -r b6081c2c4647 mercurial/revset.py --- a/mercurial/revset.py Sat May 12 00:19:30 2012 +0200 +++ b/mercurial/revset.py Sat May 12 00:24:07 2012 +0200 @@ -463,7 +463,8 @@ """``draft()`` Changeset in draft phase.""" getargs(x, 0, 0, _("draft takes no arguments")) - return [r for r in subset if repo._phaserev[r] == phases.draft] + pc = repo._phasecache + return [r for r in subset if pc.phase(repo, r) == phases.draft] def filelog(repo, subset, x): """``filelog(pattern)`` @@ -852,7 +853,8 @@ """``public()`` Changeset in public phase.""" getargs(x, 0, 0, _("public takes no arguments")) - return [r for r in subset if repo._phaserev[r] == phases.public] + pc = repo._phasecache + return [r for r in subset if pc.phase(repo, r) == phases.public] def remote(repo, subset, x): """``remote([id [,path]])`` @@ -1031,7 +1033,8 @@ """``secret()`` Changeset in secret phase.""" getargs(x, 0, 0, _("secret takes no arguments")) - return [r for r in subset if repo._phaserev[r] == phases.secret] + pc = repo._phasecache + return [r for r in subset if pc.phase(repo, r) == phases.secret] def sort(repo, subset, x): """``sort(set[, [-]key...])``