Mercurial > evolve
view hgext3rd/evolve/compat.py @ 6561:d08590ce067d
topic: also add topic and topic namespace caches to bundlerepository
This allows ctx.topic() and ctx.topic_namespace() actually return something
when ctx belongs to a bundle repository.
This should in theory be safe to just do because we only expose already
existing commit extras that we'd get from the repo on pull anyway.
author | Anton Shestakov <av6@dwimlabs.net> |
---|---|
date | Fri, 29 Sep 2023 16:37:53 -0300 |
parents | e44d343b9ed2 |
children |
line wrap: on
line source
# Copyright 2017 Octobus <contact@octobus.net> # # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. """ Compatibility module """ import contextlib from mercurial.i18n import _ from mercurial import ( cmdutil, context, copies as copiesmod, dirstate, error, hg, logcmdutil, merge as mergemod, node, obsolete, pycompat, rewriteutil, scmutil, util, ) # hg <= 5.2 (c21aca51b392) try: from mercurial import pathutil dirs = pathutil.dirs except (AttributeError, ImportError): dirs = util.dirs # pytype: disable=module-attr # hg <= 5.4 (b7808443ed6a) try: from mercurial import mergestate as mergestatemod mergestate = mergestatemod.mergestate except (AttributeError, ImportError): mergestate = mergemod.mergestate # pytype: disable=module-attr from . import ( exthelper, ) eh = exthelper.exthelper() # Evolution renaming compat TROUBLES = { r'ORPHAN': b'orphan', r'CONTENTDIVERGENT': b'content-divergent', r'PHASEDIVERGENT': b'phase-divergent', } def memfilectx(repo, ctx, fctx, flags, copied, path): # XXX Would it be better at the module level? varnames = context.memfilectx.__init__.__code__.co_varnames # pytype: disable=attribute-error if r"copysource" in varnames: mctx = context.memfilectx(repo, ctx, fctx.path(), fctx.data(), islink=b'l' in flags, isexec=b'x' in flags, copysource=copied.get(path)) # hg <= 4.9 (550a172a603b) elif varnames[2] == r"changectx": mctx = context.memfilectx(repo, ctx, fctx.path(), fctx.data(), islink=b'l' in flags, isexec=b'x' in flags, copied=copied.get(path)) # pytype: disable=wrong-keyword-args return mctx hg48 = util.safehasattr(copiesmod, 'stringutil') # code imported from Mercurial core at ae17555ef93f + patch def fixedcopytracing(repo, c1, c2, base): """A complete copy-patse of copies._fullcopytrace with a one line fix to handle when the base is not parent of both c1 and c2. This should be converted in a compat function once https://phab.mercurial-scm.org/D3896 gets in and once we drop support for 4.9, this should be removed.""" from mercurial import pathutil copies = copiesmod # In certain scenarios (e.g. graft, update or rebase), base can be # overridden We still need to know a real common ancestor in this case We # can't just compute _c1.ancestor(_c2) and compare it to ca, because there # can be multiple common ancestors, e.g. in case of bidmerge. Because our # caller may not know if the revision passed in lieu of the CA is a genuine # common ancestor or not without explicitly checking it, it's better to # determine that here. # # base.isancestorof(wc) is False, work around that _c1 = c1.p1() if c1.rev() is None else c1 _c2 = c2.p1() if c2.rev() is None else c2 # an endpoint is "dirty" if it isn't a descendant of the merge base # if we have a dirty endpoint, we need to trigger graft logic, and also # keep track of which endpoint is dirty dirtyc1 = not base.isancestorof(_c1) dirtyc2 = not base.isancestorof(_c2) graft = dirtyc1 or dirtyc2 tca = base if graft: tca = _c1.ancestor(_c2) # hg <= 4.9 (dc50121126ae) try: limit = copies._findlimit(repo, c1, c2) # pytype: disable=module-attr except (AttributeError, TypeError): limit = copies._findlimit(repo, c1.rev(), c2.rev()) # pytype: disable=module-attr if limit is None: # no common ancestor, no copies return {}, {}, {}, {}, {} repo.ui.debug(b" searching for copies back to rev %d\n" % limit) m1 = c1.manifest() m2 = c2.manifest() mb = base.manifest() # gather data from _checkcopies: # - diverge = record all diverges in this dict # - copy = record all non-divergent copies in this dict # - fullcopy = record all copies in this dict # - incomplete = record non-divergent partial copies here # - incompletediverge = record divergent partial copies here diverge = {} # divergence data is shared incompletediverge = {} data1 = {b'copy': {}, b'fullcopy': {}, b'incomplete': {}, b'diverge': diverge, b'incompletediverge': incompletediverge, } data2 = {b'copy': {}, b'fullcopy': {}, b'incomplete': {}, b'diverge': diverge, b'incompletediverge': incompletediverge, } # find interesting file sets from manifests if hg48: addedinm1 = m1.filesnotin(mb, repo.narrowmatch()) addedinm2 = m2.filesnotin(mb, repo.narrowmatch()) else: addedinm1 = m1.filesnotin(mb) addedinm2 = m2.filesnotin(mb) bothnew = sorted(addedinm1 & addedinm2) if tca == base: # unmatched file from base u1r, u2r = copies._computenonoverlap(repo, c1, c2, addedinm1, addedinm2) # pytype: disable=module-attr u1u, u2u = u1r, u2r else: # unmatched file from base (DAG rotation in the graft case) u1r, u2r = copies._computenonoverlap(repo, c1, c2, addedinm1, addedinm2, # pytype: disable=module-attr baselabel=b'base') # unmatched file from topological common ancestors (no DAG rotation) # need to recompute this for directory move handling when grafting mta = tca.manifest() if hg48: m1f = m1.filesnotin(mta, repo.narrowmatch()) m2f = m2.filesnotin(mta, repo.narrowmatch()) baselabel = b'topological common ancestor' u1u, u2u = copies._computenonoverlap(repo, c1, c2, m1f, m2f, # pytype: disable=module-attr baselabel=baselabel) else: u1u, u2u = copies._computenonoverlap(repo, c1, c2, m1.filesnotin(mta), # pytype: disable=module-attr m2.filesnotin(mta), baselabel=b'topological common ancestor') for f in u1u: copies._checkcopies(c1, c2, f, base, tca, dirtyc1, limit, data1) # pytype: disable=module-attr for f in u2u: copies._checkcopies(c2, c1, f, base, tca, dirtyc2, limit, data2) # pytype: disable=module-attr copy = dict(data1[b'copy']) copy.update(data2[b'copy']) fullcopy = dict(data1[b'fullcopy']) fullcopy.update(data2[b'fullcopy']) if dirtyc1: copies._combinecopies(data2[b'incomplete'], data1[b'incomplete'], copy, diverge, # pytype: disable=module-attr incompletediverge) else: copies._combinecopies(data1[b'incomplete'], data2[b'incomplete'], copy, diverge, # pytype: disable=module-attr incompletediverge) renamedelete = {} renamedeleteset = set() divergeset = set() for of, fl in list(diverge.items()): if len(fl) == 1 or of in c1 or of in c2: del diverge[of] # not actually divergent, or not a rename if of not in c1 and of not in c2: # renamed on one side, deleted on the other side, but filter # out files that have been renamed and then deleted renamedelete[of] = [f for f in fl if f in c1 or f in c2] renamedeleteset.update(fl) # reverse map for below else: divergeset.update(fl) # reverse map for below if bothnew: repo.ui.debug(b" unmatched files new in both:\n %s\n" % b"\n ".join(bothnew)) bothdiverge = {} bothincompletediverge = {} remainder = {} both1 = {b'copy': {}, b'fullcopy': {}, b'incomplete': {}, b'diverge': bothdiverge, b'incompletediverge': bothincompletediverge } both2 = {b'copy': {}, b'fullcopy': {}, b'incomplete': {}, b'diverge': bothdiverge, b'incompletediverge': bothincompletediverge } for f in bothnew: copies._checkcopies(c1, c2, f, base, tca, dirtyc1, limit, both1) # pytype: disable=module-attr copies._checkcopies(c2, c1, f, base, tca, dirtyc2, limit, both2) # pytype: disable=module-attr if dirtyc1 and dirtyc2: pass elif dirtyc1: # incomplete copies may only be found on the "dirty" side for bothnew assert not both2[b'incomplete'] remainder = copies._combinecopies({}, both1[b'incomplete'], copy, bothdiverge, # pytype: disable=module-attr bothincompletediverge) elif dirtyc2: assert not both1[b'incomplete'] remainder = copies._combinecopies({}, both2[b'incomplete'], copy, bothdiverge, # pytype: disable=module-attr bothincompletediverge) else: # incomplete copies and divergences can't happen outside grafts assert not both1[b'incomplete'] assert not both2[b'incomplete'] assert not bothincompletediverge for f in remainder: assert f not in bothdiverge ic = remainder[f] if ic[0] in (m1 if dirtyc1 else m2): # backed-out rename on one side, but watch out for deleted files bothdiverge[f] = ic for of, fl in bothdiverge.items(): if len(fl) == 2 and fl[0] == fl[1]: copy[fl[0]] = of # not actually divergent, just matching renames if fullcopy and repo.ui.debugflag: repo.ui.debug(b" all copies found (* = to merge, ! = divergent, " b"% = renamed and deleted):\n") for f in sorted(fullcopy): note = b"" if f in copy: note += b"*" if f in divergeset: note += b"!" if f in renamedeleteset: note += b"%" repo.ui.debug(b" src: '%s' -> dst: '%s' %s\n" % (fullcopy[f], f, note)) del divergeset if not fullcopy: return copy, {}, diverge, renamedelete, {} repo.ui.debug(b" checking for directory renames\n") # generate a directory move map d1, d2 = c1.dirs(), c2.dirs() # Hack for adding '', which is not otherwise added, to d1 and d2 d1.addpath(b'/') d2.addpath(b'/') invalid = set() dirmove = {} # examine each file copy for a potential directory move, which is # when all the files in a directory are moved to a new directory for dst, src in fullcopy.items(): dsrc, ddst = pathutil.dirname(src), pathutil.dirname(dst) if dsrc in invalid: # already seen to be uninteresting continue elif dsrc in d1 and ddst in d1: # directory wasn't entirely moved locally invalid.add(dsrc + b"/") elif dsrc in d2 and ddst in d2: # directory wasn't entirely moved remotely invalid.add(dsrc + b"/") elif dsrc + b"/" in dirmove and dirmove[dsrc + b"/"] != ddst + b"/": # files from the same directory moved to two different places invalid.add(dsrc + b"/") else: # looks good so far dirmove[dsrc + b"/"] = ddst + b"/" for i in invalid: if i in dirmove: del dirmove[i] del d1, d2, invalid if not dirmove: return copy, {}, diverge, renamedelete, {} for d in dirmove: repo.ui.debug(b" discovered dir src: '%s' -> dst: '%s'\n" % (d, dirmove[d])) movewithdir = {} # check unaccounted nonoverlapping files against directory moves for f in u1r + u2r: if f not in fullcopy: for d in dirmove: if f.startswith(d): # new file added in a directory that was moved, move it df = dirmove[d] + f[len(d):] if df not in copy: movewithdir[f] = df repo.ui.debug((b" pending file src: '%s' -> " b"dst: '%s'\n") % (f, df)) break return copy, movewithdir, diverge, renamedelete, dirmove # hg <= 4.9 (7694b685bb10) fixupstreamed = util.safehasattr(scmutil, 'movedirstate') if not fixupstreamed: copiesmod._fullcopytracing = fixedcopytracing # nodemap.get and index.[has_node|rev|get_rev] # hg <= 5.2 (02802fa87b74) def getgetrev(cl): """Returns index.get_rev or nodemap.get (for pre-5.3 Mercurial).""" if util.safehasattr(cl.index, 'get_rev'): return cl.index.get_rev return cl.nodemap.get @contextlib.contextmanager def changing_parents(repo): if util.safehasattr(repo.dirstate, 'changing_parents'): changing_parents = repo.dirstate.changing_parents(repo) else: # hg <= 6.3 (7a8bfc05b691) changing_parents = repo.dirstate.parentchange() try: with changing_parents: yield finally: # hg <= 5.2 (85c4cd73996b) if util.safehasattr(repo, '_quick_access_changeid_invalidate'): repo._quick_access_changeid_invalidate() if util.safehasattr(mergemod, '_update'): def _update(*args, **kwargs): return mergemod._update(*args, **kwargs) else: # hg <= 5.5 (2c86b9587740) def _update(*args, **kwargs): return mergemod.update(*args, **kwargs) if (util.safehasattr(mergemod, '_update') and util.safehasattr(mergemod, 'update')): def update(ctx): mergemod.update(ctx) def clean_update(ctx): mergemod.clean_update(ctx) else: # hg <= 5.5 (c1b603cdc95a) def update(ctx): hg.updaterepo(ctx.repo(), ctx.node(), overwrite=False) def clean_update(ctx): hg.updaterepo(ctx.repo(), ctx.node(), overwrite=True) if util.safehasattr(cmdutil, 'format_changeset_summary'): def format_changeset_summary_fn(ui, repo, command, default_spec): def show(ctx): text = cmdutil.format_changeset_summary(ui, ctx, command=command, default_spec=default_spec) ui.write(b'%s\n' % text) return show else: # hg <= 5.6 (96fcc37a9c80) def format_changeset_summary_fn(ui, repo, command, default_spec): return logcmdutil.changesetdisplayer(ui, repo, {b'template': default_spec}).show if util.safehasattr(cmdutil, 'check_at_most_one_arg'): def check_at_most_one_arg(opts, *args): return cmdutil.check_at_most_one_arg(opts, *args) else: # hg <= 5.2 (d587937600be) def check_at_most_one_arg(opts, *args): def to_display(name): return pycompat.sysbytes(name).replace(b'_', b'-') previous = None for x in args: if opts.get(x): if previous: raise InputError(_(b'cannot specify both --%s and --%s') % (to_display(previous), to_display(x))) previous = x return previous if util.safehasattr(cmdutil, 'check_incompatible_arguments'): code = cmdutil.check_incompatible_arguments.__code__ if r'others' in code.co_varnames[:code.co_argcount]: def check_incompatible_arguments(opts, first, others): return cmdutil.check_incompatible_arguments(opts, first, others) else: # hg <= 5.3 (d4c1501225c4) def check_incompatible_arguments(opts, first, others): return cmdutil.check_incompatible_arguments(opts, first, *others) else: # hg <= 5.2 (023ad45e2fd2) def check_incompatible_arguments(opts, first, others): for other in others: check_at_most_one_arg(opts, first, other) # allowdivergenceopt is a much newer addition to obsolete.py # hg <= 5.8 (ba6881c6a178) allowdivergenceopt = b'allowdivergence' def isenabled(repo, option): if option == allowdivergenceopt: if obsolete._getoptionvalue(repo, obsolete.createmarkersopt): return obsolete._getoptionvalue(repo, allowdivergenceopt) else: # note that we're not raising error.Abort when divergence is # allowed, but creating markers is not, even on older hg versions return False else: return obsolete.isenabled(repo, option) if util.safehasattr(dirstate.dirstate, 'set_clean'): movedirstate = scmutil.movedirstate else: # hg <= 5.8 (8a50fb0784a9) # TODO: call core's version once we've dropped support for hg <= 4.9 def movedirstate(repo, newctx, match=None): """Move the dirstate to newctx and adjust it as necessary. A matcher can be provided as an optimization. It is probably a bug to pass a matcher that doesn't match all the differences between the parent of the working copy and newctx. """ oldctx = repo[b'.'] ds = repo.dirstate dscopies = dict(ds.copies()) ds.setparents(newctx.node(), node.nullid) s = newctx.status(oldctx, match=match) for f in s.modified: if ds[f] == b'r': # modified + removed -> removed continue ds.normallookup(f) for f in s.added: if ds[f] == b'r': # added + removed -> unknown ds.drop(f) elif ds[f] != b'a': ds.add(f) for f in s.removed: if ds[f] == b'a': # removed + added -> normal ds.normallookup(f) elif ds[f] != b'r': ds.remove(f) # Merge old parent and old working dir copies oldcopies = copiesmod.pathcopies(newctx, oldctx, match) oldcopies.update(dscopies) newcopies = { dst: oldcopies.get(src, src) for dst, src in oldcopies.items() } # Adjust the dirstate copies for dst, src in newcopies.items(): if src not in newctx or dst in newctx or ds[dst] != b'a': src = None ds.copy(src, dst) # hg <= 4.9 (e1ceefab9bca) code = context.overlayworkingctx._markdirty.__code__ if 'copied' not in code.co_varnames[:code.co_argcount]: def fixedmarkcopied(self, path, origin): self._markdirty(path, exists=True, date=self.filedate(path), flags=self.flags(path), copied=origin) context.overlayworkingctx.markcopied = fixedmarkcopied # what we're actually targeting here is e079e001d536 # hg <= 5.0 (dc3fdd1b5af4) try: from mercurial import state as statemod markdirtyfixed = util.safehasattr(statemod, '_statecheck') except (AttributeError, ImportError): markdirtyfixed = False if not markdirtyfixed: def fixedmarkdirty( self, path, exists, data=None, date=None, flags='', copied=None, ): # data not provided, let's see if we already have some; if not, let's # grab it from our underlying context, so that we always have data if # the file is marked as existing. if exists and data is None: oldentry = self._cache.get(path) or {} data = oldentry.get('data') if data is None: data = self._wrappedctx[path].data() self._cache[path] = { 'exists': exists, 'data': data, 'date': date, 'flags': flags, 'copied': copied, } context.overlayworkingctx._markdirty = fixedmarkdirty def setbranch(repo, branch): # this attribute was introduced at about the same time dirstate.setbranch() # was modified # hg <= 6.3 (e9379b55ed80) if util.safehasattr(dirstate, 'requires_changing_files_or_status'): repo.dirstate.setbranch(branch, repo.currenttransaction()) else: repo.dirstate.setbranch(branch) if util.safehasattr(dirstate.dirstate, 'get_entry'): def dirchanges(dirstate): return [ f for f in dirstate if not dirstate.get_entry(f).maybe_clean ] else: # hg <= 5.9 (dcd97b082b3b) def dirchanges(dirstate): return [f for f in dirstate if dirstate[f] != b'n'] if util.safehasattr(error, 'InputError'): InputError = error.InputError else: # hg <= 5.6 (8d72e29ad1e0) InputError = error.Abort if util.safehasattr(error, 'StateError'): StateError = error.StateError else: # hg <= 5.6 (527ce85c2e60) StateError = error.Abort try: retained_extras_on_rebase = rewriteutil.retained_extras_on_rebase preserve_extras_on_rebase = rewriteutil.preserve_extras_on_rebase except AttributeError: # hg <= 6.4 (cbcbf63b6dbf) retained_extras_on_rebase = { b'source', b'intermediate-source', } def preserve_extras_on_rebase(old_ctx, new_extra): """preserve the relevant `extra` entries from old_ctx on rebase-like operations """ old_extra = old_ctx.extra() for key in retained_extras_on_rebase: value = old_extra.get(key) if value is not None: new_extra[key] = value # give other extensions an opportunity to collaborate rewriteutil.retained_extras_on_rebase = retained_extras_on_rebase rewriteutil.preserve_extras_on_rebase = preserve_extras_on_rebase