scmutil.py
2299 lines
| 75.3 KiB
| text/x-python
|
PythonLexer
/ mercurial / scmutil.py
|
r13962 | # scmutil.py - Mercurial core utility functions | ||
# | ||||
|
r47575 | # Copyright Olivia Mackall <olivia@selenic.com> | ||
|
r13962 | # | ||
# This software may be used and distributed according to the terms of the | ||||
# GNU General Public License version 2 or any later version. | ||||
|
r27482 | from __future__ import absolute_import | ||
import errno | ||||
import glob | ||||
import os | ||||
|
r41799 | import posixpath | ||
|
r27482 | import re | ||
|
r34462 | import subprocess | ||
r33249 | import weakref | |||
|
r27482 | |||
from .i18n import _ | ||||
|
r32656 | from .node import ( | ||
|
r37546 | bin, | ||
|
r33088 | hex, | ||
|
r39930 | nullrev, | ||
|
r34328 | short, | ||
|
r32656 | wdirrev, | ||
) | ||||
|
r43359 | from .pycompat import getattr | ||
|
r44053 | from .thirdparty import attr | ||
|
r27482 | from . import ( | ||
|
r42103 | copies as copiesmod, | ||
|
r27482 | encoding, | ||
error, | ||||
match as matchmod, | ||||
|
r33088 | obsolete, | ||
r33249 | obsutil, | |||
|
r27482 | pathutil, | ||
phases, | ||||
|
r39262 | policy, | ||
|
r30305 | pycompat, | ||
|
r46054 | requirements as requirementsmod, | ||
|
r31024 | revsetlang, | ||
|
r27482 | similar, | ||
|
r39933 | smartset, | ||
|
r34457 | url, | ||
|
r27482 | util, | ||
|
r34544 | vfs, | ||
|
r27482 | ) | ||
|
r18690 | |||
|
r37102 | from .utils import ( | ||
|
r44517 | hashutil, | ||
|
r37138 | procutil, | ||
|
r37102 | stringutil, | ||
) | ||||
|
r34646 | if pycompat.iswindows: | ||
|
r27482 | from . import scmwindows as scmplatform | ||
|
r18690 | else: | ||
|
r27482 | from . import scmposix as scmplatform | ||
|
r18690 | |||
|
r43906 | parsers = policy.importmod('parsers') | ||
|
r44465 | rustrevlog = policy.importrust('revlog') | ||
|
r39262 | |||
|
r30314 | termsize = scmplatform.termsize | ||
|
r13962 | |||
|
r43346 | |||
|
r44053 | @attr.s(slots=True, repr=False) | ||
class status(object): | ||||
|
r46554 | """Struct with a list of files per status. | ||
|
r44053 | |||
The 'deleted', 'unknown' and 'ignored' properties are only | ||||
relevant to the working copy. | ||||
|
r46554 | """ | ||
|
r22913 | |||
|
r44214 | modified = attr.ib(default=attr.Factory(list)) | ||
added = attr.ib(default=attr.Factory(list)) | ||||
removed = attr.ib(default=attr.Factory(list)) | ||||
deleted = attr.ib(default=attr.Factory(list)) | ||||
unknown = attr.ib(default=attr.Factory(list)) | ||||
ignored = attr.ib(default=attr.Factory(list)) | ||||
clean = attr.ib(default=attr.Factory(list)) | ||||
|
r22913 | |||
|
r44053 | def __iter__(self): | ||
yield self.modified | ||||
yield self.added | ||||
yield self.removed | ||||
yield self.deleted | ||||
yield self.unknown | ||||
yield self.ignored | ||||
yield self.clean | ||||
|
r22913 | |||
|
r44053 | def __repr__(self): | ||
|
r43346 | return ( | ||
r'<status modified=%s, added=%s, removed=%s, deleted=%s, ' | ||||
r'unknown=%s, ignored=%s, clean=%s>' | ||||
) % tuple(pycompat.sysstr(stringutil.pprint(v)) for v in self) | ||||
|
r22913 | |||
|
r20392 | def itersubrepos(ctx1, ctx2): | ||
"""find subrepos in ctx1 or ctx2""" | ||||
# Create a (subpath, ctx) mapping where we prefer subpaths from | ||||
# ctx1. The subpaths from ctx2 are important when the .hgsub file | ||||
# has been modified (in ctx2) but not yet committed (in ctx1). | ||||
subpaths = dict.fromkeys(ctx2.substate, ctx2) | ||||
subpaths.update(dict.fromkeys(ctx1.substate, ctx1)) | ||||
|
r25418 | |||
missing = set() | ||||
for subpath in ctx2.substate: | ||||
if subpath not in ctx1.substate: | ||||
del subpaths[subpath] | ||||
missing.add(subpath) | ||||
|
r43376 | for subpath, ctx in sorted(pycompat.iteritems(subpaths)): | ||
|
r20392 | yield subpath, ctx.sub(subpath) | ||
|
r25418 | # Yield an empty subrepo based on ctx1 for anything only in ctx2. That way, | ||
# status and diff will have an accurate result when it does | ||||
# 'sub.{status|diff}(rev2)'. Otherwise, the ctx2 subrepo is compared | ||||
# against itself. | ||||
for subpath in missing: | ||||
yield subpath, ctx2.nullsub(subpath, ctx1) | ||||
|
r43346 | |||
|
r17248 | def nochangesfound(ui, repo, excluded=None): | ||
|
r46554 | """Report no changes for push/pull, excluded is None or a list of | ||
|
r17248 | nodes excluded from the push/pull. | ||
|
r46554 | """ | ||
|
r17248 | secretlist = [] | ||
if excluded: | ||||
for n in excluded: | ||||
ctx = repo[n] | ||||
if ctx.phase() >= phases.secret and not ctx.extinct(): | ||||
secretlist.append(n) | ||||
|
r15993 | if secretlist: | ||
|
r43346 | ui.status( | ||
|
r43347 | _(b"no changes found (ignored %d secret changesets)\n") | ||
|
r43346 | % len(secretlist) | ||
) | ||||
|
r15993 | else: | ||
|
r43347 | ui.status(_(b"no changes found\n")) | ||
|
r15993 | |||
|
r43346 | |||
|
r30520 | def callcatch(ui, func): | ||
"""call func() with global exception handling | ||||
return func() if no exception happens. otherwise do some error handling | ||||
and return an exit code accordingly. does not handle all exceptions. | ||||
""" | ||||
|
r46430 | coarse_exit_code = -1 | ||
detailed_exit_code = -1 | ||||
|
r30520 | try: | ||
|
r32041 | try: | ||
return func() | ||||
|
r43346 | except: # re-raises | ||
|
r32041 | ui.traceback() | ||
raise | ||||
|
r30520 | # Global exception handling, alphabetically | ||
# Mercurial-specific first, followed by built-in and library exceptions | ||||
except error.LockHeld as inst: | ||||
|
r46432 | detailed_exit_code = 20 | ||
|
r30520 | if inst.errno == errno.ETIMEDOUT: | ||
|
r43347 | reason = _(b'timed out waiting for lock held by %r') % ( | ||
|
r43346 | pycompat.bytestr(inst.locker) | ||
) | ||||
|
r30520 | else: | ||
|
r43347 | reason = _(b'lock held by %r') % inst.locker | ||
|
r43346 | ui.error( | ||
|
r43347 | _(b"abort: %s: %s\n") | ||
|
r43346 | % (inst.desc or stringutil.forcebytestr(inst.filename), reason) | ||
) | ||||
|
r32089 | if not inst.locker: | ||
|
r43347 | ui.error(_(b"(lock might be very busy)\n")) | ||
|
r30520 | except error.LockUnavailable as inst: | ||
|
r46432 | detailed_exit_code = 20 | ||
|
r43346 | ui.error( | ||
|
r43347 | _(b"abort: could not lock %s: %s\n") | ||
|
r43346 | % ( | ||
inst.desc or stringutil.forcebytestr(inst.filename), | ||||
encoding.strtolocal(inst.strerror), | ||||
) | ||||
) | ||||
|
r30520 | except error.RepoError as inst: | ||
|
r46518 | ui.error(_(b"abort: %s\n") % inst) | ||
|
r30520 | if inst.hint: | ||
|
r43347 | ui.error(_(b"(%s)\n") % inst.hint) | ||
|
r30520 | except error.ResponseError as inst: | ||
|
r43347 | ui.error(_(b"abort: %s") % inst.args[0]) | ||
|
r36679 | msg = inst.args[1] | ||
if isinstance(msg, type(u'')): | ||||
msg = pycompat.sysbytes(msg) | ||||
|
r47430 | if msg is None: | ||
ui.error(b"\n") | ||||
elif not isinstance(msg, bytes): | ||||
|
r43347 | ui.error(b" %r\n" % (msg,)) | ||
|
r36713 | elif not msg: | ||
|
r43347 | ui.error(_(b" empty string\n")) | ||
|
r30520 | else: | ||
|
r43347 | ui.error(b"\n%r\n" % pycompat.bytestr(stringutil.ellipsis(msg))) | ||
|
r30520 | except error.CensoredNodeError as inst: | ||
|
r46518 | ui.error(_(b"abort: file censored %s\n") % inst) | ||
|
r32657 | except error.WdirUnsupported: | ||
|
r43347 | ui.error(_(b"abort: working directory revision cannot be specified\n")) | ||
|
r48073 | except error.Error as inst: | ||
|
r48069 | if inst.detailed_exit_code is not None: | ||
detailed_exit_code = inst.detailed_exit_code | ||||
|
r48071 | if inst.coarse_exit_code is not None: | ||
coarse_exit_code = inst.coarse_exit_code | ||||
|
r46497 | ui.error(inst.format()) | ||
|
r46429 | except error.WorkerError as inst: | ||
# Don't print a message -- the worker already should have | ||||
return inst.status_code | ||||
|
r30520 | except ImportError as inst: | ||
|
r46518 | ui.error(_(b"abort: %s\n") % stringutil.forcebytestr(inst)) | ||
|
r37102 | m = stringutil.forcebytestr(inst).split()[-1] | ||
|
r43347 | if m in b"mpatch bdiff".split(): | ||
ui.error(_(b"(did you forget to compile extensions?)\n")) | ||||
elif m in b"zlib".split(): | ||||
ui.error(_(b"(is your Python install correct?)\n")) | ||||
|
r46442 | except util.urlerr.httperror as inst: | ||
|
r46443 | detailed_exit_code = 100 | ||
|
r46442 | ui.error(_(b"abort: %s\n") % stringutil.forcebytestr(inst)) | ||
except util.urlerr.urlerror as inst: | ||||
|
r46443 | detailed_exit_code = 100 | ||
|
r46442 | try: # usually it is in the form (errno, strerror) | ||
reason = inst.reason.args[1] | ||||
except (AttributeError, IndexError): | ||||
# it might be anything, for example a string | ||||
reason = inst.reason | ||||
if isinstance(reason, pycompat.unicode): | ||||
# SSLError of Python 2.7.9 contains a unicode | ||||
reason = encoding.unitolocal(reason) | ||||
ui.error(_(b"abort: error: %s\n") % stringutil.forcebytestr(reason)) | ||||
|
r41466 | except (IOError, OSError) as inst: | ||
|
r46442 | if ( | ||
|
r43347 | util.safehasattr(inst, b"args") | ||
|
r43346 | and inst.args | ||
and inst.args[0] == errno.EPIPE | ||||
): | ||||
|
r30520 | pass | ||
|
r43346 | elif getattr(inst, "strerror", None): # common IOError or OSError | ||
|
r41465 | if getattr(inst, "filename", None) is not None: | ||
|
r43346 | ui.error( | ||
|
r43347 | _(b"abort: %s: '%s'\n") | ||
|
r43346 | % ( | ||
encoding.strtolocal(inst.strerror), | ||||
stringutil.forcebytestr(inst.filename), | ||||
) | ||||
) | ||||
|
r30520 | else: | ||
|
r43347 | ui.error(_(b"abort: %s\n") % encoding.strtolocal(inst.strerror)) | ||
|
r43346 | else: # suspicious IOError | ||
|
r30520 | raise | ||
except MemoryError: | ||||
|
r43347 | ui.error(_(b"abort: out of memory\n")) | ||
|
r30520 | except SystemExit as inst: | ||
# Commands shouldn't sys.exit directly, but give a return code. | ||||
# Just in case catch this and and pass exit code to caller. | ||||
|
r46430 | detailed_exit_code = 254 | ||
coarse_exit_code = inst.code | ||||
|
r30520 | |||
|
r46430 | if ui.configbool(b'ui', b'detailed-exit-code'): | ||
return detailed_exit_code | ||||
else: | ||||
return coarse_exit_code | ||||
|
r30520 | |||
|
r43346 | |||
|
r17821 | def checknewlabel(repo, lbl, kind): | ||
|
r19070 | # Do not use the "kind" parameter in ui output. | ||
# It makes strings difficult to translate. | ||||
|
r43347 | if lbl in [b'tip', b'.', b'null']: | ||
|
r46449 | raise error.InputError(_(b"the name '%s' is reserved") % lbl) | ||
|
r43347 | for c in (b':', b'\0', b'\n', b'\r'): | ||
|
r17821 | if c in lbl: | ||
|
r46449 | raise error.InputError( | ||
|
r43347 | _(b"%r cannot be used in a name") % pycompat.bytestr(c) | ||
|
r43346 | ) | ||
|
r18566 | try: | ||
int(lbl) | ||||
|
r46449 | raise error.InputError(_(b"cannot use an integer as a name")) | ||
|
r18566 | except ValueError: | ||
pass | ||||
|
r36162 | if lbl.strip() != lbl: | ||
|
r46449 | raise error.InputError( | ||
_(b"leading or trailing whitespace in name %r") % lbl | ||||
) | ||||
|
r17817 | |||
|
r43346 | |||
|
r13974 | def checkfilename(f): | ||
'''Check that the filename f is an acceptable filename for a tracked file''' | ||||
|
r43347 | if b'\r' in f or b'\n' in f: | ||
|
r46448 | raise error.InputError( | ||
|
r43347 | _(b"'\\n' and '\\r' disallowed in filenames: %r") | ||
|
r43346 | % pycompat.bytestr(f) | ||
) | ||||
|
r13974 | |||
|
r13962 | def checkportable(ui, f): | ||
'''Check if filename f is portable and warn or abort depending on config''' | ||||
|
r13974 | checkfilename(f) | ||
|
r14138 | abort, warn = checkportabilityalert(ui) | ||
if abort or warn: | ||||
|
r13962 | msg = util.checkwinfilename(f) | ||
if msg: | ||||
|
r43347 | msg = b"%s: %s" % (msg, procutil.shellquote(f)) | ||
|
r14138 | if abort: | ||
|
r46448 | raise error.InputError(msg) | ||
|
r43347 | ui.warn(_(b"warning: %s\n") % msg) | ||
|
r14068 | |||
|
r43346 | |||
|
r14067 | def checkportabilityalert(ui): | ||
|
r46554 | """check if the user's config requests nothing, a warning, or abort for | ||
non-portable filenames""" | ||||
|
r43347 | val = ui.config(b'ui', b'portablefilenames') | ||
|
r14067 | lval = val.lower() | ||
|
r37102 | bval = stringutil.parsebool(val) | ||
|
r43347 | abort = pycompat.iswindows or lval == b'abort' | ||
warn = bval or lval == b'warn' | ||||
if bval is None and not (warn or abort or lval == b'ignore'): | ||||
|
r13962 | raise error.ConfigError( | ||
|
r43347 | _(b"ui.portablefilenames value is invalid ('%s')") % val | ||
|
r43346 | ) | ||
|
r14067 | return abort, warn | ||
|
r43346 | |||
|
r14138 | class casecollisionauditor(object): | ||
|
r17201 | def __init__(self, ui, abort, dirstate): | ||
|
r14138 | self._ui = ui | ||
self._abort = abort | ||||
|
r43347 | allfiles = b'\0'.join(dirstate) | ||
self._loweredfiles = set(encoding.lower(allfiles).split(b'\0')) | ||||
|
r17201 | self._dirstate = dirstate | ||
# The purpose of _newfiles is so that we don't complain about | ||||
# case collisions if someone were to call this object with the | ||||
# same filename twice. | ||||
self._newfiles = set() | ||||
|
r14067 | |||
|
r14138 | def __call__(self, f): | ||
|
r20006 | if f in self._newfiles: | ||
return | ||||
|
r14980 | fl = encoding.lower(f) | ||
|
r20006 | if fl in self._loweredfiles and f not in self._dirstate: | ||
|
r43347 | msg = _(b'possible case-folding collision for %s') % f | ||
|
r14138 | if self._abort: | ||
|
r26587 | raise error.Abort(msg) | ||
|
r43347 | self._ui.warn(_(b"warning: %s\n") % msg) | ||
|
r17201 | self._loweredfiles.add(fl) | ||
self._newfiles.add(f) | ||||
|
r13970 | |||
|
r43346 | |||
|
r24723 | def filteredhash(repo, maxrev): | ||
"""build hash of filtered revisions in the current repoview. | ||||
Multiple caches perform up-to-date validation by checking that the | ||||
tiprev and tipnode stored in the cache file match the current repository. | ||||
However, this is not sufficient for validating repoviews because the set | ||||
of revisions in the view may change without the repository tiprev and | ||||
tipnode changing. | ||||
This function hashes all the revs filtered from the view and returns | ||||
that SHA-1 digest. | ||||
""" | ||||
cl = repo.changelog | ||||
if not cl.filteredrevs: | ||||
return None | ||||
|
r46088 | key = cl._filteredrevs_hashcache.get(maxrev) | ||
if not key: | ||||
revs = sorted(r for r in cl.filteredrevs if r <= maxrev) | ||||
if revs: | ||||
s = hashutil.sha1() | ||||
for rev in revs: | ||||
s.update(b'%d;' % rev) | ||||
key = s.digest() | ||||
cl._filteredrevs_hashcache[maxrev] = key | ||||
|
r24723 | return key | ||
|
r43346 | |||
|
r13975 | def walkrepos(path, followsym=False, seen_dirs=None, recurse=False): | ||
|
r46554 | """yield every hg repository under path, always recursively. | ||
The recurse flag will only control recursion into repo working dirs""" | ||||
|
r43346 | |||
|
r13975 | def errhandler(err): | ||
if err.filename == path: | ||||
raise err | ||||
|
r43346 | |||
|
r14961 | samestat = getattr(os.path, 'samestat', None) | ||
if followsym and samestat is not None: | ||||
|
r43346 | |||
|
r14227 | def adddir(dirlst, dirname): | ||
|
r13975 | dirstat = os.stat(dirname) | ||
|
r36356 | match = any(samestat(dirstat, lstdirstat) for lstdirstat in dirlst) | ||
|
r13975 | if not match: | ||
dirlst.append(dirstat) | ||||
return not match | ||||
|
r43346 | |||
|
r13975 | else: | ||
followsym = False | ||||
if (seen_dirs is None) and followsym: | ||||
seen_dirs = [] | ||||
|
r14227 | adddir(seen_dirs, path) | ||
|
r13975 | for root, dirs, files in os.walk(path, topdown=True, onerror=errhandler): | ||
dirs.sort() | ||||
|
r43347 | if b'.hg' in dirs: | ||
|
r43346 | yield root # found a repository | ||
|
r43347 | qroot = os.path.join(root, b'.hg', b'patches') | ||
if os.path.isdir(os.path.join(qroot, b'.hg')): | ||||
|
r43346 | yield qroot # we have a patch queue repo here | ||
|
r13975 | if recurse: | ||
# avoid recursing inside the .hg directory | ||||
|
r43347 | dirs.remove(b'.hg') | ||
|
r13975 | else: | ||
|
r43346 | dirs[:] = [] # don't descend further | ||
|
r13975 | elif followsym: | ||
newdirs = [] | ||||
for d in dirs: | ||||
fname = os.path.join(root, d) | ||||
|
r14227 | if adddir(seen_dirs, fname): | ||
|
r13975 | if os.path.islink(fname): | ||
for hgname in walkrepos(fname, True, seen_dirs): | ||||
yield hgname | ||||
else: | ||||
newdirs.append(d) | ||||
dirs[:] = newdirs | ||||
|
r13984 | |||
|
r43346 | |||
|
r32656 | def binnode(ctx): | ||
"""Return binary node id for a given basectx""" | ||||
node = ctx.node() | ||||
if node is None: | ||||
|
r47771 | return ctx.repo().nodeconstants.wdirid | ||
|
r32656 | return node | ||
|
r43346 | |||
|
r32654 | def intrev(ctx): | ||
"""Return integer for a given basectx that can be used in comparison or | ||||
|
r24582 | arithmetic operation""" | ||
|
r32654 | rev = ctx.rev() | ||
|
r24582 | if rev is None: | ||
|
r25739 | return wdirrev | ||
|
r24582 | return rev | ||
|
r43346 | |||
|
r34328 | def formatchangeid(ctx): | ||
"""Format changectx as '{rev}:{node|formatnode}', which is the default | ||||
|
r35906 | template provided by logcmdutil.changesettemplater""" | ||
|
r34328 | repo = ctx.repo() | ||
return formatrevnode(repo.ui, intrev(ctx), binnode(ctx)) | ||||
|
r43346 | |||
|
r34328 | def formatrevnode(ui, rev, node): | ||
"""Format given revision and node depending on the current verbosity""" | ||||
if ui.debugflag: | ||||
hexfunc = hex | ||||
else: | ||||
hexfunc = short | ||||
|
r43347 | return b'%d:%s' % (rev, hexfunc(node)) | ||
|
r34328 | |||
|
r43346 | |||
|
r37696 | def resolvehexnodeidprefix(repo, prefix): | ||
|
r45324 | if prefix.startswith(b'x'): | ||
|
r38891 | prefix = prefix[1:] | ||
|
r38878 | try: | ||
# Uses unfiltered repo because it's faster when prefix is ambiguous/ | ||||
# This matches the shortesthexnodeidprefix() function below. | ||||
node = repo.unfiltered().changelog._partialmatch(prefix) | ||||
except error.AmbiguousPrefixLookupError: | ||||
|
r43347 | revset = repo.ui.config( | ||
b'experimental', b'revisions.disambiguatewithin' | ||||
) | ||||
|
r38878 | if revset: | ||
# Clear config to avoid infinite recursion | ||||
|
r43346 | configoverrides = { | ||
|
r43347 | (b'experimental', b'revisions.disambiguatewithin'): None | ||
|
r43346 | } | ||
|
r38878 | with repo.ui.configoverride(configoverrides): | ||
revs = repo.anyrevs([revset], user=True) | ||||
matches = [] | ||||
for rev in revs: | ||||
node = repo.changelog.node(rev) | ||||
if hex(node).startswith(prefix): | ||||
matches.append(node) | ||||
if len(matches) == 1: | ||||
return matches[0] | ||||
raise | ||||
|
r37522 | if node is None: | ||
return | ||||
repo.changelog.rev(node) # make sure node isn't filtered | ||||
return node | ||||
|
r43346 | |||
|
r38890 | def mayberevnum(repo, prefix): | ||
"""Checks if the given prefix may be mistaken for a revision number""" | ||||
try: | ||||
i = int(prefix) | ||||
# if we are a pure int, then starting with zero will not be | ||||
# confused as a rev; or, obviously, if the int is larger | ||||
|
r40377 | # than the value of the tip rev. We still need to disambiguate if | ||
# prefix == '0', since that *is* a valid revnum. | ||||
if (prefix != b'0' and prefix[0:1] == b'0') or i >= len(repo): | ||||
|
r38890 | return False | ||
return True | ||||
except ValueError: | ||||
return False | ||||
|
r43346 | |||
|
r38889 | def shortesthexnodeidprefix(repo, node, minlength=1, cache=None): | ||
"""Find the shortest unambiguous prefix that matches hexnode. | ||||
If "cache" is not None, it must be a dictionary that can be used for | ||||
caching between calls to this method. | ||||
""" | ||||
|
r37726 | # _partialmatch() of filtered changelog could take O(len(repo)) time, | ||
# which would be unacceptably slow. so we look for hash collision in | ||||
# unfiltered space, which means some hashes may be slightly longer. | ||||
|
r37990 | |||
|
r43346 | minlength = max(minlength, 1) | ||
|
r40439 | |||
|
r37990 | def disambiguate(prefix): | ||
"""Disambiguate against revnums.""" | ||||
|
r43347 | if repo.ui.configbool(b'experimental', b'revisions.prefixhexnode'): | ||
|
r38892 | if mayberevnum(repo, prefix): | ||
|
r43347 | return b'x' + prefix | ||
|
r38892 | else: | ||
return prefix | ||||
|
r37990 | hexnode = hex(node) | ||
|
r38000 | for length in range(len(prefix), len(hexnode) + 1): | ||
|
r37990 | prefix = hexnode[:length] | ||
|
r38890 | if not mayberevnum(repo, prefix): | ||
|
r37990 | return prefix | ||
|
r38890 | cl = repo.unfiltered().changelog | ||
|
r43347 | revset = repo.ui.config(b'experimental', b'revisions.disambiguatewithin') | ||
|
r38879 | if revset: | ||
|
r38889 | revs = None | ||
if cache is not None: | ||||
|
r43347 | revs = cache.get(b'disambiguationrevset') | ||
|
r38889 | if revs is None: | ||
revs = repo.anyrevs([revset], user=True) | ||||
if cache is not None: | ||||
|
r43347 | cache[b'disambiguationrevset'] = revs | ||
|
r38879 | if cl.rev(node) in revs: | ||
hexnode = hex(node) | ||||
|
r39262 | nodetree = None | ||
if cache is not None: | ||||
|
r43347 | nodetree = cache.get(b'disambiguationnodetree') | ||
|
r39262 | if not nodetree: | ||
r44363 | if util.safehasattr(parsers, 'nodetree'): | |||
# The CExt is the only implementation to provide a nodetree | ||||
# class so far. | ||||
|
r44465 | index = cl.index | ||
if util.safehasattr(index, 'get_cindex'): | ||||
# the rust wrapped need to give access to its internal index | ||||
index = index.get_cindex() | ||||
nodetree = parsers.nodetree(index, len(revs)) | ||||
|
r39262 | for r in revs: | ||
nodetree.insert(r) | ||||
if cache is not None: | ||||
|
r43347 | cache[b'disambiguationnodetree'] = nodetree | ||
|
r39262 | if nodetree is not None: | ||
length = max(nodetree.shortest(node), minlength) | ||||
prefix = hexnode[:length] | ||||
return disambiguate(prefix) | ||||
|
r38879 | for length in range(minlength, len(hexnode) + 1): | ||
matches = [] | ||||
prefix = hexnode[:length] | ||||
for rev in revs: | ||||
otherhexnode = repo[rev].hex() | ||||
if prefix == otherhexnode[:length]: | ||||
matches.append(otherhexnode) | ||||
if len(matches) == 1: | ||||
return disambiguate(prefix) | ||||
|
r37882 | try: | ||
|
r37990 | return disambiguate(cl.shortest(node, minlength)) | ||
|
r37882 | except error.LookupError: | ||
raise error.RepoLookupError() | ||||
|
r37698 | |||
|
r43346 | |||
|
r37368 | def isrevsymbol(repo, symbol): | ||
|
r37695 | """Checks if a symbol exists in the repo. | ||
|
r38877 | See revsymbol() for details. Raises error.AmbiguousPrefixLookupError if the | ||
symbol is an ambiguous nodeid prefix. | ||||
|
r37695 | """ | ||
|
r37368 | try: | ||
revsymbol(repo, symbol) | ||||
return True | ||||
except error.RepoLookupError: | ||||
return False | ||||
|
r43346 | |||
|
r37289 | def revsymbol(repo, symbol): | ||
"""Returns a context given a single revision symbol (as string). | ||||
This is similar to revsingle(), but accepts only a single revision symbol, | ||||
i.e. things like ".", "tip", "1234", "deadbeef", "my-bookmark" work, but | ||||
not "max(public())". | ||||
""" | ||||
if not isinstance(symbol, bytes): | ||||
|
r43346 | msg = ( | ||
|
r43347 | b"symbol (%s of type %s) was not a string, did you mean " | ||
b"repo[symbol]?" % (symbol, type(symbol)) | ||||
|
r43346 | ) | ||
|
r37289 | raise error.ProgrammingError(msg) | ||
|
r37403 | try: | ||
|
r43347 | if symbol in (b'.', b'tip', b'null'): | ||
|
r37545 | return repo[symbol] | ||
try: | ||||
r = int(symbol) | ||||
|
r43347 | if b'%d' % r != symbol: | ||
|
r37545 | raise ValueError | ||
l = len(repo.changelog) | ||||
if r < 0: | ||||
r += l | ||||
if r < 0 or r >= l and r != wdirrev: | ||||
raise ValueError | ||||
return repo[r] | ||||
except error.FilteredIndexError: | ||||
raise | ||||
except (ValueError, OverflowError, IndexError): | ||||
pass | ||||
|
r47815 | if len(symbol) == 2 * repo.nodeconstants.nodelen: | ||
|
r37546 | try: | ||
node = bin(symbol) | ||||
rev = repo.changelog.rev(node) | ||||
return repo[rev] | ||||
except error.FilteredLookupError: | ||||
raise | ||||
except (TypeError, LookupError): | ||||
pass | ||||
|
r37547 | # look up bookmarks through the name interface | ||
try: | ||||
node = repo.names.singlenode(repo, symbol) | ||||
rev = repo.changelog.rev(node) | ||||
return repo[rev] | ||||
except KeyError: | ||||
pass | ||||
|
r37697 | node = resolvehexnodeidprefix(repo, symbol) | ||
|
r37548 | if node is not None: | ||
rev = repo.changelog.rev(node) | ||||
return repo[rev] | ||||
|
r43347 | raise error.RepoLookupError(_(b"unknown revision '%s'") % symbol) | ||
|
r37545 | |||
|
r37546 | except error.WdirUnsupported: | ||
return repo[None] | ||||
|
r43346 | except ( | ||
error.FilteredIndexError, | ||||
error.FilteredLookupError, | ||||
error.FilteredRepoLookupError, | ||||
): | ||||
|
r37403 | raise _filterederror(repo, symbol) | ||
|
r43346 | |||
|
r37403 | def _filterederror(repo, changeid): | ||
"""build an exception to be raised about a filtered changeid | ||||
This is extracted in a function to help extensions (eg: evolve) to | ||||
experiment with various message variants.""" | ||||
|
r43347 | if repo.filtername.startswith(b'visible'): | ||
|
r37403 | |||
# Check if the changeset is obsolete | ||||
unfilteredrepo = repo.unfiltered() | ||||
ctx = revsymbol(unfilteredrepo, changeid) | ||||
# If the changeset is obsolete, enrich the message with the reason | ||||
# that made this changeset not visible | ||||
if ctx.obsolete(): | ||||
msg = obsutil._getfilteredreason(repo, changeid, ctx) | ||||
else: | ||||
|
r43347 | msg = _(b"hidden revision '%s'") % changeid | ||
|
r37403 | |||
|
r43347 | hint = _(b'use --hidden to access hidden revisions') | ||
|
r37403 | |||
return error.FilteredRepoLookupError(msg, hint=hint) | ||||
|
r43347 | msg = _(b"filtered revision '%s' (not in '%s' subset)") | ||
|
r37403 | msg %= (changeid, repo.filtername) | ||
return error.FilteredRepoLookupError(msg) | ||||
|
r37289 | |||
|
r43346 | |||
|
r43347 | def revsingle(repo, revspec, default=b'.', localalias=None): | ||
|
r19509 | if not revspec and revspec != 0: | ||
|
r14319 | return repo[default] | ||
|
r34007 | l = revrange(repo, [revspec], localalias=localalias) | ||
|
r22814 | if not l: | ||
|
r43347 | raise error.Abort(_(b'empty revision set')) | ||
|
r22815 | return repo[l.last()] | ||
|
r14319 | |||
|
r43346 | |||
|
r26020 | def _pairspec(revspec): | ||
|
r31024 | tree = revsetlang.parse(revspec) | ||
|
r43347 | return tree and tree[0] in ( | ||
b'range', | ||||
b'rangepre', | ||||
b'rangepost', | ||||
b'rangeall', | ||||
) | ||||
|
r26020 | |||
|
r43346 | |||
|
r14319 | def revpair(repo, revs): | ||
if not revs: | ||||
|
r43347 | return repo[b'.'], repo[None] | ||
|
r14319 | |||
l = revrange(repo, revs) | ||||
|
r41415 | if not l: | ||
|
r43347 | raise error.Abort(_(b'empty revision range')) | ||
|
r41415 | |||
|
r41414 | first = l.first() | ||
second = l.last() | ||||
|
r20862 | |||
|
r43346 | if ( | ||
first == second | ||||
and len(revs) >= 2 | ||||
and not all(revrange(repo, [r]) for r in revs) | ||||
): | ||||
|
r43347 | raise error.Abort(_(b'empty revision on one side of range')) | ||
|
r14319 | |||
|
r26020 | # if top-level is range expression, the result must always be a pair | ||
if first == second and len(revs) == 1 and not _pairspec(revs[0]): | ||||
|
r37269 | return repo[first], repo[None] | ||
|
r14319 | |||
|
r37269 | return repo[first], repo[second] | ||
|
r14319 | |||
|
r43346 | |||
|
r34007 | def revrange(repo, specs, localalias=None): | ||
|
r29417 | """Execute 1 to many revsets and return the union. | ||
This is the preferred mechanism for executing revsets using user-specified | ||||
config options, such as revset aliases. | ||||
The revsets specified by ``specs`` will be executed via a chained ``OR`` | ||||
expression. If ``specs`` is empty, an empty result is returned. | ||||
``specs`` can contain integers, in which case they are assumed to be | ||||
revision numbers. | ||||
It is assumed the revsets are already formatted. If you have arguments | ||||
|
r31024 | that need to be expanded in the revset, call ``revsetlang.formatspec()`` | ||
|
r29417 | and pass the result as an element of ``specs``. | ||
Specifying a single revset is allowed. | ||||
|
r44532 | Returns a ``smartset.abstractsmartset`` which is a list-like interface over | ||
|
r29417 | integer revisions. | ||
""" | ||||
|
r25928 | allspecs = [] | ||
|
r29417 | for spec in specs: | ||
|
r25904 | if isinstance(spec, int): | ||
|
r43347 | spec = revsetlang.formatspec(b'%d', spec) | ||
|
r25928 | allspecs.append(spec) | ||
|
r34007 | return repo.anyrevs(allspecs, user=True, localalias=localalias) | ||
|
r14320 | |||
|
r43346 | |||
|
r46312 | def increasingwindows(windowsize=8, sizelimit=512): | ||
while True: | ||||
yield windowsize | ||||
if windowsize < sizelimit: | ||||
windowsize *= 2 | ||||
def walkchangerevs(repo, revs, makefilematcher, prepare): | ||||
|
r46554 | """Iterate over files and the revs in a "windowed" way. | ||
|
r46312 | |||
Callers most commonly need to iterate backwards over the history | ||||
in which they are interested. Doing so has awful (quadratic-looking) | ||||
performance, so we use iterators in a "windowed" way. | ||||
We walk a window of revisions in the desired order. Within the | ||||
window, we first walk forwards to gather data, then in the desired | ||||
order (usually backwards) to display it. | ||||
This function returns an iterator yielding contexts. Before | ||||
yielding each context, the iterator will first call the prepare | ||||
|
r46554 | function on each context in the window in forward order.""" | ||
|
r46312 | |||
if not revs: | ||||
return [] | ||||
change = repo.__getitem__ | ||||
def iterate(): | ||||
it = iter(revs) | ||||
stopiteration = False | ||||
for windowsize in increasingwindows(): | ||||
nrevs = [] | ||||
for i in pycompat.xrange(windowsize): | ||||
rev = next(it, None) | ||||
if rev is None: | ||||
stopiteration = True | ||||
break | ||||
nrevs.append(rev) | ||||
for rev in sorted(nrevs): | ||||
ctx = change(rev) | ||||
prepare(ctx, makefilematcher(ctx)) | ||||
for rev in nrevs: | ||||
yield change(rev) | ||||
if stopiteration: | ||||
break | ||||
return iterate() | ||||
|
r26433 | def meaningfulparents(repo, ctx): | ||
"""Return list of meaningful (or all if debug) parentrevs for rev. | ||||
For merges (two non-nullrev revisions) both parents are meaningful. | ||||
Otherwise the first parent revision is considered meaningful if it | ||||
is not the preceding revision. | ||||
""" | ||||
parents = ctx.parents() | ||||
if len(parents) > 1: | ||||
return parents | ||||
if repo.ui.debugflag: | ||||
|
r39930 | return [parents[0], repo[nullrev]] | ||
|
r32654 | if parents[0].rev() >= intrev(ctx) - 1: | ||
|
r26433 | return [] | ||
return parents | ||||
|
r43346 | |||
|
r41716 | def getuipathfn(repo, legacyrelativevalue=False, forcerelativevalue=None): | ||
"""Return a function that produced paths for presenting to the user. | ||||
The returned function takes a repo-relative path and produces a path | ||||
that can be presented in the UI. | ||||
Depending on the value of ui.relative-paths, either a repo-relative or | ||||
cwd-relative path will be produced. | ||||
legacyrelativevalue is the value to use if ui.relative-paths=legacy | ||||
If forcerelativevalue is not None, then that value will be used regardless | ||||
of what ui.relative-paths is set to. | ||||
""" | ||||
if forcerelativevalue is not None: | ||||
relative = forcerelativevalue | ||||
else: | ||||
|
r43347 | config = repo.ui.config(b'ui', b'relative-paths') | ||
if config == b'legacy': | ||||
|
r41716 | relative = legacyrelativevalue | ||
else: | ||||
relative = stringutil.parsebool(config) | ||||
if relative is None: | ||||
raise error.ConfigError( | ||||
|
r43347 | _(b"ui.relative-paths is not a boolean ('%s')") % config | ||
|
r43346 | ) | ||
|
r41716 | |||
|
r41632 | if relative: | ||
cwd = repo.getcwd() | ||||
|
r45385 | if cwd != b'': | ||
|
r45420 | # this branch would work even if cwd == b'' (ie cwd = repo | ||
# root), but its generality makes the returned function slower | ||||
|
r45385 | pathto = repo.pathto | ||
return lambda f: pathto(f, cwd) | ||||
if repo.ui.configbool(b'ui', b'slash'): | ||||
|
r41833 | return lambda f: f | ||
|
r41632 | else: | ||
|
r41833 | return util.localpath | ||
|
r41632 | |||
|
r43346 | |||
|
r41799 | def subdiruipathfn(subpath, uipathfn): | ||
'''Create a new uipathfn that treats the file as relative to subpath.''' | ||||
return lambda f: uipathfn(posixpath.join(subpath, f)) | ||||
|
r43346 | |||
|
r41801 | def anypats(pats, opts): | ||
|
r46554 | """Checks if any patterns, including --include and --exclude were given. | ||
|
r41801 | |||
Some commands (e.g. addremove) use this condition for deciding whether to | ||||
print absolute or relative paths. | ||||
|
r46554 | """ | ||
|
r43347 | return bool(pats or opts.get(b'include') or opts.get(b'exclude')) | ||
|
r41801 | |||
|
r43346 | |||
|
r14320 | def expandpats(pats): | ||
|
r46554 | """Expand bare globs when running on windows. | ||
On posix we assume it already has already been done by sh.""" | ||||
|
r14320 | if not util.expandglobs: | ||
return list(pats) | ||||
ret = [] | ||||
|
r21111 | for kindpat in pats: | ||
kind, pat = matchmod._patsplit(kindpat, None) | ||||
|
r14320 | if kind is None: | ||
try: | ||||
|
r21111 | globbed = glob.glob(pat) | ||
|
r14320 | except re.error: | ||
|
r21111 | globbed = [pat] | ||
|
r14320 | if globbed: | ||
ret.extend(globbed) | ||||
continue | ||||
|
r21111 | ret.append(kindpat) | ||
|
r14320 | return ret | ||
|
r43346 | |||
def matchandpats( | ||||
|
r43347 | ctx, pats=(), opts=None, globbed=False, default=b'relpath', badfn=None | ||
|
r43346 | ): | ||
|
r46554 | """Return a matcher and the patterns that were used. | ||
|
r25467 | The matcher will warn about bad matches, unless an alternate badfn callback | ||
|
r46554 | is provided.""" | ||
|
r26326 | if opts is None: | ||
opts = {} | ||||
|
r43347 | if not globbed and default == b'relpath': | ||
|
r14320 | pats = expandpats(pats or []) | ||
|
r14670 | |||
|
r41814 | uipathfn = getuipathfn(ctx.repo(), legacyrelativevalue=True) | ||
|
r43346 | |||
|
r25467 | def bad(f, msg): | ||
|
r43347 | ctx.repo().ui.warn(b"%s: %s\n" % (uipathfn(f), msg)) | ||
|
r25466 | |||
|
r25467 | if badfn is None: | ||
badfn = bad | ||||
|
r43346 | m = ctx.match( | ||
pats, | ||||
|
r43347 | opts.get(b'include'), | ||
opts.get(b'exclude'), | ||||
|
r43346 | default, | ||
|
r43347 | listsubrepos=opts.get(b'subrepos'), | ||
|
r43346 | badfn=badfn, | ||
) | ||||
|
r25466 | |||
|
r24447 | if m.always(): | ||
pats = [] | ||||
|
r16171 | return m, pats | ||
|
r43346 | |||
def match( | ||||
|
r43347 | ctx, pats=(), opts=None, globbed=False, default=b'relpath', badfn=None | ||
|
r43346 | ): | ||
|
r21111 | '''Return a matcher that will warn about bad matches.''' | ||
|
r25467 | return matchandpats(ctx, pats, opts, globbed, default, badfn=badfn)[0] | ||
|
r14320 | |||
|
r43346 | |||
|
r14320 | def matchall(repo): | ||
|
r21111 | '''Return a matcher that will efficiently match everything.''' | ||
|
r41825 | return matchmod.always() | ||
|
r14320 | |||
|
r43346 | |||
|
r25467 | def matchfiles(repo, files, badfn=None): | ||
|
r21111 | '''Return a matcher that will efficiently match exactly these files.''' | ||
|
r41825 | return matchmod.exact(files, badfn=badfn) | ||
|
r14320 | |||
|
r43346 | |||
|
r34855 | def parsefollowlinespattern(repo, rev, pat, msg): | ||
"""Return a file name from `pat` pattern suitable for usage in followlines | ||||
logic. | ||||
""" | ||||
if not matchmod.patkind(pat): | ||||
return pathutil.canonpath(repo.root, repo.getcwd(), pat) | ||||
else: | ||||
ctx = repo[rev] | ||||
m = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=ctx) | ||||
files = [f for f in ctx if m(f)] | ||||
if len(files) != 1: | ||||
raise error.ParseError(msg) | ||||
return files[0] | ||||
|
r43346 | |||
|
r40783 | def getorigvfs(ui, repo): | ||
"""return a vfs suitable to save 'orig' file | ||||
return None if no special directory is configured""" | ||||
|
r43347 | origbackuppath = ui.config(b'ui', b'origbackuppath') | ||
|
r40783 | if not origbackuppath: | ||
return None | ||||
return vfs.vfs(repo.wvfs.join(origbackuppath)) | ||||
|
r43346 | |||
|
r41735 | def backuppath(ui, repo, filepath): | ||
|
r46554 | """customize where working copy backup files (.orig files) are created | ||
|
r41735 | |||
Fetch user defined path from config file: [ui] origbackuppath = <path> | ||||
Fall back to default (filepath with .orig suffix) if not specified | ||||
filepath is repo-relative | ||||
Returns an absolute path | ||||
|
r46554 | """ | ||
|
r41735 | origvfs = getorigvfs(ui, repo) | ||
if origvfs is None: | ||||
|
r43347 | return repo.wjoin(filepath + b".orig") | ||
|
r41735 | |||
origbackupdir = origvfs.dirname(filepath) | ||||
if not origvfs.isdir(origbackupdir) or origvfs.islink(origbackupdir): | ||||
|
r43347 | ui.note(_(b'creating directory: %s\n') % origvfs.join(origbackupdir)) | ||
|
r41735 | |||
# Remove any files that conflict with the backup file's path | ||||
|
r44032 | for f in reversed(list(pathutil.finddirs(filepath))): | ||
|
r41735 | if origvfs.isfileorlink(f): | ||
|
r43347 | ui.note(_(b'removing conflicting file: %s\n') % origvfs.join(f)) | ||
|
r41735 | origvfs.unlink(f) | ||
break | ||||
origvfs.makedirs(origbackupdir) | ||||
if origvfs.isdir(filepath) and not origvfs.islink(filepath): | ||||
|
r43346 | ui.note( | ||
|
r43347 | _(b'removing conflicting directory: %s\n') % origvfs.join(filepath) | ||
|
r43346 | ) | ||
|
r41735 | origvfs.rmtree(filepath, forcibly=True) | ||
return origvfs.join(filepath) | ||||
|
r43346 | |||
|
r33331 | class _containsnode(object): | ||
"""proxy __contains__(node) to container.__contains__ which accepts revs""" | ||||
def __init__(self, repo, revcontainer): | ||||
self._torev = repo.changelog.rev | ||||
self._revcontains = revcontainer.__contains__ | ||||
def __contains__(self, node): | ||||
return self._revcontains(self._torev(node)) | ||||
|
r43346 | |||
def cleanupnodes( | ||||
repo, | ||||
replacements, | ||||
operation, | ||||
moves=None, | ||||
metadata=None, | ||||
fixphase=False, | ||||
targetphase=None, | ||||
backup=True, | ||||
): | ||||
|
r33088 | """do common cleanups when old nodes are replaced by new nodes | ||
That includes writing obsmarkers or stripping nodes, and moving bookmarks. | ||||
(we might also want to move working directory parent in the future) | ||||
|
r34364 | By default, bookmark moves are calculated automatically from 'replacements', | ||
but 'moves' can be used to override that. Also, 'moves' may include | ||||
additional bookmark moves that should not have associated obsmarkers. | ||||
|
r34363 | replacements is {oldnode: [newnode]} or a iterable of nodes if they do not | ||
have replacements. operation is a string, like "rebase". | ||||
|
r34794 | |||
metadata is dictionary containing metadata to be stored in obsmarker if | ||||
obsolescence is enabled. | ||||
|
r33088 | """ | ||
|
r38442 | assert fixphase or targetphase is None | ||
|
r34364 | if not replacements and not moves: | ||
return | ||||
# translate mapping's other forms | ||||
|
r43347 | if not util.safehasattr(replacements, b'items'): | ||
|
r39927 | replacements = {(n,): () for n in replacements} | ||
else: | ||||
# upgrading non tuple "source" to tuple ones for BC | ||||
repls = {} | ||||
for key, value in replacements.items(): | ||||
if not isinstance(key, tuple): | ||||
key = (key,) | ||||
repls[key] = value | ||||
replacements = repls | ||||
|
r33088 | |||
|
r40890 | # Unfiltered repo is needed since nodes in replacements might be hidden. | ||
unfi = repo.unfiltered() | ||||
|
r34362 | # Calculate bookmark movements | ||
|
r34364 | if moves is None: | ||
moves = {} | ||||
|
r40890 | for oldnodes, newnodes in replacements.items(): | ||
for oldnode in oldnodes: | ||||
if oldnode in moves: | ||||
continue | ||||
if len(newnodes) > 1: | ||||
# usually a split, take the one with biggest rev number | ||||
|
r43347 | newnode = next(unfi.set(b'max(%ln)', newnodes)).node() | ||
|
r40890 | elif len(newnodes) == 0: | ||
# move bookmark backwards | ||||
allreplaced = [] | ||||
for rep in replacements: | ||||
allreplaced.extend(rep) | ||||
|
r43346 | roots = list( | ||
|
r43347 | unfi.set(b'max((::%n) - %ln)', oldnode, allreplaced) | ||
|
r43346 | ) | ||
|
r40890 | if roots: | ||
newnode = roots[0].node() | ||||
else: | ||||
|
r47771 | newnode = repo.nullid | ||
|
r39927 | else: | ||
|
r40890 | newnode = newnodes[0] | ||
moves[oldnode] = newnode | ||||
|
r34362 | |||
|
r38442 | allnewnodes = [n for ns in replacements.values() for n in ns] | ||
toretract = {} | ||||
toadvance = {} | ||||
if fixphase: | ||||
precursors = {} | ||||
|
r39927 | for oldnodes, newnodes in replacements.items(): | ||
for oldnode in oldnodes: | ||||
for newnode in newnodes: | ||||
precursors.setdefault(newnode, []).append(oldnode) | ||||
|
r38442 | |||
allnewnodes.sort(key=lambda n: unfi[n].rev()) | ||||
newphases = {} | ||||
|
r43346 | |||
|
r38442 | def phase(ctx): | ||
return newphases.get(ctx.node(), ctx.phase()) | ||||
|
r43346 | |||
|
r38442 | for newnode in allnewnodes: | ||
ctx = unfi[newnode] | ||||
|
r38451 | parentphase = max(phase(p) for p in ctx.parents()) | ||
|
r38442 | if targetphase is None: | ||
|
r43346 | oldphase = max( | ||
unfi[oldnode].phase() for oldnode in precursors[newnode] | ||||
) | ||||
|
r38442 | newphase = max(oldphase, parentphase) | ||
else: | ||||
|
r38451 | newphase = max(targetphase, parentphase) | ||
|
r38442 | newphases[newnode] = newphase | ||
if newphase > ctx.phase(): | ||||
toretract.setdefault(newphase, []).append(newnode) | ||||
elif newphase < ctx.phase(): | ||||
toadvance.setdefault(newphase, []).append(newnode) | ||||
|
r43347 | with repo.transaction(b'cleanup') as tr: | ||
|
r33088 | # Move bookmarks | ||
bmarks = repo._bookmarks | ||||
|
r33511 | bmarkchanges = [] | ||
|
r34362 | for oldnode, newnode in moves.items(): | ||
|
r33088 | oldbmarks = repo.nodebookmarks(oldnode) | ||
if not oldbmarks: | ||||
continue | ||||
|
r43346 | from . import bookmarks # avoid import cycle | ||
repo.ui.debug( | ||||
|
r43347 | b'moving bookmarks %r from %s to %s\n' | ||
|
r43346 | % ( | ||
pycompat.rapply(pycompat.maybebytestr, oldbmarks), | ||||
hex(oldnode), | ||||
hex(newnode), | ||||
) | ||||
) | ||||
|
r33331 | # Delete divergent bookmarks being parents of related newnodes | ||
|
r43346 | deleterevs = repo.revs( | ||
|
r43347 | b'parents(roots(%ln & (::%n))) - parents(%n)', | ||
|
r43346 | allnewnodes, | ||
newnode, | ||||
oldnode, | ||||
) | ||||
|
r33331 | deletenodes = _containsnode(repo, deleterevs) | ||
|
r33088 | for name in oldbmarks: | ||
|
r33511 | bmarkchanges.append((name, newnode)) | ||
for b in bookmarks.divergent2delete(repo, deletenodes, name): | ||||
bmarkchanges.append((b, None)) | ||||
if bmarkchanges: | ||||
bmarks.applychanges(repo, tr, bmarkchanges) | ||||
|
r33088 | |||
|
r38442 | for phase, nodes in toretract.items(): | ||
phases.retractboundary(repo, tr, phase, nodes) | ||||
for phase, nodes in toadvance.items(): | ||||
phases.advanceboundary(repo, tr, phase, nodes) | ||||
|
r43347 | mayusearchived = repo.ui.config(b'experimental', b'cleanup-as-archived') | ||
|
r33088 | # Obsolete or strip nodes | ||
if obsolete.isenabled(repo, obsolete.createmarkersopt): | ||||
# If a node is already obsoleted, and we want to obsolete it | ||||
# without a successor, skip that obssolete request since it's | ||||
# unnecessary. That's the "if s or not isobs(n)" check below. | ||||
# Also sort the node in topology order, that might be useful for | ||||
# some obsstore logic. | ||||
|
r40077 | # NOTE: the sorting might belong to createmarkers. | ||
|
r33330 | torev = unfi.changelog.rev | ||
|
r39927 | sortfunc = lambda ns: torev(ns[0][0]) | ||
|
r39926 | rels = [] | ||
|
r39927 | for ns, s in sorted(replacements.items(), key=sortfunc): | ||
|
r39959 | rel = (tuple(unfi[n] for n in ns), tuple(unfi[m] for m in s)) | ||
rels.append(rel) | ||||
|
r34364 | if rels: | ||
|
r43346 | obsolete.createmarkers( | ||
repo, rels, operation=operation, metadata=metadata | ||||
) | ||||
|
r41961 | elif phases.supportinternal(repo) and mayusearchived: | ||
# this assume we do not have "unstable" nodes above the cleaned ones | ||||
allreplaced = set() | ||||
for ns in replacements.keys(): | ||||
allreplaced.update(ns) | ||||
if backup: | ||||
|
r43346 | from . import repair # avoid import cycle | ||
|
r41961 | node = min(allreplaced, key=repo.changelog.rev) | ||
|
r43346 | repair.backupbundle( | ||
repo, allreplaced, allreplaced, node, operation | ||||
) | ||||
|
r41961 | phases.retractboundary(repo, tr, phases.archived, allreplaced) | ||
|
r33088 | else: | ||
|
r43346 | from . import repair # avoid import cycle | ||
|
r39927 | tostrip = list(n for ns in replacements for n in ns) | ||
|
r34364 | if tostrip: | ||
|
r43346 | repair.delayedstrip( | ||
repo.ui, repo, tostrip, operation, backup=backup | ||||
) | ||||
|
r33088 | |||
|
r41801 | def addremove(repo, matcher, prefix, uipathfn, opts=None): | ||
|
r26329 | if opts is None: | ||
opts = {} | ||||
|
r23533 | m = matcher | ||
|
r43347 | dry_run = opts.get(b'dry_run') | ||
|
r37322 | try: | ||
|
r43347 | similarity = float(opts.get(b'similarity') or 0) | ||
|
r37322 | except ValueError: | ||
|
r43347 | raise error.Abort(_(b'similarity must be a number')) | ||
|
r37322 | if similarity < 0 or similarity > 100: | ||
|
r43347 | raise error.Abort(_(b'similarity must be between 0 and 100')) | ||
|
r37322 | similarity /= 100.0 | ||
|
r23533 | |||
|
r23537 | ret = 0 | ||
wctx = repo[None] | ||||
for subpath in sorted(wctx.substate): | ||||
|
r29802 | submatch = matchmod.subdirmatcher(subpath, m) | ||
|
r43347 | if opts.get(b'subrepos') or m.exact(subpath) or any(submatch.files()): | ||
|
r23537 | sub = wctx.sub(subpath) | ||
|
r41778 | subprefix = repo.wvfs.reljoin(prefix, subpath) | ||
|
r41801 | subuipathfn = subdiruipathfn(subpath, uipathfn) | ||
|
r23537 | try: | ||
|
r41801 | if sub.addremove(submatch, subprefix, subuipathfn, opts): | ||
|
r23537 | ret = 1 | ||
except error.LookupError: | ||||
|
r43346 | repo.ui.status( | ||
|
r43347 | _(b"skipping missing subrepository: %s\n") | ||
|
r43346 | % uipathfn(subpath) | ||
) | ||||
|
r23537 | |||
|
r16167 | rejected = [] | ||
|
r43346 | |||
|
r23534 | def badfn(f, msg): | ||
if f in m.files(): | ||||
|
r25434 | m.bad(f, msg) | ||
|
r23534 | rejected.append(f) | ||
|
r16167 | |||
|
r25434 | badmatch = matchmod.badmatch(m, badfn) | ||
|
r43346 | added, unknown, deleted, removed, forgotten = _interestingfiles( | ||
repo, badmatch | ||||
) | ||||
|
r18863 | |||
|
r23259 | unknownset = set(unknown + forgotten) | ||
|
r18863 | toprint = unknownset.copy() | ||
toprint.update(deleted) | ||||
for abs in sorted(toprint): | ||||
if repo.ui.verbose or not m.exact(abs): | ||||
if abs in unknownset: | ||||
|
r43347 | status = _(b'adding %s\n') % uipathfn(abs) | ||
label = b'ui.addremove.added' | ||||
|
r18863 | else: | ||
|
r43347 | status = _(b'removing %s\n') % uipathfn(abs) | ||
label = b'ui.addremove.removed' | ||||
|
r39123 | repo.ui.status(status, label=label) | ||
|
r18863 | |||
|
r43346 | renames = _findrenames( | ||
repo, m, added + unknown, removed + deleted, similarity, uipathfn | ||||
) | ||||
|
r14320 | |||
if not dry_run: | ||||
|
r23259 | _markchanges(repo, unknown + forgotten, deleted, renames) | ||
|
r14320 | |||
|
r16167 | for f in rejected: | ||
if f in m.files(): | ||||
return 1 | ||||
|
r23537 | return ret | ||
|
r16167 | |||
|
r43346 | |||
|
r19154 | def marktouched(repo, files, similarity=0.0): | ||
|
r46554 | """Assert that files have somehow been operated upon. files are relative to | ||
the repo root.""" | ||||
|
r25467 | m = matchfiles(repo, files, badfn=lambda x, y: rejected.append(x)) | ||
|
r19154 | rejected = [] | ||
|
r23259 | added, unknown, deleted, removed, forgotten = _interestingfiles(repo, m) | ||
|
r19154 | |||
if repo.ui.verbose: | ||||
|
r23259 | unknownset = set(unknown + forgotten) | ||
|
r19154 | toprint = unknownset.copy() | ||
toprint.update(deleted) | ||||
for abs in sorted(toprint): | ||||
if abs in unknownset: | ||||
|
r43347 | status = _(b'adding %s\n') % abs | ||
|
r19154 | else: | ||
|
r43347 | status = _(b'removing %s\n') % abs | ||
|
r19154 | repo.ui.status(status) | ||
|
r41811 | # TODO: We should probably have the caller pass in uipathfn and apply it to | ||
|
r41846 | # the messages above too. legacyrelativevalue=True is consistent with how | ||
|
r41811 | # it used to work. | ||
|
r41834 | uipathfn = getuipathfn(repo, legacyrelativevalue=True) | ||
|
r43346 | renames = _findrenames( | ||
repo, m, added + unknown, removed + deleted, similarity, uipathfn | ||||
) | ||||
|
r19154 | |||
|
r23259 | _markchanges(repo, unknown + forgotten, deleted, renames) | ||
|
r19154 | |||
for f in rejected: | ||||
if f in m.files(): | ||||
return 1 | ||||
return 0 | ||||
|
r43346 | |||
|
r19150 | def _interestingfiles(repo, matcher): | ||
|
r46554 | """Walk dirstate with matcher, looking for files that addremove would care | ||
|
r19150 | about. | ||
This is different from dirstate.status because it doesn't care about | ||||
|
r46554 | whether files are modified or clean.""" | ||
|
r23259 | added, unknown, deleted, removed, forgotten = [], [], [], [], [] | ||
|
r33722 | audit_path = pathutil.pathauditor(repo.root, cached=True) | ||
|
r19150 | |||
ctx = repo[None] | ||||
dirstate = repo.dirstate | ||||
|
r40123 | matcher = repo.narrowmatch(matcher, includeexact=True) | ||
|
r43346 | walkresults = dirstate.walk( | ||
matcher, | ||||
subrepos=sorted(ctx.substate), | ||||
unknown=True, | ||||
ignored=False, | ||||
full=False, | ||||
) | ||||
|
r43376 | for abs, st in pycompat.iteritems(walkresults): | ||
|
r19150 | dstate = dirstate[abs] | ||
|
r43347 | if dstate == b'?' and audit_path.check(abs): | ||
|
r19150 | unknown.append(abs) | ||
|
r43347 | elif dstate != b'r' and not st: | ||
|
r19150 | deleted.append(abs) | ||
|
r43347 | elif dstate == b'r' and st: | ||
|
r23259 | forgotten.append(abs) | ||
|
r19150 | # for finding renames | ||
|
r43347 | elif dstate == b'r' and not st: | ||
|
r19150 | removed.append(abs) | ||
|
r43347 | elif dstate == b'a': | ||
|
r19150 | added.append(abs) | ||
|
r23259 | return added, unknown, deleted, removed, forgotten | ||
|
r19150 | |||
|
r43346 | |||
|
r41811 | def _findrenames(repo, matcher, added, removed, similarity, uipathfn): | ||
|
r19152 | '''Find renames from removed files to added ones.''' | ||
renames = {} | ||||
if similarity > 0: | ||||
|
r43346 | for old, new, score in similar.findrenames( | ||
repo, added, removed, similarity | ||||
): | ||||
if ( | ||||
repo.ui.verbose | ||||
or not matcher.exact(old) | ||||
or not matcher.exact(new) | ||||
): | ||||
repo.ui.status( | ||||
_( | ||||
|
r43347 | b'recording removal of %s as rename to %s ' | ||
b'(%d%% similar)\n' | ||||
|
r43346 | ) | ||
% (uipathfn(old), uipathfn(new), score * 100) | ||||
) | ||||
|
r19152 | renames[new] = old | ||
return renames | ||||
|
r43346 | |||
|
r19153 | def _markchanges(repo, unknown, deleted, renames): | ||
|
r46554 | """Marks the files in unknown as added, the files in deleted as removed, | ||
and the files in renames as copied.""" | ||||
|
r19153 | wctx = repo[None] | ||
|
r27851 | with repo.wlock(): | ||
|
r19153 | wctx.forget(deleted) | ||
wctx.add(unknown) | ||||
|
r43376 | for new, old in pycompat.iteritems(renames): | ||
|
r19153 | wctx.copy(old, new) | ||
|
r43346 | |||
|
r41947 | def getrenamedfn(repo, endrev=None): | ||
|
r42284 | if copiesmod.usechangesetcentricalgo(repo): | ||
|
r43346 | |||
|
r42283 | def getrenamed(fn, rev): | ||
ctx = repo[rev] | ||||
p1copies = ctx.p1copies() | ||||
if fn in p1copies: | ||||
return p1copies[fn] | ||||
p2copies = ctx.p2copies() | ||||
if fn in p2copies: | ||||
return p2copies[fn] | ||||
return None | ||||
|
r43346 | |||
|
r42283 | return getrenamed | ||
|
r41947 | rcache = {} | ||
if endrev is None: | ||||
endrev = len(repo) | ||||
def getrenamed(fn, rev): | ||||
|
r46554 | """looks up all renames for a file (up to endrev) the first | ||
|
r41947 | time the file is given. It indexes on the changerev and only | ||
parses the manifest if linkrev != changerev. | ||||
|
r46554 | Returns rename info for fn at changerev rev.""" | ||
|
r41947 | if fn not in rcache: | ||
rcache[fn] = {} | ||||
fl = repo.file(fn) | ||||
for i in fl: | ||||
lr = fl.linkrev(i) | ||||
renamed = fl.renamed(fl.node(i)) | ||||
rcache[fn][lr] = renamed and renamed[0] | ||||
if lr >= endrev: | ||||
break | ||||
if rev in rcache[fn]: | ||||
return rcache[fn][rev] | ||||
# If linkrev != rev (i.e. rev not found in rcache) fallback to | ||||
# filectx logic. | ||||
try: | ||||
return repo[rev][fn].copysource() | ||||
except error.LookupError: | ||||
return None | ||||
return getrenamed | ||||
|
r43346 | |||
|
r42703 | def getcopiesfn(repo, endrev=None): | ||
if copiesmod.usechangesetcentricalgo(repo): | ||||
|
r43346 | |||
|
r42703 | def copiesfn(ctx): | ||
if ctx.p2copies(): | ||||
allcopies = ctx.p1copies().copy() | ||||
# There should be no overlap | ||||
allcopies.update(ctx.p2copies()) | ||||
return sorted(allcopies.items()) | ||||
else: | ||||
return sorted(ctx.p1copies().items()) | ||||
|
r43346 | |||
|
r42703 | else: | ||
getrenamed = getrenamedfn(repo, endrev) | ||||
|
r43346 | |||
|
r42703 | def copiesfn(ctx): | ||
copies = [] | ||||
for fn in ctx.files(): | ||||
rename = getrenamed(fn, ctx.rev()) | ||||
if rename: | ||||
copies.append((fn, rename)) | ||||
return copies | ||||
return copiesfn | ||||
|
r43346 | |||
|
r14320 | def dirstatecopy(ui, repo, wctx, src, dst, dryrun=False, cwd=None): | ||
"""Update the dirstate to reflect the intent of copying src to dst. For | ||||
different reasons it might not end with dst being marked as copied from src. | ||||
""" | ||||
origsrc = repo.dirstate.copied(src) or src | ||||
|
r43346 | if dst == origsrc: # copying back a copy? | ||
|
r43347 | if repo.dirstate[dst] not in b'mn' and not dryrun: | ||
|
r14320 | repo.dirstate.normallookup(dst) | ||
else: | ||||
|
r43347 | if repo.dirstate[origsrc] == b'a' and origsrc == src: | ||
|
r14320 | if not ui.quiet: | ||
|
r43346 | ui.warn( | ||
_( | ||||
|
r43347 | b"%s has not been committed yet, so no copy " | ||
b"data will be stored for %s.\n" | ||||
|
r43346 | ) | ||
% (repo.pathto(origsrc, cwd), repo.pathto(dst, cwd)) | ||||
) | ||||
|
r43347 | if repo.dirstate[dst] in b'?r' and not dryrun: | ||
|
r14320 | wctx.add([dst]) | ||
elif not dryrun: | ||||
wctx.copy(origsrc, dst) | ||||
|
r14482 | |||
|
r43346 | |||
|
r42103 | def movedirstate(repo, newctx, match=None): | ||
|
r42104 | """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. | ||||
""" | ||||
|
r43347 | oldctx = repo[b'.'] | ||
|
r42103 | ds = repo.dirstate | ||
|
r44490 | copies = dict(ds.copies()) | ||
|
r47771 | ds.setparents(newctx.node(), repo.nullid) | ||
|
r42103 | s = newctx.status(oldctx, match=match) | ||
for f in s.modified: | ||||
|
r43347 | if ds[f] == b'r': | ||
|
r42103 | # modified + removed -> removed | ||
continue | ||||
ds.normallookup(f) | ||||
for f in s.added: | ||||
|
r43347 | if ds[f] == b'r': | ||
|
r42103 | # added + removed -> unknown | ||
ds.drop(f) | ||||
|
r43347 | elif ds[f] != b'a': | ||
|
r42103 | ds.add(f) | ||
for f in s.removed: | ||||
|
r43347 | if ds[f] == b'a': | ||
|
r42103 | # removed + added -> normal | ||
ds.normallookup(f) | ||||
|
r43347 | elif ds[f] != b'r': | ||
|
r42103 | ds.remove(f) | ||
# Merge old parent and old working dir copies | ||||
oldcopies = copiesmod.pathcopies(newctx, oldctx, match) | ||||
oldcopies.update(copies) | ||||
|
r44937 | copies = { | ||
dst: oldcopies.get(src, src) | ||||
|
r43376 | for dst, src in pycompat.iteritems(oldcopies) | ||
|
r44937 | } | ||
|
r42103 | # Adjust the dirstate copies | ||
|
r43376 | for dst, src in pycompat.iteritems(copies): | ||
|
r43347 | if src not in newctx or dst in newctx or ds[dst] != b'a': | ||
|
r42103 | src = None | ||
ds.copy(src, dst) | ||||
r44563 | repo._quick_access_changeid_invalidate() | |||
|
r42103 | |||
|
r43346 | |||
|
r46054 | def filterrequirements(requirements): | ||
|
r46554 | """filters the requirements into two sets: | ||
|
r46054 | |||
wcreq: requirements which should be written in .hg/requires | ||||
storereq: which should be written in .hg/store/requires | ||||
Returns (wcreq, storereq) | ||||
""" | ||||
|
r46055 | if requirementsmod.SHARESAFE_REQUIREMENT in requirements: | ||
|
r46054 | wc, store = set(), set() | ||
for r in requirements: | ||||
if r in requirementsmod.WORKING_DIR_REQUIREMENTS: | ||||
wc.add(r) | ||||
else: | ||||
store.add(r) | ||||
return wc, store | ||||
return requirements, None | ||||
|
r46129 | def istreemanifest(repo): | ||
|
r47856 | """returns whether the repository is using treemanifest or not""" | ||
|
r46129 | return requirementsmod.TREEMANIFEST_REQUIREMENT in repo.requirements | ||
|
r45666 | def writereporequirements(repo, requirements=None): | ||
|
r46859 | """writes requirements for the repo | ||
Requirements are written to .hg/requires and .hg/store/requires based | ||||
on whether share-safe mode is enabled and which requirements are wdir | ||||
requirements and which are store requirements | ||||
""" | ||||
|
r45666 | if requirements: | ||
repo.requirements = requirements | ||||
|
r46054 | wcreq, storereq = filterrequirements(repo.requirements) | ||
if wcreq is not None: | ||||
writerequires(repo.vfs, wcreq) | ||||
if storereq is not None: | ||||
writerequires(repo.svfs, storereq) | ||||
|
r46531 | elif repo.ui.configbool(b'format', b'usestore'): | ||
# only remove store requires if we are using store | ||||
repo.svfs.tryunlink(b'requires') | ||||
|
r45666 | |||
|
r24934 | def writerequires(opener, requirements): | ||
|
r43347 | with opener(b'requires', b'w', atomictemp=True) as fp: | ||
|
r27706 | for r in sorted(requirements): | ||
|
r43347 | fp.write(b"%s\n" % r) | ||
|
r24934 | |||
|
r43346 | |||
|
r20043 | class filecachesubentry(object): | ||
|
r20042 | def __init__(self, path, stat): | ||
|
r14928 | self.path = path | ||
|
r18315 | self.cachestat = None | ||
self._cacheable = None | ||||
|
r14928 | |||
|
r18315 | if stat: | ||
|
r20043 | self.cachestat = filecachesubentry.stat(self.path) | ||
|
r18315 | |||
if self.cachestat: | ||||
self._cacheable = self.cachestat.cacheable() | ||||
else: | ||||
# None means we don't know yet | ||||
self._cacheable = None | ||||
|
r14928 | |||
def refresh(self): | ||||
if self.cacheable(): | ||||
|
r20043 | self.cachestat = filecachesubentry.stat(self.path) | ||
|
r14928 | |||
def cacheable(self): | ||||
if self._cacheable is not None: | ||||
return self._cacheable | ||||
# we don't know yet, assume it is for now | ||||
return True | ||||
def changed(self): | ||||
# no point in going further if we can't cache it | ||||
if not self.cacheable(): | ||||
return True | ||||
|
r20043 | newstat = filecachesubentry.stat(self.path) | ||
|
r14928 | |||
# we may not know if it's cacheable yet, check again now | ||||
if newstat and self._cacheable is None: | ||||
self._cacheable = newstat.cacheable() | ||||
# check again | ||||
if not self._cacheable: | ||||
return True | ||||
if self.cachestat != newstat: | ||||
self.cachestat = newstat | ||||
return True | ||||
else: | ||||
return False | ||||
@staticmethod | ||||
def stat(path): | ||||
try: | ||||
return util.cachestat(path) | ||||
|
r25660 | except OSError as e: | ||
|
r14928 | if e.errno != errno.ENOENT: | ||
raise | ||||
|
r43346 | |||
|
r20044 | class filecacheentry(object): | ||
def __init__(self, paths, stat=True): | ||||
self._entries = [] | ||||
for path in paths: | ||||
self._entries.append(filecachesubentry(path, stat)) | ||||
def changed(self): | ||||
'''true if any entry has changed''' | ||||
for entry in self._entries: | ||||
if entry.changed(): | ||||
return True | ||||
return False | ||||
def refresh(self): | ||||
for entry in self._entries: | ||||
entry.refresh() | ||||
|
r43346 | |||
|
r14928 | class filecache(object): | ||
|
r38698 | """A property like decorator that tracks files under .hg/ for updates. | ||
|
r14928 | |||
|
r38698 | On first access, the files defined as arguments are stat()ed and the | ||
results cached. The decorated function is called. The results are stashed | ||||
away in a ``_filecache`` dict on the object whose method is decorated. | ||||
|
r14928 | |||
|
r40454 | On subsequent access, the cached result is used as it is set to the | ||
instance dictionary. | ||||
|
r38698 | |||
|
r40454 | On external property set/delete operations, the caller must update the | ||
corresponding _filecache entry appropriately. Use __class__.<attr>.set() | ||||
instead of directly setting <attr>. | ||||
|
r14928 | |||
|
r40454 | When using the property API, the cached data is always used if available. | ||
No stat() is performed to check if the file has changed. | ||||
|
r20045 | |||
|
r38698 | Others can muck about with the state of the ``_filecache`` dict. e.g. they | ||
can populate an entry before the property's getter is called. In this case, | ||||
entries in ``_filecache`` will be used during property operations, | ||||
if available. If the underlying file changes, it is up to external callers | ||||
to reflect this by e.g. calling ``delattr(obj, attr)`` to remove the cached | ||||
method result as well as possibly calling ``del obj._filecache[attr]`` to | ||||
remove the ``filecacheentry``. | ||||
""" | ||||
|
r20045 | def __init__(self, *paths): | ||
self.paths = paths | ||||
|
r16198 | |||
def join(self, obj, fname): | ||||
|
r20045 | """Used to compute the runtime path of a cached file. | ||
|
r16198 | |||
Users should subclass filecache and provide their own version of this | ||||
function to call the appropriate join function on 'obj' (an instance | ||||
of the class that its member function was decorated). | ||||
""" | ||||
|
r31285 | raise NotImplementedError | ||
|
r14928 | |||
def __call__(self, func): | ||||
self.func = func | ||||
|
r37886 | self.sname = func.__name__ | ||
self.name = pycompat.sysbytes(self.sname) | ||||
|
r14928 | return self | ||
def __get__(self, obj, type=None): | ||||
|
r29373 | # if accessed on the class, return the descriptor itself. | ||
if obj is None: | ||||
return self | ||||
|
r40454 | |||
assert self.sname not in obj.__dict__ | ||||
|
r16115 | |||
|
r14928 | entry = obj._filecache.get(self.name) | ||
if entry: | ||||
if entry.changed(): | ||||
entry.obj = self.func(obj) | ||||
else: | ||||
|
r20045 | paths = [self.join(obj, path) for path in self.paths] | ||
|
r14928 | |||
# We stat -before- creating the object so our cache doesn't lie if | ||||
# a writer modified between the time we read and stat | ||||
|
r20045 | entry = filecacheentry(paths, True) | ||
|
r14928 | entry.obj = self.func(obj) | ||
obj._filecache[self.name] = entry | ||||
|
r37886 | obj.__dict__[self.sname] = entry.obj | ||
|
r14928 | return entry.obj | ||
|
r16115 | |||
|
r40454 | # don't implement __set__(), which would make __dict__ lookup as slow as | ||
# function call. | ||||
def set(self, obj, value): | ||||
|
r18316 | if self.name not in obj._filecache: | ||
# we add an entry for the missing value because X in __dict__ | ||||
# implies X in _filecache | ||||
|
r20045 | paths = [self.join(obj, path) for path in self.paths] | ||
ce = filecacheentry(paths, False) | ||||
|
r18316 | obj._filecache[self.name] = ce | ||
else: | ||||
ce = obj._filecache[self.name] | ||||
|
r43346 | ce.obj = value # update cached copy | ||
obj.__dict__[self.sname] = value # update copy returned by obj.x | ||||
|
r16115 | |||
|
r34457 | def extdatasource(repo, source): | ||
"""Gather a map of rev -> value dict from the specified source | ||||
A source spec is treated as a URL, with a special case shell: type | ||||
for parsing the output from a shell command. | ||||
The data is parsed as a series of newline-separated records where | ||||
each record is a revision specifier optionally followed by a space | ||||
and a freeform string value. If the revision is known locally, it | ||||
is converted to a rev, otherwise the record is skipped. | ||||
Note that both key and value are treated as UTF-8 and converted to | ||||
the local encoding. This allows uniformity between local and | ||||
remote data sources. | ||||
""" | ||||
|
r43347 | spec = repo.ui.config(b"extdata", source) | ||
|
r34457 | if not spec: | ||
|
r43347 | raise error.Abort(_(b"unknown extdata source '%s'") % source) | ||
|
r34457 | |||
data = {} | ||||
|
r34462 | src = proc = None | ||
|
r34457 | try: | ||
|
r43347 | if spec.startswith(b"shell:"): | ||
|
r34462 | # external commands should be run relative to the repo root | ||
cmd = spec[6:] | ||||
|
r43346 | proc = subprocess.Popen( | ||
procutil.tonativestr(cmd), | ||||
shell=True, | ||||
bufsize=-1, | ||||
close_fds=procutil.closefds, | ||||
stdout=subprocess.PIPE, | ||||
cwd=procutil.tonativestr(repo.root), | ||||
) | ||||
|
r34462 | src = proc.stdout | ||
else: | ||||
# treat as a URL or file | ||||
src = url.open(repo.ui, spec) | ||||
|
r34461 | for l in src: | ||
|
r43347 | if b" " in l: | ||
k, v = l.strip().split(b" ", 1) | ||||
|
r34457 | else: | ||
|
r43347 | k, v = l.strip(), b"" | ||
|
r34457 | |||
k = encoding.tolocal(k) | ||||
|
r34460 | try: | ||
|
r37378 | data[revsingle(repo, k).rev()] = encoding.tolocal(v) | ||
|
r46731 | except (error.LookupError, error.RepoLookupError, error.InputError): | ||
|
r43346 | pass # we ignore data for nodes that don't exist locally | ||
|
r34457 | finally: | ||
|
r34462 | if proc: | ||
|
r42776 | try: | ||
proc.communicate() | ||||
except ValueError: | ||||
# This happens if we started iterating src and then | ||||
# get a parse error on a line. It should be safe to ignore. | ||||
pass | ||||
|
r34462 | if src: | ||
src.close() | ||||
|
r35413 | if proc and proc.returncode != 0: | ||
|
r43346 | raise error.Abort( | ||
|
r43347 | _(b"extdata command '%s' failed: %s") | ||
|
r43346 | % (cmd, procutil.explainexit(proc.returncode)) | ||
) | ||||
|
r34457 | |||
return data | ||||
|
r43346 | |||
|
r38364 | class progress(object): | ||
|
r43347 | def __init__(self, ui, updatebar, topic, unit=b"", total=None): | ||
|
r38364 | self.ui = ui | ||
self.pos = 0 | ||||
self.topic = topic | ||||
self.unit = unit | ||||
self.total = total | ||||
|
r43347 | self.debug = ui.configbool(b'progress', b'debug') | ||
|
r41181 | self._updatebar = updatebar | ||
|
r38364 | |||
|
r38393 | def __enter__(self): | ||
|
r38522 | return self | ||
|
r38393 | |||
def __exit__(self, exc_type, exc_value, exc_tb): | ||||
self.complete() | ||||
|
r43347 | def update(self, pos, item=b"", total=None): | ||
|
r38438 | assert pos is not None | ||
|
r38364 | if total: | ||
self.total = total | ||||
self.pos = pos | ||||
|
r41245 | self._updatebar(self.topic, self.pos, item, self.unit, self.total) | ||
|
r41180 | if self.debug: | ||
self._printdebug(item) | ||||
|
r38364 | |||
|
r43347 | def increment(self, step=1, item=b"", total=None): | ||
|
r38364 | self.update(self.pos + step, item, total) | ||
|
r38392 | def complete(self): | ||
|
r41178 | self.pos = None | ||
|
r43347 | self.unit = b"" | ||
|
r41178 | self.total = None | ||
|
r43347 | self._updatebar(self.topic, self.pos, b"", self.unit, self.total) | ||
|
r38392 | |||
|
r41180 | def _printdebug(self, item): | ||
|
r44520 | unit = b'' | ||
|
r41178 | if self.unit: | ||
|
r43347 | unit = b' ' + self.unit | ||
|
r41178 | if item: | ||
|
r43347 | item = b' ' + item | ||
|
r41178 | |||
if self.total: | ||||
pct = 100.0 * self.pos / self.total | ||||
|
r43346 | self.ui.debug( | ||
|
r43347 | b'%s:%s %d/%d%s (%4.2f%%)\n' | ||
|
r43346 | % (self.topic, item, self.pos, self.total, unit, pct) | ||
) | ||||
|
r41178 | else: | ||
|
r43347 | self.ui.debug(b'%s:%s %d%s\n' % (self.topic, item, self.pos, unit)) | ||
|
r38364 | |||
|
r43346 | |||
|
r26906 | def gdinitconfig(ui): | ||
|
r46554 | """helper function to know if a repo should be created as general delta""" | ||
|
r26907 | # experimental config: format.generaldelta | ||
|
r43347 | return ui.configbool(b'format', b'generaldelta') or ui.configbool( | ||
b'format', b'usegeneraldelta' | ||||
|
r43346 | ) | ||
|
r26906 | |||
|
r26907 | def gddeltaconfig(ui): | ||
|
r46554 | """helper function to know if incoming delta should be optimised""" | ||
|
r26906 | # experimental config: format.generaldelta | ||
|
r43347 | return ui.configbool(b'format', b'generaldelta') | ||
|
r31553 | |||
|
r43346 | |||
|
r31553 | class simplekeyvaluefile(object): | ||
"""A simple file with key=value lines | ||||
Keys must be alphanumerics and start with a letter, values must not | ||||
contain '\n' characters""" | ||||
|
r43346 | |||
|
r43347 | firstlinekey = b'__firstline' | ||
|
r31553 | |||
def __init__(self, vfs, path, keys=None): | ||||
self.vfs = vfs | ||||
self.path = path | ||||
|
r32270 | def read(self, firstlinenonkeyval=False): | ||
"""Read the contents of a simple key-value file | ||||
'firstlinenonkeyval' indicates whether the first line of file should | ||||
be treated as a key-value pair or reuturned fully under the | ||||
__firstline key.""" | ||||
|
r31553 | lines = self.vfs.readlines(self.path) | ||
|
r32270 | d = {} | ||
if firstlinenonkeyval: | ||||
if not lines: | ||||
|
r43347 | e = _(b"empty simplekeyvalue file") | ||
|
r32270 | raise error.CorruptedState(e) | ||
# we don't want to include '\n' in the __firstline | ||||
d[self.firstlinekey] = lines[0][:-1] | ||||
del lines[0] | ||||
|
r31553 | try: | ||
|
r32269 | # the 'if line.strip()' part prevents us from failing on empty | ||
# lines which only contain '\n' therefore are not skipped | ||||
# by 'if line' | ||||
|
r43346 | updatedict = dict( | ||
|
r43347 | line[:-1].split(b'=', 1) for line in lines if line.strip() | ||
|
r43346 | ) | ||
|
r32270 | if self.firstlinekey in updatedict: | ||
|
r43347 | e = _(b"%r can't be used as a key") | ||
|
r32270 | raise error.CorruptedState(e % self.firstlinekey) | ||
d.update(updatedict) | ||||
|
r31553 | except ValueError as e: | ||
|
r43682 | raise error.CorruptedState(stringutil.forcebytestr(e)) | ||
|
r31553 | return d | ||
|
r32270 | def write(self, data, firstline=None): | ||
|
r31553 | """Write key=>value mapping to a file | ||
data is a dict. Keys must be alphanumerical and start with a letter. | ||||
|
r32270 | Values must not contain newline characters. | ||
If 'firstline' is not None, it is written to file before | ||||
everything else, as it is, not in a key=value form""" | ||||
|
r31553 | lines = [] | ||
|
r32270 | if firstline is not None: | ||
|
r43347 | lines.append(b'%s\n' % firstline) | ||
|
r32270 | |||
|
r31553 | for k, v in data.items(): | ||
|
r32270 | if k == self.firstlinekey: | ||
|
r43347 | e = b"key name '%s' is reserved" % self.firstlinekey | ||
|
r32270 | raise error.ProgrammingError(e) | ||
|
r35931 | if not k[0:1].isalpha(): | ||
|
r43347 | e = b"keys must start with a letter in a key-value file" | ||
|
r31553 | raise error.ProgrammingError(e) | ||
if not k.isalnum(): | ||||
|
r43347 | e = b"invalid key name in a simple key-value file" | ||
|
r31553 | raise error.ProgrammingError(e) | ||
|
r43347 | if b'\n' in v: | ||
e = b"invalid value in a simple key-value file" | ||||
|
r31553 | raise error.ProgrammingError(e) | ||
|
r43347 | lines.append(b"%s=%s\n" % (k, v)) | ||
with self.vfs(self.path, mode=b'wb', atomictemp=True) as fp: | ||||
fp.write(b''.join(lines)) | ||||
r33249 | ||||
|
r43346 | |||
|
r33541 | _reportobsoletedsource = [ | ||
|
r43347 | b'debugobsolete', | ||
b'pull', | ||||
b'push', | ||||
b'serve', | ||||
b'unbundle', | ||||
|
r33541 | ] | ||
|
r34662 | _reportnewcssource = [ | ||
|
r43347 | b'pull', | ||
b'unbundle', | ||||
|
r34662 | ] | ||
|
r43346 | |||
|
r45632 | def prefetchfiles(repo, revmatches): | ||
|
r37780 | """Invokes the registered file prefetch functions, allowing extensions to | ||
ensure the corresponding files are available locally, before the command | ||||
|
r45632 | uses them. | ||
Args: | ||||
revmatches: a list of (revision, match) tuples to indicate the files to | ||||
fetch at each revision. If any of the match elements is None, it matches | ||||
all files. | ||||
""" | ||||
|
r37780 | |||
|
r45632 | def _matcher(m): | ||
if m: | ||||
assert isinstance(m, matchmod.basematcher) | ||||
# The command itself will complain about files that don't exist, so | ||||
# don't duplicate the message. | ||||
return matchmod.badmatch(m, lambda fn, msg: None) | ||||
else: | ||||
return matchall(repo) | ||||
revbadmatches = [(rev, _matcher(match)) for (rev, match) in revmatches] | ||||
fileprefetchhooks(repo, revbadmatches) | ||||
|
r37780 | |||
|
r43346 | |||
|
r37780 | # a list of (repo, revs, match) prefetch functions | ||
|
r36154 | fileprefetchhooks = util.hooks() | ||
|
r35727 | # A marker that tells the evolve extension to suppress its own reporting | ||
_reportstroubledchangesets = True | ||||
|
r43346 | |||
|
r45032 | def registersummarycallback(repo, otr, txnname=b'', as_validator=False): | ||
r33249 | """register a callback to issue a summary after the transaction is closed | |||
|
r45032 | |||
If as_validator is true, then the callbacks are registered as transaction | ||||
validators instead | ||||
r33249 | """ | |||
|
r43346 | |||
|
r34620 | def txmatch(sources): | ||
return any(txnname.startswith(source) for source in sources) | ||||
|
r34621 | categories = [] | ||
def reportsummary(func): | ||||
"""decorator for report callbacks.""" | ||||
|
r35140 | # The repoview life cycle is shorter than the one of the actual | ||
# underlying repository. So the filtered object can die before the | ||||
# weakref is used leading to troubles. We keep a reference to the | ||||
# unfiltered object and restore the filtering when retrieving the | ||||
# repository through the weakref. | ||||
filtername = repo.filtername | ||||
reporef = weakref.ref(repo.unfiltered()) | ||||
|
r43346 | |||
|
r34621 | def wrapped(tr): | ||
|
r34620 | repo = reporef() | ||
|
r35140 | if filtername: | ||
|
r44120 | assert repo is not None # help pytype | ||
|
r35140 | repo = repo.filtered(filtername) | ||
|
r34621 | func(repo, tr) | ||
|
r43346 | |||
|
r43347 | newcat = b'%02i-txnreport' % len(categories) | ||
|
r45032 | if as_validator: | ||
otr.addvalidator(newcat, wrapped) | ||||
else: | ||||
otr.addpostclose(newcat, wrapped) | ||||
|
r34621 | categories.append(newcat) | ||
return wrapped | ||||
r43167 | @reportsummary | |||
def reportchangegroup(repo, tr): | ||||
|
r43347 | cgchangesets = tr.changes.get(b'changegroup-count-changesets', 0) | ||
cgrevisions = tr.changes.get(b'changegroup-count-revisions', 0) | ||||
cgfiles = tr.changes.get(b'changegroup-count-files', 0) | ||||
cgheads = tr.changes.get(b'changegroup-count-heads', 0) | ||||
r43167 | if cgchangesets or cgrevisions or cgfiles: | |||
|
r43347 | htext = b"" | ||
r43167 | if cgheads: | |||
|
r43347 | htext = _(b" (%+d heads)") % cgheads | ||
msg = _(b"added %d changesets with %d changes to %d files%s\n") | ||||
|
r45032 | if as_validator: | ||
msg = _(b"adding %d changesets with %d changes to %d files%s\n") | ||||
|
r44120 | assert repo is not None # help pytype | ||
r43167 | repo.ui.status(msg % (cgchangesets, cgrevisions, cgfiles, htext)) | |||
|
r34621 | if txmatch(_reportobsoletedsource): | ||
|
r43346 | |||
|
r34621 | @reportsummary | ||
def reportobsoleted(repo, tr): | ||||
|
r34620 | obsoleted = obsutil.getobsoleted(repo, tr) | ||
|
r43347 | newmarkers = len(tr.changes.get(b'obsmarkers', ())) | ||
r43164 | if newmarkers: | |||
|
r43347 | repo.ui.status(_(b'%i new obsolescence markers\n') % newmarkers) | ||
|
r34620 | if obsoleted: | ||
|
r45032 | msg = _(b'obsoleted %i changesets\n') | ||
if as_validator: | ||||
msg = _(b'obsoleting %i changesets\n') | ||||
repo.ui.status(msg % len(obsoleted)) | ||||
|
r34662 | |||
|
r43346 | if obsolete.isenabled( | ||
repo, obsolete.createmarkersopt | ||||
|
r43347 | ) and repo.ui.configbool( | ||
b'experimental', b'evolution.report-instabilities' | ||||
): | ||||
|
r35727 | instabilitytypes = [ | ||
|
r43347 | (b'orphan', b'orphan'), | ||
(b'phase-divergent', b'phasedivergent'), | ||||
(b'content-divergent', b'contentdivergent'), | ||||
|
r35727 | ] | ||
def getinstabilitycounts(repo): | ||||
filtered = repo.changelog.filteredrevs | ||||
counts = {} | ||||
for instability, revset in instabilitytypes: | ||||
|
r43346 | counts[instability] = len( | ||
set(obsolete.getrevs(repo, revset)) - filtered | ||||
) | ||||
|
r35727 | return counts | ||
oldinstabilitycounts = getinstabilitycounts(repo) | ||||
|
r43346 | |||
|
r35727 | @reportsummary | ||
def reportnewinstabilities(repo, tr): | ||||
newinstabilitycounts = getinstabilitycounts(repo) | ||||
for instability, revset in instabilitytypes: | ||||
|
r43346 | delta = ( | ||
newinstabilitycounts[instability] | ||||
- oldinstabilitycounts[instability] | ||||
) | ||||
|
r38474 | msg = getinstabilitymessage(delta, instability) | ||
if msg: | ||||
repo.ui.warn(msg) | ||||
|
r35727 | |||
|
r34662 | if txmatch(_reportnewcssource): | ||
|
r43346 | |||
|
r34662 | @reportsummary | ||
def reportnewcs(repo, tr): | ||||
"""Report the range of new revisions pulled/unbundled.""" | ||||
|
r43347 | origrepolen = tr.changes.get(b'origrepolen', len(repo)) | ||
|
r39934 | unfi = repo.unfiltered() | ||
if origrepolen >= len(unfi): | ||||
|
r34662 | return | ||
|
r39933 | # Compute the bounds of new visible revisions' range. | ||
revs = smartset.spanset(repo, start=origrepolen) | ||||
|
r39934 | if revs: | ||
minrev, maxrev = repo[revs.min()], repo[revs.max()] | ||||
|
r34662 | |||
|
r39934 | if minrev == maxrev: | ||
revrange = minrev | ||||
else: | ||||
|
r43347 | revrange = b'%s:%s' % (minrev, maxrev) | ||
draft = len(repo.revs(b'%ld and draft()', revs)) | ||||
secret = len(repo.revs(b'%ld and secret()', revs)) | ||||
|
r39934 | if not (draft or secret): | ||
|
r43347 | msg = _(b'new changesets %s\n') % revrange | ||
|
r39934 | elif draft and secret: | ||
|
r43347 | msg = _(b'new changesets %s (%d drafts, %d secrets)\n') | ||
|
r39934 | msg %= (revrange, draft, secret) | ||
elif draft: | ||||
|
r43347 | msg = _(b'new changesets %s (%d drafts)\n') | ||
|
r39934 | msg %= (revrange, draft) | ||
elif secret: | ||||
|
r43347 | msg = _(b'new changesets %s (%d secrets)\n') | ||
|
r39934 | msg %= (revrange, secret) | ||
else: | ||||
|
r43347 | errormsg = b'entered unreachable condition' | ||
|
r39934 | raise error.ProgrammingError(errormsg) | ||
repo.ui.status(msg) | ||||
|
r35169 | |||
|
r39935 | # search new changesets directly pulled as obsolete | ||
|
r43347 | duplicates = tr.changes.get(b'revduplicates', ()) | ||
|
r43346 | obsadded = unfi.revs( | ||
|
r43347 | b'(%d: + %ld) and obsolete()', origrepolen, duplicates | ||
|
r43346 | ) | ||
|
r39935 | cl = repo.changelog | ||
extinctadded = [r for r in obsadded if r not in cl] | ||||
if extinctadded: | ||||
# They are not just obsolete, but obsolete and invisible | ||||
# we call them "extinct" internally but the terms have not been | ||||
# exposed to users. | ||||
|
r43347 | msg = b'(%d other changesets obsolete on arrival)\n' | ||
|
r39935 | repo.ui.status(msg % len(extinctadded)) | ||
|
r38189 | @reportsummary | ||
def reportphasechanges(repo, tr): | ||||
"""Report statistics of phase changes for changesets pre-existing | ||||
pull/unbundle. | ||||
""" | ||||
|
r43347 | origrepolen = tr.changes.get(b'origrepolen', len(repo)) | ||
|
r45036 | published = [] | ||
for revs, (old, new) in tr.changes.get(b'phases', []): | ||||
if new != phases.public: | ||||
continue | ||||
published.extend(rev for rev in revs if rev < origrepolen) | ||||
|
r38189 | if not published: | ||
return | ||||
|
r45032 | msg = _(b'%d local changesets published\n') | ||
if as_validator: | ||||
msg = _(b'%d local changesets will be published\n') | ||||
repo.ui.status(msg % len(published)) | ||||
|
r43346 | |||
|
r38189 | |||
|
r38474 | def getinstabilitymessage(delta, instability): | ||
"""function to return the message to show warning about new instabilities | ||||
exists as a separate function so that extension can wrap to show more | ||||
information like how to fix instabilities""" | ||||
if delta > 0: | ||||
|
r43347 | return _(b'%i new %s changesets\n') % (delta, instability) | ||
|
r38474 | |||
|
r43346 | |||
|
r35185 | def nodesummaries(repo, nodes, maxnumnodes=4): | ||
if len(nodes) <= maxnumnodes or repo.ui.verbose: | ||||
|
r43347 | return b' '.join(short(h) for h in nodes) | ||
first = b' '.join(short(h) for h in nodes[:maxnumnodes]) | ||||
return _(b"%s and %d others") % (first, len(nodes) - maxnumnodes) | ||||
|
r35185 | |||
|
r43346 | |||
|
r46712 | def enforcesinglehead(repo, tr, desc, accountclosed, filtername): | ||
|
r35186 | """check that no named branch has multiple heads""" | ||
|
r43347 | if desc in (b'strip', b'repair'): | ||
|
r35186 | # skip the logic during strip | ||
return | ||||
|
r46712 | visible = repo.filtered(filtername) | ||
|
r35186 | # possible improvement: we could restrict the check to affected branch | ||
r43239 | bm = visible.branchmap() | |||
for name in bm: | ||||
heads = bm.branchheads(name, closed=accountclosed) | ||||
|
r35186 | if len(heads) > 1: | ||
|
r43347 | msg = _(b'rejecting multiple heads on branch "%s"') | ||
|
r35186 | msg %= name | ||
|
r43347 | hint = _(b'%d heads: %s') | ||
|
r35186 | hint %= (len(heads), nodesummaries(repo, heads)) | ||
raise error.Abort(msg, hint=hint) | ||||
|
r43346 | |||
|
r35169 | def wrapconvertsink(sink): | ||
"""Allow extensions to wrap the sink returned by convcmd.convertsink() | ||||
before it is used, whether or not the convert extension was formally loaded. | ||||
""" | ||||
return sink | ||||
|
r35512 | |||
|
r43346 | |||
|
r35512 | def unhidehashlikerevs(repo, specs, hiddentype): | ||
"""parse the user specs and unhide changesets whose hash or revision number | ||||
is passed. | ||||
hiddentype can be: 1) 'warn': warn while unhiding changesets | ||||
2) 'nowarn': don't warn while unhiding changesets | ||||
returns a repo object with the required changesets unhidden | ||||
""" | ||||
|
r43346 | if not repo.filtername or not repo.ui.configbool( | ||
|
r43347 | b'experimental', b'directaccess' | ||
|
r43346 | ): | ||
|
r35512 | return repo | ||
|
r43347 | if repo.filtername not in (b'visible', b'visible-hidden'): | ||
|
r35512 | return repo | ||
symbols = set() | ||||
for spec in specs: | ||||
try: | ||||
tree = revsetlang.parse(spec) | ||||
|
r43346 | except error.ParseError: # will be reported by scmutil.revrange() | ||
|
r35512 | continue | ||
symbols.update(revsetlang.gethashlikesymbols(tree)) | ||||
if not symbols: | ||||
return repo | ||||
revs = _getrevsfromsymbols(repo, symbols) | ||||
if not revs: | ||||
return repo | ||||
|
r43347 | if hiddentype == b'warn': | ||
|
r35512 | unfi = repo.unfiltered() | ||
|
r43347 | revstr = b", ".join([pycompat.bytestr(unfi[l]) for l in revs]) | ||
|
r43346 | repo.ui.warn( | ||
_( | ||||
|
r43347 | b"warning: accessing hidden changesets for write " | ||
b"operation: %s\n" | ||||
|
r43346 | ) | ||
% revstr | ||||
) | ||||
|
r35512 | |||
|
r35515 | # we have to use new filtername to separate branch/tags cache until we can | ||
# disbale these cache when revisions are dynamically pinned. | ||||
|
r43347 | return repo.filtered(b'visible-hidden', revs) | ||
|
r35512 | |||
|
r43346 | |||
|
r35512 | def _getrevsfromsymbols(repo, symbols): | ||
"""parse the list of symbols and returns a set of revision numbers of hidden | ||||
changesets present in symbols""" | ||||
revs = set() | ||||
unfi = repo.unfiltered() | ||||
unficl = unfi.changelog | ||||
cl = repo.changelog | ||||
tiprev = len(unficl) | ||||
|
r43347 | allowrevnums = repo.ui.configbool(b'experimental', b'directaccess.revnums') | ||
|
r35512 | for s in symbols: | ||
try: | ||||
n = int(s) | ||||
if n <= tiprev: | ||||
if not allowrevnums: | ||||
continue | ||||
else: | ||||
if n not in cl: | ||||
revs.add(n) | ||||
continue | ||||
except ValueError: | ||||
pass | ||||
try: | ||||
|
r37885 | s = resolvehexnodeidprefix(unfi, s) | ||
|
r37112 | except (error.LookupError, error.WdirUnsupported): | ||
|
r35512 | s = None | ||
if s is not None: | ||||
rev = unficl.rev(s) | ||||
if rev not in cl: | ||||
revs.add(rev) | ||||
return revs | ||||
|
r38146 | |||
|
r43346 | |||
|
r38146 | def bookmarkrevs(repo, mark): | ||
|
r46654 | """Select revisions reachable by a given bookmark | ||
If the bookmarked revision isn't a head, an empty set will be returned. | ||||
|
r38146 | """ | ||
|
r46655 | return repo.revs(format_bookmark_revspec(mark)) | ||
def format_bookmark_revspec(mark): | ||||
"""Build a revset expression to select revisions reachable by a given | ||||
bookmark""" | ||||
|
r46657 | mark = b'literal:' + mark | ||
|
r46655 | return revsetlang.formatspec( | ||
|
r43347 | b"ancestors(bookmark(%s)) - " | ||
b"ancestors(head() and not bookmark(%s)) - " | ||||
b"ancestors(bookmark() and not bookmark(%s))", | ||||
|
r43346 | mark, | ||
mark, | ||||
mark, | ||||
) | ||||