Mercurial > evolve
view hgext3rd/topic/topicmap.py @ 6935:954d7ea5cd67 stable tip
stack: when stack base is obsolete, pick any successor, even if at random
There are situations when s0 is obsolete and we also cannot pick just one
successor for it to use in stack. In such a case, let's pick the "latest"
successor from the first set.
We're assuming that obsutil.successorssets() returns data in the same order (it
should, since it makes sure to sort data internally). Keeping that in mind,
while the successor picked for s0 by this code is not based on any sort of
sophisticated logic, it should nonetheless be the same every time.
This patch is probably not going to completely break anything that was
previously working fine, because the previous behavior was to just abort with
an exception.
author | Anton Shestakov <av6@dwimlabs.net> |
---|---|
date | Sat, 16 Nov 2024 17:01:02 +0400 |
parents | 1b59ddda3242 |
children | ed00ed185249 |
line wrap: on
line source
import contextlib import hashlib from mercurial.i18n import _ from mercurial.node import nullid from mercurial import ( branchmap, changegroup, extensions, repoview, util, ) from . import ( common, compat, discovery, ) basefilter = set([b'base', b'immutable']) def topicfilter(name): """return a "topic" version of a filter level""" if name in basefilter: return name elif name is None: return None elif name.endswith(b'-topic'): return name else: return name + b'-topic' def istopicfilter(filtername): if filtername is None: return False return filtername.endswith(b'-topic') def gettopicrepo(repo): if not common.hastopicext(repo): return repo filtername = topicfilter(repo.filtername) if filtername is None or filtername == repo.filtername: return repo return repo.filtered(filtername) def _setuptopicfilter(ui): """extend the filter related mapping with topic related one""" funcmap = repoview.filtertable # hg <= 4.9 (caebe5e7f4bd) partialmap = branchmap.subsettable for plainname in list(funcmap): newfilter = topicfilter(plainname) if newfilter == plainname: # filter level not affected by topic that we should not override continue def revsfunc(repo, name=plainname): return repoview.filterrevs(repo, name) base = topicfilter(partialmap[plainname]) if newfilter not in funcmap: funcmap[newfilter] = revsfunc partialmap[newfilter] = base funcmap[b'unfiltered-topic'] = lambda repo: frozenset() partialmap[b'unfiltered-topic'] = b'visible-topic' def _phaseshash(repo, maxrev): """uniq ID for a phase matching a set of rev""" cl = repo.changelog fr = cl.filteredrevs nppr = compat.nonpublicphaseroots(repo) # starting with hg 6.7rc0 phase roots are already revs instead of nodes # hg <= 6.6 (68289ed170c7) if not util.safehasattr(repo._phasecache, '_phaseroots'): getrev = compat.getgetrev(cl) nppr = set(getrev(n) for n in nppr) revs = sorted(set(r for r in nppr if r not in fr and r < maxrev)) key = nullid if revs: s = hashlib.sha1() for rev in revs: s.update(b'%d;' % rev) key = s.digest() return key def modsetup(ui): """call at uisetup time to install various wrappings""" _setuptopicfilter(ui) _wrapbmcache(ui) extensions.wrapfunction(changegroup.cg1unpacker, 'apply', cgapply) compat.overridecommitstatus(commitstatus) def cgapply(orig, self, repo, *args, **kwargs): """make sure a topicmap is used when applying a changegroup""" newfilter = topicfilter(repo.filtername) if newfilter is None: other = repo else: other = repo.filtered(newfilter) return orig(self, other, *args, **kwargs) def commitstatus(orig, repo, node, branch, bheads=None, tip=None, **opts): # wrap commit status use the topic branch heads ctx = repo[node] ctxbranch = common.formatfqbn(branch=ctx.branch()) if ctx.topic() and ctxbranch == branch: bheads = repo.branchheads(b"%s:%s" % (branch, ctx.topic())) with discovery.override_context_branch(repo) as repo: ret = orig(repo, node, branch, bheads=bheads, tip=tip, **opts) # logic copy-pasted from cmdutil.commitstatus() if ctx.topic(): return ret parents = ctx.parents() if (not opts.get('amend') and bheads and node not in bheads and not any( p.node() in bheads and common.formatfqbn(branch=p.branch()) == branch for p in parents )): repo.ui.status(_(b"(consider using topic for lightweight branches." b" See 'hg help topic')\n")) return ret def _wrapbmcache(ui): if util.safehasattr(branchmap, 'BranchCacheV2'): class TopicCache(_TopicCacheV2, branchmap.BranchCacheV2): pass branchmap.BranchCacheV2 = TopicCache class remotetopiccache(_TopicCacheV2, branchmap.remotebranchcache): pass branchmap.remotebranchcache = remotetopiccache else: # hg <= 6.7 (ec640dc9cebd) class topiccache(_topiccache, branchmap.branchcache): pass branchmap.branchcache = topiccache try: # Mercurial 5.0 class remotetopiccache(_topiccache, branchmap.remotebranchcache): pass branchmap.remotebranchcache = remotetopiccache def _wrapupdatebmcachemethod(orig, self, repo): # pass in the bound method as the original return _wrapupdatebmcache(orig.__get__(self), repo) extensions.wrapfunction(branchmap.BranchMapCache, 'updatecache', _wrapupdatebmcachemethod) except AttributeError: # hg <= 4.9 (3461814417f3) extensions.wrapfunction(branchmap, 'updatecache', _wrapupdatebmcache) # branchcache in hg <= 4.9 doesn't have load method, instead there's a # module-level function to read on-disk cache and return a branchcache extensions.wrapfunction(branchmap, 'read', _wrapbmread) def _wrapupdatebmcache(orig, repo): previous = getattr(repo, '_autobranchmaptopic', False) try: repo._autobranchmaptopic = False return orig(repo) finally: repo._autobranchmaptopic = previous if util.safehasattr(branchmap, 'branchcache') and dict in branchmap.branchcache.__mro__: # hg <= 4.9 (624d6683c705) # let's break infinite recursion in __init__() that uses super() orig = branchmap.branchcache @contextlib.contextmanager def oldbranchmap(): current = branchmap.branchcache try: branchmap.branchcache = orig yield finally: branchmap.branchcache = current else: oldbranchmap = util.nullcontextmanager if util.safehasattr(branchmap, 'branchcache'): allbccls = (branchmap.branchcache,) if util.safehasattr(branchmap, 'remotebranchcache'): # hg <= 4.9 (eb7ce452e0fb) allbccls = (branchmap.branchcache, branchmap.remotebranchcache) class _topiccache(object): # combine me with branchmap.branchcache def __init__(self, *args, **kwargs): with oldbranchmap(): super(_topiccache, self).__init__(*args, **kwargs) self.phaseshash = None def copy(self): """return an deep copy of the branchcache object""" assert isinstance(self, allbccls) # help pytype entries = compat.bcentries(self) args = (entries, self.tipnode, self.tiprev, self.filteredhash, self._closednodes) if util.safehasattr(self, '_repo'): # hg <= 5.7 (6266d19556ad) args = (self._repo,) + args new = self.__class__(*args) new.phaseshash = self.phaseshash return new def load(self, repo, lineiter): """call branchmap.load(), and then transform branch names to be in the new "//" format """ assert isinstance(self, branchmap.branchcache) # help pytype super(_topiccache, self).load(repo, lineiter) entries = compat.bcentries(self) for branch in tuple(entries): formatted = common.formatfqbn(branch=branch) if branch != formatted: entries[formatted] = entries.pop(branch) def validfor(self, repo): """Is the cache content valid regarding a repo - False when cached tipnode is unknown or if we detect a strip. - True when cache is up to date or a subset of current repo.""" assert isinstance(self, allbccls) # help pytype valid = super(_topiccache, self).validfor(repo) if not valid: return False elif not istopicfilter(repo.filtername) or self.phaseshash is None: # phasehash at None means this is a branchmap # come from non topic thing return True else: try: valid = self.phaseshash == _phaseshash(repo, self.tiprev) return valid except IndexError: return False def write(self, repo): """write cache to disk if it's not topic-only, but first transform cache keys from branches in "//" format into bare branch names """ # we expect mutable set to be small enough to be that computing it all # the time will be fast enough if not istopicfilter(repo.filtername): cache = self.copy() entries = compat.bcentries(cache) for formatted in tuple(entries): branch, tns, topic = common.parsefqbn(formatted) if branch != formatted: entries[branch] = entries.pop(formatted) super(_topiccache, cache).write(repo) def update(self, repo, revgen): """Given a branchhead cache, self, that may have extra nodes or be missing heads, and a generator of nodes that are strictly a superset of heads missing, this function updates self to be correct. """ assert isinstance(self, allbccls) # help pytype if not istopicfilter(repo.filtername): return super(_topiccache, self).update(repo, revgen) # See topic.discovery._headssummary(), where repo.unfiltered gets # overridden to return .filtered('unfiltered-topic'). revbranchcache # only can be created for unfiltered repo (filtername is None), so we # do that here, and this revbranchcache will be cached inside repo. # When we get rid of *-topic filters, then this workaround can be # removed too. repo.unfiltered().revbranchcache() super(_topiccache, self).update(repo, revgen) self.phaseshash = _phaseshash(repo, self.tiprev) class _TopicCacheV2(object): # combine me with branchmap.BranchCacheV2 def __init__(self, *args, **kwargs): super(_TopicCacheV2, self).__init__(*args, **kwargs) self.phaseshash = None def _load_heads(self, repo, lineiter): """call BranchCacheV2._load_heads(), and then transform branch names to be in the new "//" format """ assert isinstance(self, branchmap.BranchCacheV2) # help pytype super(_TopicCacheV2, self)._load_heads(repo, lineiter) for branch in tuple(self._entries): formatted = common.formatfqbn(branch=branch) if branch != formatted: self._entries[formatted] = self._entries.pop(branch) def validfor(self, repo): """Is the cache content valid regarding a repo - False when cached tipnode is unknown or if we detect a strip. - True when cache is up to date or a subset of current repo.""" assert isinstance(self, (branchmap.BranchCacheV2, branchmap.remotebranchcache)) # help pytype valid = super(_TopicCacheV2, self).validfor(repo) if not valid: return False elif not istopicfilter(repo.filtername) or self.phaseshash is None: # phasehash at None means this is a branchmap # come from non topic thing return True else: try: valid = self.phaseshash == _phaseshash(repo, self.tiprev) return valid except IndexError: return False def write(self, repo): """write cache to disk if it's not topic-only, but first transform cache keys from branches in "//" format into bare branch names """ # we expect mutable set to be small enough to be that computing it all # the time will be fast enough if not istopicfilter(repo.filtername): entries = self._entries.copy() for formatted in tuple(entries): branch, tns, topic = common.parsefqbn(formatted) if branch != formatted: entries[branch] = entries.pop(formatted) oldentries = self._entries try: self._entries = entries super(_TopicCacheV2, self).write(repo) finally: self._entries = oldentries def update(self, repo, revgen): """Given a branchhead cache, self, that may have extra nodes or be missing heads, and a generator of nodes that are strictly a superset of heads missing, this function updates self to be correct. """ assert isinstance(self, (branchmap.BranchCacheV2, branchmap.remotebranchcache)) # help pytype if not istopicfilter(repo.filtername): return super(_TopicCacheV2, self).update(repo, revgen) # See topic.discovery._headssummary(), where repo.unfiltered gets # overridden to return .filtered('unfiltered-topic'). revbranchcache # only can be created for unfiltered repo (filtername is None), so we # do that here, and this revbranchcache will be cached inside repo. # When we get rid of *-topic filters, then this workaround can be # removed too. repo.unfiltered().revbranchcache() super(_TopicCacheV2, self).update(repo, revgen) if util.safehasattr(self, 'tiprev'): # remotebranchcache doesn't have self.tiprev self.phaseshash = _phaseshash(repo, self.tiprev) def _wrapbmread(orig, repo): """call branchmap.read(), and then transform branch names to be in the new "//" format """ partial = orig(repo) if partial is None: # because of IOError or OSError return partial entries = compat.bcentries(partial) for branch in tuple(entries): formatted = common.formatfqbn(branch=branch) if branch != formatted: entries[formatted] = entries.pop(branch) return partial