# HG changeset patch # User Raphaël Gomès # Date 1666618334 -7200 # Node ID a3356ab610fc50000cf0ba55c424a4d96da11db7 # Parent 192949b6815923cc3438999dc27f4bca5ddaafb9# Parent 52dd7a43ad5c008121910b71ce6fe838a22029b7 branching: merge default into stable This marks the feature freeze for the 6.3 release diff -r 192949b68159 -r a3356ab610fc Makefile --- a/Makefile Thu Oct 20 12:05:17 2022 -0400 +++ b/Makefile Mon Oct 24 15:32:14 2022 +0200 @@ -203,9 +203,11 @@ packaging_targets := \ rhel7 \ rhel8 \ + rhel9 \ deb \ docker-rhel7 \ docker-rhel8 \ + docker-rhel9 \ docker-debian-bullseye \ docker-debian-buster \ docker-debian-stretch \ diff -r 192949b68159 -r a3356ab610fc contrib/heptapod-ci.yml --- a/contrib/heptapod-ci.yml Thu Oct 20 12:05:17 2022 -0400 +++ b/contrib/heptapod-ci.yml Mon Oct 24 15:32:14 2022 +0200 @@ -89,7 +89,7 @@ - hg -R /tmp/mercurial-ci/ update `hg log --rev '.' --template '{node}'` - cd /tmp/mercurial-ci/ - make local PYTHON=$PYTHON - - $PYTHON -m pip install --user -U pytype==2021.04.15 + - $PYTHON -m pip install --user -U libcst==0.3.20 pytype==2022.03.29 script: - echo "Entering script section" - sh contrib/check-pytype.sh diff -r 192949b68159 -r a3356ab610fc contrib/packaging/Makefile --- a/contrib/packaging/Makefile Thu Oct 20 12:05:17 2022 -0400 +++ b/contrib/packaging/Makefile Mon Oct 24 15:32:14 2022 +0200 @@ -15,7 +15,8 @@ RHEL_RELEASES := \ 7 \ - 8 + 8 \ + 9 # Build a Python for these RHEL (and derivatives) releases. RHEL_WITH_PYTHON_RELEASES := diff -r 192949b68159 -r a3356ab610fc contrib/packaging/docker/rhel9 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/contrib/packaging/docker/rhel9 Mon Oct 24 15:32:14 2022 +0200 @@ -0,0 +1,25 @@ +FROM rockylinux/rockylinux:9 + +RUN groupadd -g %GID% build && \ + useradd -u %UID% -g %GID% -s /bin/bash -d /build -m build + +RUN dnf install 'dnf-command(config-manager)' -y +# crb repository is necessary for docutils +RUN dnf config-manager --set-enabled crb + +RUN yum install -y \ + gcc \ + gettext \ + make \ + python3-devel \ + python3-docutils \ + rpm-build + +# For creating repo meta data +RUN yum install -y createrepo + +# For rust extensions +RUN yum install -y cargo + +# avoid incorrect docker image permissions on /tmp preventing writes by non-root users +RUN chmod 1777 /tmp diff -r 192949b68159 -r a3356ab610fc contrib/packaging/packagelib.sh --- a/contrib/packaging/packagelib.sh Thu Oct 20 12:05:17 2022 -0400 +++ b/contrib/packaging/packagelib.sh Mon Oct 24 15:32:14 2022 +0200 @@ -9,7 +9,7 @@ # node: the node|short hg was built from, or empty if built from a tag gethgversion() { if [ -z "${1+x}" ]; then - python="python" + python="python3" else python="$1" fi diff -r 192949b68159 -r a3356ab610fc contrib/perf-utils/compare-discovery-case --- a/contrib/perf-utils/compare-discovery-case Thu Oct 20 12:05:17 2022 -0400 +++ b/contrib/perf-utils/compare-discovery-case Mon Oct 24 15:32:14 2022 +0200 @@ -97,6 +97,16 @@ assert set(VARIANTS.keys()) == set(VARIANTS_KEYS) +def parse_case(case): + case_type, case_args = case.split('-', 1) + if case_type == 'file': + case_args = (case_args,) + else: + case_args = tuple(int(x) for x in case_args.split('-')) + case = (case_type,) + case_args + return case + + def format_case(case): return '-'.join(str(s) for s in case) @@ -109,12 +119,41 @@ return '::randomantichain(all(), "%d")' % case[1] elif t == 'rev': return '::%d' % case[1] + elif t == 'file': + return '::nodefromfile("%s")' % case[1] else: assert False -def compare(repo, local_case, remote_case): +def compare( + repo, + local_case, + remote_case, + display_header=True, + display_case=True, +): case = (repo, local_case, remote_case) + if display_header: + pieces = ['#'] + if display_case: + pieces += [ + "repo", + "local-subset", + "remote-subset", + ] + + pieces += [ + "discovery-variant", + "roundtrips", + "queries", + "revs", + "local-heads", + "common-heads", + "undecided-initial", + "undecided-common", + "undecided-missing", + ] + print(*pieces) for variant in VARIANTS_KEYS: res = process(case, VARIANTS[variant]) revs = res["nb-revs"] @@ -122,36 +161,31 @@ common_heads = res["nb-common-heads"] roundtrips = res["total-roundtrips"] queries = res["total-queries"] - if 'tree-discovery' in variant: - print( + pieces = [] + if display_case: + pieces += [ repo, format_case(local_case), format_case(remote_case), - variant, - roundtrips, - queries, - revs, - local_heads, - common_heads, - ) - else: + ] + pieces += [ + variant, + roundtrips, + queries, + revs, + local_heads, + common_heads, + ] + if 'tree-discovery' not in variant: undecided_common = res["nb-ini_und-common"] undecided_missing = res["nb-ini_und-missing"] undecided = undecided_common + undecided_missing - print( - repo, - format_case(local_case), - format_case(remote_case), - variant, - roundtrips, - queries, - revs, - local_heads, - common_heads, + pieces += [ undecided, undecided_common, undecided_missing, - ) + ] + print(*pieces) return 0 @@ -171,13 +205,23 @@ if __name__ == '__main__': - if len(sys.argv) != 4: + + argv = sys.argv[:] + + kwargs = {} + # primitive arg parsing + if '--no-header' in argv: + kwargs['display_header'] = False + argv = [a for a in argv if a != '--no-header'] + if '--no-case' in argv: + kwargs['display_case'] = False + argv = [a for a in argv if a != '--no-case'] + + if len(argv) != 4: usage = f'USAGE: {script_name} REPO LOCAL_CASE REMOTE_CASE' print(usage, file=sys.stderr) sys.exit(128) - repo = sys.argv[1] - local_case = sys.argv[2].split('-') - local_case = (local_case[0],) + tuple(int(x) for x in local_case[1:]) - remote_case = sys.argv[3].split('-') - remote_case = (remote_case[0],) + tuple(int(x) for x in remote_case[1:]) - sys.exit(compare(repo, local_case, remote_case)) + repo = argv[1] + local_case = parse_case(argv[2]) + remote_case = parse_case(argv[3]) + sys.exit(compare(repo, local_case, remote_case, **kwargs)) diff -r 192949b68159 -r a3356ab610fc contrib/perf.py --- a/contrib/perf.py Thu Oct 20 12:05:17 2022 -0400 +++ b/contrib/perf.py Mon Oct 24 15:32:14 2022 +0200 @@ -925,6 +925,71 @@ fm.end() +@command( + b'perf::delta-find', + revlogopts + formatteropts, + b'-c|-m|FILE REV', +) +def perf_delta_find(ui, repo, arg_1, arg_2=None, **opts): + """benchmark the process of finding a valid delta for a revlog revision + + When a revlog receives a new revision (e.g. from a commit, or from an + incoming bundle), it searches for a suitable delta-base to produce a delta. + This perf command measures how much time we spend in this process. It + operates on an already stored revision. + + See `hg help debug-delta-find` for another related command. + """ + from mercurial import revlogutils + import mercurial.revlogutils.deltas as deltautil + + opts = _byteskwargs(opts) + if arg_2 is None: + file_ = None + rev = arg_1 + else: + file_ = arg_1 + rev = arg_2 + + repo = repo.unfiltered() + + timer, fm = gettimer(ui, opts) + + rev = int(rev) + + revlog = cmdutil.openrevlog(repo, b'perf::delta-find', file_, opts) + + deltacomputer = deltautil.deltacomputer(revlog) + + node = revlog.node(rev) + p1r, p2r = revlog.parentrevs(rev) + p1 = revlog.node(p1r) + p2 = revlog.node(p2r) + full_text = revlog.revision(rev) + textlen = len(full_text) + cachedelta = None + flags = revlog.flags(rev) + + revinfo = revlogutils.revisioninfo( + node, + p1, + p2, + [full_text], # btext + textlen, + cachedelta, + flags, + ) + + # Note: we should probably purge the potential caches (like the full + # manifest cache) between runs. + def find_one(): + with revlog._datafp() as fh: + deltacomputer.finddeltainfo(revinfo, fh, target_rev=rev) + + timer(find_one) + fm.end() + + @command(b'perf::discovery|perfdiscovery', formatteropts, b'PATH') def perfdiscovery(ui, repo, path, **opts): """benchmark discovery between local repo and the peer at given path""" @@ -974,6 +1039,111 @@ fm.end() +@command( + b'perf::bundle', + [ + ( + b'r', + b'rev', + [], + b'changesets to bundle', + b'REV', + ), + ( + b't', + b'type', + b'none', + b'bundlespec to use (see `hg help bundlespec`)', + b'TYPE', + ), + ] + + formatteropts, + b'REVS', +) +def perfbundle(ui, repo, *revs, **opts): + """benchmark the creation of a bundle from a repository + + For now, this only supports "none" compression. + """ + try: + from mercurial import bundlecaches + + parsebundlespec = bundlecaches.parsebundlespec + except ImportError: + from mercurial import exchange + + parsebundlespec = exchange.parsebundlespec + + from mercurial import discovery + from mercurial import bundle2 + + opts = _byteskwargs(opts) + timer, fm = gettimer(ui, opts) + + cl = repo.changelog + revs = list(revs) + revs.extend(opts.get(b'rev', ())) + revs = scmutil.revrange(repo, revs) + if not revs: + raise error.Abort(b"not revision specified") + # make it a consistent set (ie: without topological gaps) + old_len = len(revs) + revs = list(repo.revs(b"%ld::%ld", revs, revs)) + if old_len != len(revs): + new_count = len(revs) - old_len + msg = b"add %d new revisions to make it a consistent set\n" + ui.write_err(msg % new_count) + + targets = [cl.node(r) for r in repo.revs(b"heads(::%ld)", revs)] + bases = [cl.node(r) for r in repo.revs(b"heads(::%ld - %ld)", revs, revs)] + outgoing = discovery.outgoing(repo, bases, targets) + + bundle_spec = opts.get(b'type') + + bundle_spec = parsebundlespec(repo, bundle_spec, strict=False) + + cgversion = bundle_spec.params.get(b"cg.version") + if cgversion is None: + if bundle_spec.version == b'v1': + cgversion = b'01' + if bundle_spec.version == b'v2': + cgversion = b'02' + if cgversion not in changegroup.supportedoutgoingversions(repo): + err = b"repository does not support bundle version %s" + raise error.Abort(err % cgversion) + + if cgversion == b'01': # bundle1 + bversion = b'HG10' + bundle_spec.wirecompression + bcompression = None + elif cgversion in (b'02', b'03'): + bversion = b'HG20' + bcompression = bundle_spec.wirecompression + else: + err = b'perf::bundle: unexpected changegroup version %s' + raise error.ProgrammingError(err % cgversion) + + if bcompression is None: + bcompression = b'UN' + + if bcompression != b'UN': + err = b'perf::bundle: compression currently unsupported: %s' + raise error.ProgrammingError(err % bcompression) + + def do_bundle(): + bundle2.writenewbundle( + ui, + repo, + b'perf::bundle', + os.devnull, + bversion, + outgoing, + bundle_spec.params, + ) + + timer(do_bundle) + fm.end() + + @command(b'perf::bundleread|perfbundleread', formatteropts, b'BUNDLE') def perfbundleread(ui, repo, bundlepath, **opts): """Benchmark reading of bundle files. @@ -2498,6 +2668,60 @@ @command( + b'perf::unbundle', + formatteropts, + b'BUNDLE_FILE', +) +def perf_unbundle(ui, repo, fname, **opts): + """benchmark application of a bundle in a repository. + + This does not include the final transaction processing""" + from mercurial import exchange + from mercurial import bundle2 + + opts = _byteskwargs(opts) + + with repo.lock(): + bundle = [None, None] + orig_quiet = repo.ui.quiet + try: + repo.ui.quiet = True + with open(fname, mode="rb") as f: + + def noop_report(*args, **kwargs): + pass + + def setup(): + gen, tr = bundle + if tr is not None: + tr.abort() + bundle[:] = [None, None] + f.seek(0) + bundle[0] = exchange.readbundle(ui, f, fname) + bundle[1] = repo.transaction(b'perf::unbundle') + bundle[1]._report = noop_report # silence the transaction + + def apply(): + gen, tr = bundle + bundle2.applybundle( + repo, + gen, + tr, + source=b'perf::unbundle', + url=fname, + ) + + timer, fm = gettimer(ui, opts) + timer(apply, setup=setup) + fm.end() + finally: + repo.ui.quiet == orig_quiet + gen, tr = bundle + if tr is not None: + tr.abort() + + +@command( b'perf::unidiff|perfunidiff', revlogopts + formatteropts diff -r 192949b68159 -r a3356ab610fc contrib/pull_logger.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/contrib/pull_logger.py Mon Oct 24 15:32:14 2022 +0200 @@ -0,0 +1,141 @@ +# pull_logger.py - Logs pulls to a JSON-line file in the repo's VFS. +# +# Copyright 2022 Pacien TRAN-GIRARD +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + + +'''logs pull parameters to a file + +This extension logs the pull parameters, i.e. the remote and common heads, +when pulling from the local repository. + +The collected data should give an idea of the state of a pair of repositories +and allow replaying past synchronisations between them. This is particularly +useful for working on data exchange, bundling and caching-related +optimisations. + +The record is a JSON-line file located in the repository's VFS at +.hg/pull_log.jsonl. + +Log write failures are not considered fatal: log writes may be skipped for any +reason such as insufficient storage or a timeout. + +Some basic log file rotation can be enabled by setting 'rotate-size' to a value +greater than 0. This causes the current log file to be moved to +.hg/pull_log.jsonl.rotated when this threshold is met, discarding any +previously rotated log file. + +The timeouts of the exclusive lock used when writing to the lock file can be +configured through the 'timeout.lock' and 'timeout.warn' options of this +plugin. Those are not expected to be held for a significant time in practice.:: + + [pull-logger] + timeout.lock = 300 + timeout.warn = 100 + rotate-size = 1kb +''' + + +import json +import time + +from mercurial.i18n import _ +from mercurial.utils import stringutil +from mercurial import ( + error, + extensions, + lock, + registrar, + wireprotov1server, +) + +EXT_NAME = b'pull-logger' +EXT_VERSION_CODE = 0 + +LOG_FILE = b'pull_log.jsonl' +OLD_LOG_FILE = LOG_FILE + b'.rotated' +LOCK_NAME = LOG_FILE + b'.lock' + +configtable = {} +configitem = registrar.configitem(configtable) +configitem(EXT_NAME, b'timeout.lock', default=600) +configitem(EXT_NAME, b'timeout.warn', default=120) +configitem(EXT_NAME, b'rotate-size', default=b'100MB') + + +def wrap_getbundle(orig, repo, proto, others, *args, **kwargs): + heads, common = extract_pull_heads(others) + log_entry = { + 'timestamp': time.time(), + 'logger_version': EXT_VERSION_CODE, + 'heads': sorted(heads), + 'common': sorted(common), + } + + try: + write_to_log(repo, log_entry) + except (IOError, error.LockError) as err: + msg = stringutil.forcebytestr(err) + repo.ui.warn(_(b'unable to append to pull log: %s\n') % msg) + + return orig(repo, proto, others, *args, **kwargs) + + +def extract_pull_heads(bundle_args): + opts = wireprotov1server.options( + b'getbundle', + wireprotov1server.wireprototypes.GETBUNDLE_ARGUMENTS.keys(), + bundle_args.copy(), # this call consumes the args destructively + ) + + heads = opts.get(b'heads', b'').decode('utf-8').split(' ') + common = opts.get(b'common', b'').decode('utf-8').split(' ') + return (heads, common) + + +def write_to_log(repo, entry): + locktimeout = repo.ui.configint(EXT_NAME, b'timeout.lock') + lockwarntimeout = repo.ui.configint(EXT_NAME, b'timeout.warn') + rotatesize = repo.ui.configbytes(EXT_NAME, b'rotate-size') + + with lock.trylock( + ui=repo.ui, + vfs=repo.vfs, + lockname=LOCK_NAME, + timeout=locktimeout, + warntimeout=lockwarntimeout, + ): + if rotatesize > 0 and repo.vfs.exists(LOG_FILE): + if repo.vfs.stat(LOG_FILE).st_size >= rotatesize: + repo.vfs.rename(LOG_FILE, OLD_LOG_FILE) + + with repo.vfs.open(LOG_FILE, b'a+') as logfile: + serialised = json.dumps(entry, sort_keys=True) + logfile.write(serialised.encode('utf-8')) + logfile.write(b'\n') + logfile.flush() + + +def reposetup(ui, repo): + if repo.local(): + repo._wlockfreeprefix.add(LOG_FILE) + repo._wlockfreeprefix.add(OLD_LOG_FILE) + + +def uisetup(ui): + del wireprotov1server.commands[b'getbundle'] + decorator = wireprotov1server.wireprotocommand( + name=b'getbundle', + args=b'*', + permission=b'pull', + ) + + extensions.wrapfunction( + container=wireprotov1server, + funcname='getbundle', + wrapper=wrap_getbundle, + ) + + decorator(wireprotov1server.getbundle) diff -r 192949b68159 -r a3356ab610fc hgext/fsmonitor/pywatchman/pybser.py --- a/hgext/fsmonitor/pywatchman/pybser.py Thu Oct 20 12:05:17 2022 -0400 +++ b/hgext/fsmonitor/pywatchman/pybser.py Mon Oct 24 15:32:14 2022 +0200 @@ -36,6 +36,7 @@ from . import compat +abc = collections.abc BSER_ARRAY = b"\x00" BSER_OBJECT = b"\x01" @@ -207,9 +208,7 @@ self.ensure_size(needed) struct.pack_into(b"=cd", self.buf, self.wpos, BSER_REAL, val) self.wpos += needed - elif isinstance(val, collections.Mapping) and isinstance( - val, collections.Sized - ): + elif isinstance(val, abc.Mapping) and isinstance(val, abc.Sized): val_len = len(val) size = _int_size(val_len) needed = 2 + size @@ -260,9 +259,7 @@ for k, v in iteritems: self.append_string(k) self.append_recursive(v) - elif isinstance(val, collections.Iterable) and isinstance( - val, collections.Sized - ): + elif isinstance(val, abc.Iterable) and isinstance(val, abc.Sized): val_len = len(val) size = _int_size(val_len) needed = 2 + size diff -r 192949b68159 -r a3356ab610fc hgext/rebase.py --- a/hgext/rebase.py Thu Oct 20 12:05:17 2022 -0400 +++ b/hgext/rebase.py Mon Oct 24 15:32:14 2022 +0200 @@ -546,7 +546,9 @@ date = self.date if date is None: date = ctx.date() - extra = {b'rebase_source': ctx.hex()} + extra = {} + if repo.ui.configbool(b'rebase', b'store-source'): + extra = {b'rebase_source': ctx.hex()} for c in self.extrafns: c(ctx, extra) destphase = max(ctx.phase(), phases.draft) diff -r 192949b68159 -r a3356ab610fc hgext/releasenotes.py --- a/hgext/releasenotes.py Thu Oct 20 12:05:17 2022 -0400 +++ b/hgext/releasenotes.py Mon Oct 24 15:32:14 2022 +0200 @@ -70,7 +70,7 @@ (b'api', _(b'API Changes')), ] -RE_DIRECTIVE = re.compile(br'^\.\. ([a-zA-Z0-9_]+)::\s*([^$]+)?$') +RE_DIRECTIVE = re.compile(br'^\.\. ([a-zA-Z0-9_]+)::\s*([^$]+)?$', re.MULTILINE) RE_ISSUE = br'\bissue ?[0-9]{4,6}(?![0-9])\b' BULLET_SECTION = _(b'Other Changes') diff -r 192949b68159 -r a3356ab610fc mercurial/cext/revlog.c --- a/mercurial/cext/revlog.c Thu Oct 20 12:05:17 2022 -0400 +++ b/mercurial/cext/revlog.c Mon Oct 24 15:32:14 2022 +0200 @@ -1382,6 +1382,7 @@ static int index_issnapshotrev(indexObject *self, Py_ssize_t rev) { int ps[2]; + int b; Py_ssize_t base; while (rev >= 0) { base = (Py_ssize_t)index_baserev(self, rev); @@ -1399,6 +1400,20 @@ assert(PyErr_Occurred()); return -1; }; + while ((index_get_length(self, ps[0]) == 0) && ps[0] >= 0) { + b = index_baserev(self, ps[0]); + if (b == ps[0]) { + break; + } + ps[0] = b; + } + while ((index_get_length(self, ps[1]) == 0) && ps[1] >= 0) { + b = index_baserev(self, ps[1]); + if (b == ps[1]) { + break; + } + ps[1] = b; + } if (base == ps[0] || base == ps[1]) { return 0; } diff -r 192949b68159 -r a3356ab610fc mercurial/cmdutil.py --- a/mercurial/cmdutil.py Thu Oct 20 12:05:17 2022 -0400 +++ b/mercurial/cmdutil.py Mon Oct 24 15:32:14 2022 +0200 @@ -832,7 +832,7 @@ @attr.s(frozen=True) class morestatus: - reporoot = attr.ib() + repo = attr.ib() unfinishedop = attr.ib() unfinishedmsg = attr.ib() activemerge = attr.ib() @@ -876,7 +876,7 @@ mergeliststr = b'\n'.join( [ b' %s' - % util.pathto(self.reporoot, encoding.getcwd(), path) + % util.pathto(self.repo.root, encoding.getcwd(), path) for path in self.unresolvedpaths ] ) @@ -898,6 +898,7 @@ # Already output. continue fm.startitem() + fm.context(repo=self.repo) # We can't claim to know the status of the file - it may just # have been in one of the states that were not requested for # display, so it could be anything. @@ -923,7 +924,7 @@ if activemerge: unresolved = sorted(mergestate.unresolved()) return morestatus( - repo.root, unfinishedop, unfinishedmsg, activemerge, unresolved + repo, unfinishedop, unfinishedmsg, activemerge, unresolved ) diff -r 192949b68159 -r a3356ab610fc mercurial/commands.py --- a/mercurial/commands.py Thu Oct 20 12:05:17 2022 -0400 +++ b/mercurial/commands.py Mon Oct 24 15:32:14 2022 +0200 @@ -1035,7 +1035,14 @@ state = hbisect.load_state(repo) if rev: - nodes = [repo[i].node() for i in logcmdutil.revrange(repo, rev)] + revs = logcmdutil.revrange(repo, rev) + goodnodes = state[b'good'] + badnodes = state[b'bad'] + if goodnodes and badnodes: + candidates = repo.revs(b'(%ln)::(%ln)', goodnodes, badnodes) + candidates += repo.revs(b'(%ln)::(%ln)', badnodes, goodnodes) + revs = candidates & revs + nodes = [repo.changelog.node(i) for i in revs] else: nodes = [repo.lookup(b'.')] @@ -1485,6 +1492,12 @@ b'bundle', [ ( + b'', + b'exact', + None, + _(b'compute the base from the revision specified'), + ), + ( b'f', b'force', None, @@ -1553,6 +1566,7 @@ Returns 0 on success, 1 if no changes found. """ opts = pycompat.byteskwargs(opts) + revs = None if b'rev' in opts: revstrings = opts[b'rev'] @@ -1586,7 +1600,19 @@ ) if opts.get(b'base'): ui.warn(_(b"ignoring --base because --all was specified\n")) + if opts.get(b'exact'): + ui.warn(_(b"ignoring --exact because --all was specified\n")) base = [nullrev] + elif opts.get(b'exact'): + if dests: + raise error.InputError( + _(b"--exact is incompatible with specifying destinations") + ) + if opts.get(b'base'): + ui.warn(_(b"ignoring --base because --exact was specified\n")) + base = repo.revs(b'parents(%ld) - %ld', revs, revs) + if not base: + base = [nullrev] else: base = logcmdutil.revrange(repo, opts.get(b'base')) if cgversion not in changegroup.supportedoutgoingversions(repo): @@ -6954,11 +6980,13 @@ ) copy = {} - if ( - opts.get(b'all') - or opts.get(b'copies') - or ui.configbool(b'ui', b'statuscopies') - ) and not opts.get(b'no_status'): + show_copies = ui.configbool(b'ui', b'statuscopies') + if opts.get(b'copies') is not None: + show_copies = opts.get(b'copies') + show_copies = (show_copies or opts.get(b'all')) and not opts.get( + b'no_status' + ) + if show_copies: copy = copies.pathcopies(ctx1, ctx2, m) morestatus = None diff -r 192949b68159 -r a3356ab610fc mercurial/configitems.py --- a/mercurial/configitems.py Thu Oct 20 12:05:17 2022 -0400 +++ b/mercurial/configitems.py Mon Oct 24 15:32:14 2022 +0200 @@ -1425,12 +1425,38 @@ default=False, experimental=True, ) + +# Moving this on by default means we are confident about the scaling of phases. +# This is not garanteed to be the case at the time this message is written. coreconfigitem( b'format', - b'internal-phase', + b'use-internal-phase', default=False, experimental=True, ) +# The interaction between the archived phase and obsolescence markers needs to +# be sorted out before wider usage of this are to be considered. +# +# At the time this message is written, behavior when archiving obsolete +# changeset differ significantly from stripping. As part of stripping, we also +# remove the obsolescence marker associated to the stripped changesets, +# revealing the precedecessors changesets when applicable. When archiving, we +# don't touch the obsolescence markers, keeping everything hidden. This can +# result in quite confusing situation for people combining exchanging draft +# with the archived phases. As some markers needed by others may be skipped +# during exchange. +coreconfigitem( + b'format', + b'exp-archived-phase', + default=False, + experimental=True, +) +coreconfigitem( + b'shelve', + b'store', + default=b'internal', + experimental=True, +) coreconfigitem( b'fsmonitor', b'warn_when_unused', @@ -2835,3 +2861,17 @@ b'experimental.inmemory', default=False, ) + +# This setting controls creation of a rebase_source extra field +# during rebase. When False, no such field is created. This is +# useful eg for incrementally converting changesets and then +# rebasing them onto an existing repo. +# WARNING: this is an advanced setting reserved for people who know +# exactly what they are doing. Misuse of this setting can easily +# result in obsmarker cycles and a vivid headache. +coreconfigitem( + b'rebase', + b'store-source', + default=True, + experimental=True, +) diff -r 192949b68159 -r a3356ab610fc mercurial/debugcommands.py --- a/mercurial/debugcommands.py Thu Oct 20 12:05:17 2022 -0400 +++ b/mercurial/debugcommands.py Mon Oct 24 15:32:14 2022 +0200 @@ -1021,7 +1021,7 @@ deltacomputer = deltautil.deltacomputer( revlog, write_debug=ui.write, - debug_search=True, + debug_search=not ui.quiet, ) node = revlog.node(rev) diff -r 192949b68159 -r a3356ab610fc mercurial/defaultrc/mergetools.rc --- a/mercurial/defaultrc/mergetools.rc Thu Oct 20 12:05:17 2022 -0400 +++ b/mercurial/defaultrc/mergetools.rc Mon Oct 24 15:32:14 2022 +0200 @@ -107,7 +107,7 @@ meld.args=--label=$labellocal $local --label='merged' $base --label=$labelother $other -o $output --auto-merge meld.check=changed -meld.diffargs=-a --label=$plabel1 $parent --label=$clabel $child +meld.diffargs=--label=$plabel1 $parent --label=$clabel $child meld.gui=True merge.check=conflicts diff -r 192949b68159 -r a3356ab610fc mercurial/dirstate.py --- a/mercurial/dirstate.py Thu Oct 20 12:05:17 2022 -0400 +++ b/mercurial/dirstate.py Mon Oct 24 15:32:14 2022 +0200 @@ -1287,6 +1287,7 @@ allowed_matchers = ( matchmod.alwaysmatcher, + matchmod.differencematcher, matchmod.exactmatcher, matchmod.includematcher, matchmod.intersectionmatcher, diff -r 192949b68159 -r a3356ab610fc mercurial/dispatch.py --- a/mercurial/dispatch.py Thu Oct 20 12:05:17 2022 -0400 +++ b/mercurial/dispatch.py Mon Oct 24 15:32:14 2022 +0200 @@ -952,14 +952,22 @@ Takes paths in [cwd]/.hg/hgrc into account." """ + try: + cwd = encoding.getcwd() + except OSError as e: + raise error.Abort( + _(b"error getting current working directory: %s") + % encoding.strtolocal(e.strerror) + ) + + # If using an alternate wd, temporarily switch to it so that relative + # paths are resolved correctly during config loading. + oldcwd = None if wd is None: - try: - wd = encoding.getcwd() - except OSError as e: - raise error.Abort( - _(b"error getting current working directory: %s") - % encoding.strtolocal(e.strerror) - ) + wd = cwd + else: + oldcwd = cwd + os.chdir(wd) path = cmdutil.findrepo(wd) or b"" if not path: @@ -979,6 +987,9 @@ lui.readconfig(os.path.join(path, b".hg", b"hgrc"), path) lui.readconfig(os.path.join(path, b".hg", b"hgrc-not-shared"), path) + if oldcwd: + os.chdir(oldcwd) + return path, lui diff -r 192949b68159 -r a3356ab610fc mercurial/hbisect.py --- a/mercurial/hbisect.py Thu Oct 20 12:05:17 2022 -0400 +++ b/mercurial/hbisect.py Mon Oct 24 15:32:14 2022 +0200 @@ -39,7 +39,7 @@ def buildancestors(bad, good): badrev = min([changelog.rev(n) for n in bad]) ancestors = collections.defaultdict(lambda: None) - for rev in repo.revs(b"descendants(%ln) - ancestors(%ln)", good, good): + for rev in repo.revs(b"(%ln::%d) - (::%ln)", good, badrev, good): ancestors[rev] = [] if ancestors[badrev] is None: return badrev, None @@ -115,11 +115,21 @@ poison.update(children.get(rev, [])) continue + unvisited = [] for c in children.get(rev, []): if ancestors[c]: ancestors[c] = list(set(ancestors[c] + a)) else: + unvisited.append(c) + + # Reuse existing ancestor list for the first unvisited child to avoid + # excessive copying for linear portions of history. + if unvisited: + first = unvisited.pop(0) + for c in unvisited: ancestors[c] = a + [c] + a.append(first) + ancestors[first] = a assert best_rev is not None best_node = changelog.node(best_rev) diff -r 192949b68159 -r a3356ab610fc mercurial/helptext/bundlespec.txt --- a/mercurial/helptext/bundlespec.txt Thu Oct 20 12:05:17 2022 -0400 +++ b/mercurial/helptext/bundlespec.txt Mon Oct 24 15:32:14 2022 +0200 @@ -67,6 +67,33 @@ .. bundlecompressionmarker +Available Options +================= + +The following options exist: + +changegroup + Include the changegroup data in the bundle (default to True). + +cg.version + Select the version of the changegroup to use. Available options are : 01, 02 + or 03. By default it will be automatically selected according to the current + repository format. + +obsolescence + Include obsolescence-markers relevant to the bundled changesets. + +phases + Include phase information relevant to the bundled changesets. + +revbranchcache + Include the "tags-fnodes" cache inside the bundle. + + +tagsfnodescache + Include the "tags-fnodes" cache inside the bundle. + + Examples ======== diff -r 192949b68159 -r a3356ab610fc mercurial/hgweb/__init__.py --- a/mercurial/hgweb/__init__.py Thu Oct 20 12:05:17 2022 -0400 +++ b/mercurial/hgweb/__init__.py Mon Oct 24 15:32:14 2022 +0200 @@ -18,12 +18,15 @@ from ..utils import procutil +# pytype: disable=pyi-error from . import ( hgweb_mod, hgwebdir_mod, server, ) +# pytype: enable=pyi-error + def hgweb(config, name=None, baseui=None): """create an hgweb wsgi object diff -r 192949b68159 -r a3356ab610fc mercurial/localrepo.py --- a/mercurial/localrepo.py Thu Oct 20 12:05:17 2022 -0400 +++ b/mercurial/localrepo.py Mon Oct 24 15:32:14 2022 +0200 @@ -522,12 +522,8 @@ # the repository. This file was introduced in Mercurial 0.9.2, # which means very old repositories may not have one. We assume # a missing file translates to no requirements. - try: - return set(vfs.read(b'requires').splitlines()) - except FileNotFoundError: - if not allowmissing: - raise - return set() + read = vfs.tryread if allowmissing else vfs.read + return set(read(b'requires').splitlines()) def makelocalrepository(baseui, path, intents=None): @@ -1281,6 +1277,7 @@ """ _basesupported = { + requirementsmod.ARCHIVED_PHASE_REQUIREMENT, requirementsmod.BOOKMARKS_IN_STORE_REQUIREMENT, requirementsmod.CHANGELOGV2_REQUIREMENT, requirementsmod.COPIESSDC_REQUIREMENT, @@ -3668,9 +3665,13 @@ requirements.discard(requirementsmod.REVLOGV1_REQUIREMENT) requirements.add(requirementsmod.REVLOGV2_REQUIREMENT) # experimental config: format.internal-phase - if ui.configbool(b'format', b'internal-phase'): + if ui.configbool(b'format', b'use-internal-phase'): requirements.add(requirementsmod.INTERNAL_PHASE_REQUIREMENT) + # experimental config: format.exp-archived-phase + if ui.configbool(b'format', b'exp-archived-phase'): + requirements.add(requirementsmod.ARCHIVED_PHASE_REQUIREMENT) + if createopts.get(b'narrowfiles'): requirements.add(requirementsmod.NARROW_REQUIREMENT) diff -r 192949b68159 -r a3356ab610fc mercurial/obsolete.py --- a/mercurial/obsolete.py Thu Oct 20 12:05:17 2022 -0400 +++ b/mercurial/obsolete.py Mon Oct 24 15:32:14 2022 +0200 @@ -70,6 +70,7 @@ import binascii import struct +import weakref from .i18n import _ from .pycompat import getattr @@ -561,10 +562,18 @@ # caches for various obsolescence related cache self.caches = {} self.svfs = svfs - self.repo = repo + self._repo = weakref.ref(repo) self._defaultformat = defaultformat self._readonly = readonly + @property + def repo(self): + r = self._repo() + if r is None: + msg = "using the obsstore of a deallocated repo" + raise error.ProgrammingError(msg) + return r + def __iter__(self): return iter(self._all) diff -r 192949b68159 -r a3356ab610fc mercurial/phases.py --- a/mercurial/phases.py Thu Oct 20 12:05:17 2022 -0400 +++ b/mercurial/phases.py Mon Oct 24 15:32:14 2022 +0200 @@ -178,6 +178,12 @@ return requirements.INTERNAL_PHASE_REQUIREMENT in repo.requirements +def supportarchived(repo): + # type: (localrepo.localrepository) -> bool + """True if the archived phase can be used on a repository""" + return requirements.ARCHIVED_PHASE_REQUIREMENT in repo.requirements + + def _readroots(repo, phasedefaults=None): # type: (localrepo.localrepository, Optional[Phasedefaults]) -> Tuple[Phaseroots, bool] """Read phase roots from disk @@ -642,7 +648,12 @@ # phaseroots values, replace them. if revs is None: revs = [] - if targetphase in (archived, internal) and not supportinternal(repo): + if ( + targetphase == internal + and not supportinternal(repo) + or targetphase == archived + and not supportarchived(repo) + ): name = phasenames[targetphase] msg = b'this repository does not support the %s phase' % name raise error.ProgrammingError(msg) diff -r 192949b68159 -r a3356ab610fc mercurial/requirements.py --- a/mercurial/requirements.py Thu Oct 20 12:05:17 2022 -0400 +++ b/mercurial/requirements.py Mon Oct 24 15:32:14 2022 +0200 @@ -29,7 +29,11 @@ # Enables the internal phase which is used to hide changesets instead # of stripping them -INTERNAL_PHASE_REQUIREMENT = b'internal-phase' +INTERNAL_PHASE_REQUIREMENT = b'internal-phase-2' + +# Enables the internal phase which is used to hide changesets instead +# of stripping them +ARCHIVED_PHASE_REQUIREMENT = b'exp-archived-phase' # Stores manifest in Tree structure TREEMANIFEST_REQUIREMENT = b'treemanifest' @@ -107,6 +111,7 @@ # # note: the list is currently inherited from previous code and miss some relevant requirement while containing some irrelevant ones. STREAM_FIXED_REQUIREMENTS = { + ARCHIVED_PHASE_REQUIREMENT, BOOKMARKS_IN_STORE_REQUIREMENT, CHANGELOGV2_REQUIREMENT, COPIESSDC_REQUIREMENT, diff -r 192949b68159 -r a3356ab610fc mercurial/revlog.py --- a/mercurial/revlog.py Thu Oct 20 12:05:17 2022 -0400 +++ b/mercurial/revlog.py Mon Oct 24 15:32:14 2022 +0200 @@ -235,6 +235,8 @@ b' expected %d bytes from offset %d, data size is %d' ) +hexdigits = b'0123456789abcdefABCDEF' + class revlog: """ @@ -1509,7 +1511,7 @@ ambiguous = True # fall through to slow path that filters hidden revisions except (AttributeError, ValueError): - # we are pure python, or key was too short to search radix tree + # we are pure python, or key is not hex pass if ambiguous: raise error.AmbiguousPrefixLookupError( @@ -1523,6 +1525,11 @@ # hex(node)[:...] l = len(id) // 2 * 2 # grab an even number of digits try: + # we're dropping the last digit, so let's check that it's hex, + # to avoid the expensive computation below if it's not + if len(id) % 2 > 0: + if not (id[-1] in hexdigits): + return None prefix = bin(id[:l]) except binascii.Error: pass @@ -1768,7 +1775,17 @@ if base == nullrev: return True p1 = entry[5] + while self.length(p1) == 0: + b = self.deltaparent(p1) + if b == p1: + break + p1 = b p2 = entry[6] + while self.length(p2) == 0: + b = self.deltaparent(p2) + if b == p2: + break + p2 = b if base == p1 or base == p2: return False return self.issnapshot(base) diff -r 192949b68159 -r a3356ab610fc mercurial/revset.py --- a/mercurial/revset.py Thu Oct 20 12:05:17 2022 -0400 +++ b/mercurial/revset.py Mon Oct 24 15:32:14 2022 +0200 @@ -7,7 +7,10 @@ import binascii +import functools +import random import re +import sys from .i18n import _ from .pycompat import getattr @@ -2339,14 +2342,28 @@ parents = repo.changelog.parentrevs def filter(r): - for p in parents(r): - if 0 <= p and p in s: - return False + try: + for p in parents(r): + if 0 <= p and p in s: + return False + except error.WdirUnsupported: + for p in repo[None].parents(): + if p.rev() in s: + return False return True return subset & s.filter(filter, condrepr=b'') +MAXINT = sys.maxsize +MININT = -MAXINT - 1 + + +def pick_random(c, gen=random): + # exists as its own function to make it possible to overwrite the seed + return gen.randint(MININT, MAXINT) + + _sortkeyfuncs = { b'rev': scmutil.intrev, b'branch': lambda c: c.branch(), @@ -2355,12 +2372,17 @@ b'author': lambda c: c.user(), b'date': lambda c: c.date()[0], b'node': scmutil.binnode, + b'random': pick_random, } def _getsortargs(x): """Parse sort options into (set, [(key, reverse)], opts)""" - args = getargsdict(x, b'sort', b'set keys topo.firstbranch') + args = getargsdict( + x, + b'sort', + b'set keys topo.firstbranch random.seed', + ) if b'set' not in args: # i18n: "sort" is a keyword raise error.ParseError(_(b'sort requires one or two arguments')) @@ -2400,6 +2422,20 @@ ) ) + if b'random.seed' in args: + if any(k == b'random' for k, reverse in keyflags): + s = args[b'random.seed'] + seed = getstring(s, _(b"random.seed must be a string")) + opts[b'random.seed'] = seed + else: + # i18n: "random" and "random.seed" are keywords + raise error.ParseError( + _( + b'random.seed can only be used ' + b'when using the random sort key' + ) + ) + return args[b'set'], keyflags, opts @@ -2419,11 +2455,14 @@ - ``date`` for the commit date - ``topo`` for a reverse topographical sort - ``node`` the nodeid of the revision + - ``random`` randomly shuffle revisions The ``topo`` sort order cannot be combined with other sort keys. This sort takes one optional argument, ``topo.firstbranch``, which takes a revset that specifies what topographical branches to prioritize in the sort. + The ``random`` sort takes one optional ``random.seed`` argument to control + the pseudo-randomness of the result. """ s, keyflags, opts = _getsortargs(x) revs = getset(repo, subset, s, order) @@ -2435,10 +2474,20 @@ return revs elif keyflags[0][0] == b"topo": firstbranch = () + parentrevs = repo.changelog.parentrevs + parentsfunc = parentrevs + if wdirrev in revs: + + def parentsfunc(r): + try: + return parentrevs(r) + except error.WdirUnsupported: + return [p.rev() for p in repo[None].parents()] + if b'topo.firstbranch' in opts: firstbranch = getset(repo, subset, opts[b'topo.firstbranch']) revs = baseset( - dagop.toposort(revs, repo.changelog.parentrevs, firstbranch), + dagop.toposort(revs, parentsfunc, firstbranch), istopo=True, ) if keyflags[0][1]: @@ -2448,7 +2497,12 @@ # sort() is guaranteed to be stable ctxs = [repo[r] for r in revs] for k, reverse in reversed(keyflags): - ctxs.sort(key=_sortkeyfuncs[k], reverse=reverse) + func = _sortkeyfuncs[k] + if k == b'random' and b'random.seed' in opts: + seed = opts[b'random.seed'] + r = random.Random(seed) + func = functools.partial(func, gen=r) + ctxs.sort(key=func, reverse=reverse) return baseset([c.rev() for c in ctxs]) diff -r 192949b68159 -r a3356ab610fc mercurial/scmutil.py --- a/mercurial/scmutil.py Thu Oct 20 12:05:17 2022 -0400 +++ b/mercurial/scmutil.py Mon Oct 24 15:32:14 2022 +0200 @@ -1191,7 +1191,7 @@ obsolete.createmarkers( repo, rels, operation=operation, metadata=metadata ) - elif phases.supportinternal(repo) and mayusearchived: + elif phases.supportarchived(repo) and mayusearchived: # this assume we do not have "unstable" nodes above the cleaned ones allreplaced = set() for ns in replacements.keys(): diff -r 192949b68159 -r a3356ab610fc mercurial/shelve.py --- a/mercurial/shelve.py Thu Oct 20 12:05:17 2022 -0400 +++ b/mercurial/shelve.py Mon Oct 24 15:32:14 2022 +0200 @@ -22,6 +22,7 @@ """ import collections +import io import itertools import stat @@ -98,6 +99,17 @@ return sorted(info, reverse=True) +def _use_internal_phase(repo): + return ( + phases.supportinternal(repo) + and repo.ui.config(b'shelve', b'store') == b'internal' + ) + + +def _target_phase(repo): + return phases.internal if _use_internal_phase(repo) else phases.secret + + class Shelf: """Represents a shelf, including possibly multiple files storing it. @@ -111,12 +123,19 @@ self.name = name def exists(self): - return self.vfs.exists(self.name + b'.patch') and self.vfs.exists( - self.name + b'.hg' - ) + return self._exists(b'.shelve') or self._exists(b'.patch', b'.hg') + + def _exists(self, *exts): + return all(self.vfs.exists(self.name + ext) for ext in exts) def mtime(self): - return self.vfs.stat(self.name + b'.patch')[stat.ST_MTIME] + try: + return self._stat(b'.shelve')[stat.ST_MTIME] + except FileNotFoundError: + return self._stat(b'.patch')[stat.ST_MTIME] + + def _stat(self, ext): + return self.vfs.stat(self.name + ext) def writeinfo(self, info): scmutil.simplekeyvaluefile(self.vfs, self.name + b'.shelve').write(info) @@ -159,9 +178,7 @@ filename = self.name + b'.hg' fp = self.vfs(filename) try: - targetphase = phases.internal - if not phases.supportinternal(repo): - targetphase = phases.secret + targetphase = _target_phase(repo) gen = exchange.readbundle(repo.ui, fp, filename, self.vfs) pretip = repo[b'tip'] bundle2.applybundle( @@ -183,6 +200,27 @@ def open_patch(self, mode=b'rb'): return self.vfs(self.name + b'.patch', mode) + def patch_from_node(self, repo, node): + repo = repo.unfiltered() + match = _optimized_match(repo, node) + fp = io.BytesIO() + cmdutil.exportfile( + repo, + [node], + fp, + opts=mdiff.diffopts(git=True), + match=match, + ) + fp.seek(0) + return fp + + def load_patch(self, repo): + try: + # prefer node-based shelf + return self.patch_from_node(repo, self.readinfo()[b'node']) + except (FileNotFoundError, error.RepoLookupError): + return self.open_patch() + def _backupfilename(self, backupvfs, filename): def gennames(base): yield base @@ -210,6 +248,15 @@ self.vfs.tryunlink(self.name + b'.' + ext) +def _optimized_match(repo, node): + """ + Create a matcher so that prefetch doesn't attempt to fetch + the entire repository pointlessly, and as an optimisation + for movedirstate, if needed. + """ + return scmutil.matchfiles(repo, repo[node].files()) + + class shelvedstate: """Handle persistence during unshelving operations. @@ -447,9 +494,7 @@ if hasmq: saved, repo.mq.checkapplied = repo.mq.checkapplied, False - targetphase = phases.internal - if not phases.supportinternal(repo): - targetphase = phases.secret + targetphase = _target_phase(repo) overrides = {(b'phases', b'new-commit'): targetphase} try: editor_ = False @@ -510,7 +555,7 @@ def _finishshelve(repo, tr): - if phases.supportinternal(repo): + if _use_internal_phase(repo): tr.close() else: _aborttransaction(repo, tr) @@ -579,10 +624,7 @@ _nothingtoshelvemessaging(ui, repo, pats, opts) return 1 - # Create a matcher so that prefetch doesn't attempt to fetch - # the entire repository pointlessly, and as an optimisation - # for movedirstate, if needed. - match = scmutil.matchfiles(repo, repo[node].files()) + match = _optimized_match(repo, node) _shelvecreatedcommit(repo, node, name, match) ui.status(_(b'shelved as %s\n') % name) @@ -668,7 +710,7 @@ ui.write(age, label=b'shelve.age') ui.write(b' ' * (12 - len(age))) used += 12 - with shelf_dir.get(name).open_patch() as fp: + with shelf_dir.get(name).load_patch(repo) as fp: while True: line = fp.readline() if not line: @@ -754,7 +796,7 @@ if state.activebookmark and state.activebookmark in repo._bookmarks: bookmarks.activate(repo, state.activebookmark) mergefiles(ui, repo, state.wctx, state.pendingctx) - if not phases.supportinternal(repo): + if not _use_internal_phase(repo): repair.strip( ui, repo, state.nodestoremove, backup=False, topic=b'shelve' ) @@ -816,9 +858,7 @@ repo.setparents(state.pendingctx.node(), repo.nullid) repo.dirstate.write(repo.currenttransaction()) - targetphase = phases.internal - if not phases.supportinternal(repo): - targetphase = phases.secret + targetphase = _target_phase(repo) overrides = {(b'phases', b'new-commit'): targetphase} with repo.ui.configoverride(overrides, b'unshelve'): with repo.dirstate.parentchange(): @@ -843,7 +883,7 @@ mergefiles(ui, repo, state.wctx, shelvectx) restorebranch(ui, repo, state.branchtorestore) - if not phases.supportinternal(repo): + if not _use_internal_phase(repo): repair.strip( ui, repo, state.nodestoremove, backup=False, topic=b'shelve' ) @@ -957,7 +997,7 @@ user=shelvectx.user(), ) if snode: - m = scmutil.matchfiles(repo, repo[snode].files()) + m = _optimized_match(repo, snode) _shelvecreatedcommit(repo, snode, basename, m) return newnode, bool(snode) @@ -1137,7 +1177,6 @@ oldtiprev = len(repo) pctx = repo[b'.'] - tmpwctx = pctx # The goal is to have a commit structure like so: # ...-> pctx -> tmpwctx -> shelvectx # where tmpwctx is an optional commit with the user's pending changes @@ -1145,9 +1184,7 @@ # to the original pctx. activebookmark = _backupactivebookmark(repo) - tmpwctx, addedbefore = _commitworkingcopychanges( - ui, repo, opts, tmpwctx - ) + tmpwctx, addedbefore = _commitworkingcopychanges(ui, repo, opts, pctx) repo, shelvectx = _unshelverestorecommit(ui, repo, tr, basename) _checkunshelveuntrackedproblems(ui, repo, shelvectx) branchtorestore = b'' diff -r 192949b68159 -r a3356ab610fc mercurial/subrepo.py --- a/mercurial/subrepo.py Thu Oct 20 12:05:17 2022 -0400 +++ b/mercurial/subrepo.py Mon Oct 24 15:32:14 2022 +0200 @@ -1099,6 +1099,10 @@ # --non-interactive. if commands[0] in (b'update', b'checkout', b'commit'): cmd.append(b'--non-interactive') + if util.safehasattr(subprocess, 'CREATE_NO_WINDOW'): + # On Windows, prevent command prompts windows from popping up when + # running in pythonw. + extrakw['creationflags'] = getattr(subprocess, 'CREATE_NO_WINDOW') cmd.extend(commands) if filename is not None: path = self.wvfs.reljoin( @@ -1150,7 +1154,7 @@ # commit revision so we can compare the subrepo state with # both. We used to store the working directory one. output, err = self._svncommand([b'info', b'--xml']) - doc = xml.dom.minidom.parseString(output) + doc = xml.dom.minidom.parseString(output) # pytype: disable=pyi-error entries = doc.getElementsByTagName('entry') lastrev, rev = b'0', b'0' if entries: @@ -1174,7 +1178,7 @@ """ output, err = self._svncommand([b'status', b'--xml']) externals, changes, missing = [], [], [] - doc = xml.dom.minidom.parseString(output) + doc = xml.dom.minidom.parseString(output) # pytype: disable=pyi-error for e in doc.getElementsByTagName('entry'): s = e.getElementsByTagName('wc-status') if not s: @@ -1319,7 +1323,7 @@ @annotatesubrepoerror def files(self): output = self._svncommand([b'list', b'--recursive', b'--xml'])[0] - doc = xml.dom.minidom.parseString(output) + doc = xml.dom.minidom.parseString(output) # pytype: disable=pyi-error paths = [] for e in doc.getElementsByTagName('entry'): kind = pycompat.bytestr(e.getAttribute('kind')) @@ -1469,6 +1473,11 @@ # insert the argument in the front, # the end of git diff arguments is used for paths commands.insert(1, b'--color') + extrakw = {} + if util.safehasattr(subprocess, 'CREATE_NO_WINDOW'): + # On Windows, prevent command prompts windows from popping up when + # running in pythonw. + extrakw['creationflags'] = getattr(subprocess, 'CREATE_NO_WINDOW') p = subprocess.Popen( pycompat.rapply( procutil.tonativestr, [self._gitexecutable] + commands @@ -1479,6 +1488,7 @@ close_fds=procutil.closefds, stdout=subprocess.PIPE, stderr=errpipe, + **extrakw ) if stream: return p.stdout, None diff -r 192949b68159 -r a3356ab610fc mercurial/templatefilters.py --- a/mercurial/templatefilters.py Thu Oct 20 12:05:17 2022 -0400 +++ b/mercurial/templatefilters.py Mon Oct 24 15:32:14 2022 +0200 @@ -390,6 +390,14 @@ return stringutil.person(author) +@templatefilter(b'reverse') +def reverse(list_): + """List. Reverses the order of list items.""" + if isinstance(list_, list): + return templateutil.hybridlist(list_[::-1], name=b'item') + raise error.ParseError(_(b'not reversible')) + + @templatefilter(b'revescape', intype=bytes) def revescape(text): """Any text. Escapes all "special" characters, except @. diff -r 192949b68159 -r a3356ab610fc mercurial/url.py --- a/mercurial/url.py Thu Oct 20 12:05:17 2022 -0400 +++ b/mercurial/url.py Mon Oct 24 15:32:14 2022 +0200 @@ -222,13 +222,16 @@ h.headers = None -def _generic_proxytunnel(self): +def _generic_proxytunnel(self: "httpsconnection"): + headers = self.headers # pytype: disable=attribute-error proxyheaders = { - pycompat.bytestr(x): pycompat.bytestr(self.headers[x]) - for x in self.headers + pycompat.bytestr(x): pycompat.bytestr(headers[x]) + for x in headers if x.lower().startswith('proxy-') } - self.send(b'CONNECT %s HTTP/1.0\r\n' % self.realhostport) + realhostport = self.realhostport # pytype: disable=attribute-error + self.send(b'CONNECT %s HTTP/1.0\r\n' % realhostport) + for header in proxyheaders.items(): self.send(b'%s: %s\r\n' % header) self.send(b'\r\n') @@ -237,10 +240,14 @@ # httplib.HTTPConnection as there are no adequate places to # override functions to provide the needed functionality. + # pytype: disable=attribute-error res = self.response_class(self.sock, method=self._method) + # pytype: enable=attribute-error while True: + # pytype: disable=attribute-error version, status, reason = res._read_status() + # pytype: enable=attribute-error if status != httplib.CONTINUE: break # skip lines that are all whitespace @@ -323,14 +330,15 @@ self.sock = socket.create_connection((self.host, self.port)) host = self.host - if self.realhostport: # use CONNECT proxy + realhostport = self.realhostport # pytype: disable=attribute-error + if realhostport: # use CONNECT proxy _generic_proxytunnel(self) - host = self.realhostport.rsplit(b':', 1)[0] + host = realhostport.rsplit(b':', 1)[0] self.sock = sslutil.wrapsocket( self.sock, self.key_file, self.cert_file, - ui=self.ui, + ui=self.ui, # pytype: disable=attribute-error serverhostname=host, ) sslutil.validatesocket(self.sock) diff -r 192949b68159 -r a3356ab610fc relnotes/next --- a/relnotes/next Thu Oct 20 12:05:17 2022 -0400 +++ b/relnotes/next Mon Oct 24 15:32:14 2022 +0200 @@ -13,6 +13,12 @@ == Backwards Compatibility Changes == + * chg worker processes will now correctly load per-repository configuration + when given a both a relative `--repository` path and an alternate working + directory via `--cwd`. A side-effect of this change is that these workers + will now return an error if hg cannot find the current working directory, + even when a different directory is specified via `--cwd`. + == Internal API Changes == == Miscellaneous == diff -r 192949b68159 -r a3356ab610fc rust/Cargo.lock --- a/rust/Cargo.lock Thu Oct 20 12:05:17 2022 -0400 +++ b/rust/Cargo.lock Mon Oct 24 15:32:14 2022 +0200 @@ -468,6 +468,7 @@ "log", "memmap2", "micro-timer", + "once_cell", "ouroboros", "pretty_assertions", "rand 0.8.5", @@ -687,6 +688,12 @@ ] [[package]] +name = "once_cell" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0" + +[[package]] name = "opaque-debug" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -981,6 +988,7 @@ "lazy_static", "log", "micro-timer", + "rayon", "regex", "users", "which", diff -r 192949b68159 -r a3356ab610fc rust/hg-core/Cargo.toml --- a/rust/hg-core/Cargo.toml Thu Oct 20 12:05:17 2022 -0400 +++ b/rust/hg-core/Cargo.toml Mon Oct 24 15:32:14 2022 +0200 @@ -35,6 +35,9 @@ memmap2 = { version = "0.5.3", features = ["stable_deref_trait"] } zstd = "0.5.3" format-bytes = "0.3.0" +# once_cell 1.15 uses edition 2021, while the heptapod CI +# uses an old version of Cargo that doesn't support it. +once_cell = "=1.14.0" # We don't use the `miniz-oxide` backend to not change rhg benchmarks and until # we have a clearer view of which backend is the fastest. diff -r 192949b68159 -r a3356ab610fc rust/hg-core/src/config.rs --- a/rust/hg-core/src/config.rs Thu Oct 20 12:05:17 2022 -0400 +++ b/rust/hg-core/src/config.rs Mon Oct 24 15:32:14 2022 +0200 @@ -11,6 +11,8 @@ mod config; mod layer; +mod plain_info; mod values; pub use config::{Config, ConfigSource, ConfigValueParseError}; pub use layer::{ConfigError, ConfigOrigin, ConfigParseError}; +pub use plain_info::PlainInfo; diff -r 192949b68159 -r a3356ab610fc rust/hg-core/src/config/config.rs --- a/rust/hg-core/src/config/config.rs Thu Oct 20 12:05:17 2022 -0400 +++ b/rust/hg-core/src/config/config.rs Mon Oct 24 15:32:14 2022 +0200 @@ -12,6 +12,7 @@ use crate::config::layer::{ ConfigError, ConfigLayer, ConfigOrigin, ConfigValue, }; +use crate::config::plain_info::PlainInfo; use crate::utils::files::get_bytes_from_os_str; use format_bytes::{write_bytes, DisplayBytes}; use std::collections::HashSet; @@ -27,6 +28,7 @@ #[derive(Clone)] pub struct Config { layers: Vec, + plain: PlainInfo, } impl DisplayBytes for Config { @@ -83,17 +85,55 @@ } } +/// Returns true if the config item is disabled by PLAIN or PLAINEXCEPT +fn should_ignore(plain: &PlainInfo, section: &[u8], item: &[u8]) -> bool { + // duplication with [_applyconfig] in [ui.py], + if !plain.is_plain() { + return false; + } + if section == b"alias" { + return plain.plainalias(); + } + if section == b"revsetalias" { + return plain.plainrevsetalias(); + } + if section == b"templatealias" { + return plain.plaintemplatealias(); + } + if section == b"ui" { + let to_delete: &[&[u8]] = &[ + b"debug", + b"fallbackencoding", + b"quiet", + b"slash", + b"logtemplate", + b"message-output", + b"statuscopies", + b"style", + b"traceback", + b"verbose", + ]; + return to_delete.contains(&item); + } + let sections_to_delete: &[&[u8]] = + &[b"defaults", b"commands", b"command-templates"]; + return sections_to_delete.contains(§ion); +} + impl Config { /// The configuration to use when printing configuration-loading errors pub fn empty() -> Self { - Self { layers: Vec::new() } + Self { + layers: Vec::new(), + plain: PlainInfo::empty(), + } } /// Load system and user configuration from various files. /// /// This is also affected by some environment variables. pub fn load_non_repo() -> Result { - let mut config = Self { layers: Vec::new() }; + let mut config = Self::empty(); let opt_rc_path = env::var_os("HGRCPATH"); // HGRCPATH replaces system config if opt_rc_path.is_none() { @@ -266,7 +306,10 @@ } } - Ok(Config { layers }) + Ok(Config { + layers, + plain: PlainInfo::empty(), + }) } /// Loads the per-repository config into a new `Config` which is combined @@ -283,6 +326,7 @@ let mut repo_config = Self { layers: other_layers, + plain: PlainInfo::empty(), }; for path in repo_config_files { // TODO: check if this file should be trusted: @@ -293,6 +337,10 @@ Ok(repo_config) } + pub fn apply_plain(&mut self, plain: PlainInfo) { + self.plain = plain; + } + fn get_parse<'config, T: 'config>( &'config self, section: &[u8], @@ -413,10 +461,25 @@ section: &[u8], item: &[u8], ) -> Option<(&ConfigLayer, &ConfigValue)> { + // Filter out the config items that are hidden by [PLAIN]. + // This differs from python hg where we delete them from the config. + let should_ignore = should_ignore(&self.plain, §ion, &item); for layer in self.layers.iter().rev() { if !layer.trusted { continue; } + //The [PLAIN] config should not affect the defaults. + // + // However, PLAIN should also affect the "tweaked" defaults (unless + // "tweakdefault" is part of "HGPLAINEXCEPT"). + // + // In practice the tweak-default layer is only added when it is + // relevant, so we can safely always take it into + // account here. + if should_ignore && !(layer.origin == ConfigOrigin::Tweakdefaults) + { + continue; + } if let Some(v) = layer.get(§ion, &item) { return Some((&layer, v)); } @@ -504,6 +567,38 @@ } res } + + // a config layer that's introduced by ui.tweakdefaults + fn tweakdefaults_layer() -> ConfigLayer { + let mut layer = ConfigLayer::new(ConfigOrigin::Tweakdefaults); + + let mut add = |section: &[u8], item: &[u8], value: &[u8]| { + layer.add( + section[..].into(), + item[..].into(), + value[..].into(), + None, + ); + }; + // duplication of [tweakrc] from [ui.py] + add(b"ui", b"rollback", b"False"); + add(b"ui", b"statuscopies", b"yes"); + add(b"ui", b"interface", b"curses"); + add(b"ui", b"relative-paths", b"yes"); + add(b"commands", b"grep.all-files", b"True"); + add(b"commands", b"update.check", b"noconflict"); + add(b"commands", b"status.verbose", b"True"); + add(b"commands", b"resolve.explicit-re-merge", b"True"); + add(b"git", b"git", b"1"); + add(b"git", b"showfunc", b"1"); + add(b"git", b"word-diff", b"1"); + return layer; + } + + // introduce the tweaked defaults as implied by ui.tweakdefaults + pub fn tweakdefaults<'a>(&mut self) -> () { + self.layers.insert(0, Config::tweakdefaults_layer()); + } } #[cfg(test)] diff -r 192949b68159 -r a3356ab610fc rust/hg-core/src/config/layer.rs --- a/rust/hg-core/src/config/layer.rs Thu Oct 20 12:05:17 2022 -0400 +++ b/rust/hg-core/src/config/layer.rs Mon Oct 24 15:32:14 2022 +0200 @@ -81,6 +81,7 @@ String::from_utf8_lossy(arg), ), CONFIG_PARSE_ERROR_ABORT, + None, ))? } } @@ -299,6 +300,8 @@ pub enum ConfigOrigin { /// From a configuration file File(PathBuf), + /// From [ui.tweakdefaults] + Tweakdefaults, /// From a `--config` CLI argument CommandLine, /// From a `--color` CLI argument @@ -321,6 +324,9 @@ ConfigOrigin::CommandLine => out.write_all(b"--config"), ConfigOrigin::CommandLineColor => out.write_all(b"--color"), ConfigOrigin::Environment(e) => write_bytes!(out, b"${}", e), + ConfigOrigin::Tweakdefaults => { + write_bytes!(out, b"ui.tweakdefaults") + } } } } diff -r 192949b68159 -r a3356ab610fc rust/hg-core/src/config/plain_info.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/hg-core/src/config/plain_info.rs Mon Oct 24 15:32:14 2022 +0200 @@ -0,0 +1,79 @@ +use crate::utils::files::get_bytes_from_os_string; +use std::env; + +/// Keeps information on whether plain mode is active. +/// +/// Plain mode means that all configuration variables which affect +/// the behavior and output of Mercurial should be +/// ignored. Additionally, the output should be stable, +/// reproducible and suitable for use in scripts or applications. +/// +/// The only way to trigger plain mode is by setting either the +/// `HGPLAIN' or `HGPLAINEXCEPT' environment variables. +/// +/// The return value can either be +/// - False if HGPLAIN is not set, or feature is in HGPLAINEXCEPT +/// - False if feature is disabled by default and not included in HGPLAIN +/// - True otherwise +#[derive(Clone)] +pub struct PlainInfo { + is_plain: bool, + except: Vec>, +} + +impl PlainInfo { + fn plain_except(except: Vec>) -> Self { + PlainInfo { + is_plain: true, + except, + } + } + + pub fn empty() -> PlainInfo { + PlainInfo { + is_plain: false, + except: vec![], + } + } + + pub fn from_env() -> PlainInfo { + if let Some(except) = env::var_os("HGPLAINEXCEPT") { + PlainInfo::plain_except( + get_bytes_from_os_string(except) + .split(|&byte| byte == b',') + .map(|x| x.to_vec()) + .collect(), + ) + } else { + PlainInfo { + is_plain: env::var_os("HGPLAIN").is_some(), + except: vec![], + } + } + } + + pub fn is_feature_plain(&self, feature: &str) -> bool { + return self.is_plain + && !self + .except + .iter() + .any(|exception| exception.as_slice() == feature.as_bytes()); + } + + pub fn is_plain(&self) -> bool { + self.is_plain + } + + pub fn plainalias(&self) -> bool { + self.is_feature_plain("alias") + } + pub fn plainrevsetalias(&self) -> bool { + self.is_feature_plain("revsetalias") + } + pub fn plaintemplatealias(&self) -> bool { + self.is_feature_plain("templatealias") + } + pub fn plaintweakdefaults(&self) -> bool { + self.is_feature_plain("tweakdefaults") + } +} diff -r 192949b68159 -r a3356ab610fc rust/hg-core/src/dirstate.rs --- a/rust/hg-core/src/dirstate.rs Thu Oct 20 12:05:17 2022 -0400 +++ b/rust/hg-core/src/dirstate.rs Mon Oct 24 15:32:14 2022 +0200 @@ -30,6 +30,10 @@ p1: NULL_NODE, p2: NULL_NODE, }; + + pub fn is_merge(&self) -> bool { + return !(self.p2 == NULL_NODE); + } } pub type StateMapIter<'a> = Box< diff -r 192949b68159 -r a3356ab610fc rust/hg-core/src/dirstate_tree/status.rs --- a/rust/hg-core/src/dirstate_tree/status.rs Thu Oct 20 12:05:17 2022 -0400 +++ b/rust/hg-core/src/dirstate_tree/status.rs Mon Oct 24 15:32:14 2022 +0200 @@ -20,6 +20,7 @@ use crate::StatusError; use crate::StatusOptions; use micro_timer::timed; +use once_cell::sync::OnceCell; use rayon::prelude::*; use sha1::{Digest, Sha1}; use std::borrow::Cow; @@ -126,14 +127,14 @@ }; let is_at_repo_root = true; let hg_path = &BorrowedPath::OnDisk(HgPath::new("")); - let has_ignored_ancestor = false; + let has_ignored_ancestor = HasIgnoredAncestor::create(None, hg_path); let root_cached_mtime = None; let root_dir_metadata = None; // If the path we have for the repository root is a symlink, do follow it. // (As opposed to symlinks within the working directory which are not // followed, using `std::fs::symlink_metadata`.) common.traverse_fs_directory_and_dirstate( - has_ignored_ancestor, + &has_ignored_ancestor, dmap.root.as_ref(), hg_path, &root_dir, @@ -196,6 +197,40 @@ Unsure, } +/// Lazy computation of whether a given path has a hgignored +/// ancestor. +struct HasIgnoredAncestor<'a> { + /// `path` and `parent` constitute the inputs to the computation, + /// `cache` stores the outcome. + path: &'a HgPath, + parent: Option<&'a HasIgnoredAncestor<'a>>, + cache: OnceCell, +} + +impl<'a> HasIgnoredAncestor<'a> { + fn create( + parent: Option<&'a HasIgnoredAncestor<'a>>, + path: &'a HgPath, + ) -> HasIgnoredAncestor<'a> { + Self { + path, + parent, + cache: OnceCell::new(), + } + } + + fn force<'b>(&self, ignore_fn: &IgnoreFnType<'b>) -> bool { + match self.parent { + None => false, + Some(parent) => { + *(parent.cache.get_or_init(|| { + parent.force(ignore_fn) || ignore_fn(&self.path) + })) + } + } + } +} + impl<'a, 'tree, 'on_disk> StatusCommon<'a, 'tree, 'on_disk> { fn push_outcome( &self, @@ -318,9 +353,9 @@ /// Returns whether all child entries of the filesystem directory have a /// corresponding dirstate node or are ignored. - fn traverse_fs_directory_and_dirstate( + fn traverse_fs_directory_and_dirstate<'ancestor>( &self, - has_ignored_ancestor: bool, + has_ignored_ancestor: &'ancestor HasIgnoredAncestor<'ancestor>, dirstate_nodes: ChildNodesRef<'tree, 'on_disk>, directory_hg_path: &BorrowedPath<'tree, 'on_disk>, directory_fs_path: &Path, @@ -418,7 +453,7 @@ } Right(fs_entry) => { has_dirstate_node_or_is_ignored = self.traverse_fs_only( - has_ignored_ancestor, + has_ignored_ancestor.force(&self.ignore_fn), directory_hg_path, fs_entry, ) @@ -429,12 +464,12 @@ .try_reduce(|| true, |a, b| Ok(a && b)) } - fn traverse_fs_and_dirstate( + fn traverse_fs_and_dirstate<'ancestor>( &self, fs_path: &Path, fs_metadata: &std::fs::Metadata, dirstate_node: NodeRef<'tree, 'on_disk>, - has_ignored_ancestor: bool, + has_ignored_ancestor: &'ancestor HasIgnoredAncestor<'ancestor>, ) -> Result<(), DirstateV2ParseError> { self.check_for_outdated_directory_cache(&dirstate_node)?; let hg_path = &dirstate_node.full_path_borrowed(self.dmap.on_disk)?; @@ -454,11 +489,14 @@ .traversed .push(hg_path.detach_from_tree()) } - let is_ignored = has_ignored_ancestor || (self.ignore_fn)(hg_path); + let is_ignored = HasIgnoredAncestor::create( + Some(&has_ignored_ancestor), + hg_path, + ); let is_at_repo_root = false; let children_all_have_dirstate_node_or_are_ignored = self .traverse_fs_directory_and_dirstate( - is_ignored, + &is_ignored, dirstate_node.children(self.dmap.on_disk)?, hg_path, fs_path, @@ -472,14 +510,14 @@ dirstate_node, )? } else { - if file_or_symlink && self.matcher.matches(hg_path) { + if file_or_symlink && self.matcher.matches(&hg_path) { if let Some(entry) = dirstate_node.entry()? { if !entry.any_tracked() { // Forward-compat if we start tracking unknown/ignored // files for caching reasons self.mark_unknown_or_ignored( - has_ignored_ancestor, - hg_path, + has_ignored_ancestor.force(&self.ignore_fn), + &hg_path, ); } if entry.added() { @@ -495,7 +533,7 @@ // `node.entry.is_none()` indicates a "directory" // node, but the filesystem has a file self.mark_unknown_or_ignored( - has_ignored_ancestor, + has_ignored_ancestor.force(&self.ignore_fn), hg_path, ); } diff -r 192949b68159 -r a3356ab610fc rust/hg-core/src/errors.rs --- a/rust/hg-core/src/errors.rs Thu Oct 20 12:05:17 2022 -0400 +++ b/rust/hg-core/src/errors.rs Mon Oct 24 15:32:14 2022 +0200 @@ -33,6 +33,7 @@ Abort { message: String, detailed_exit_code: exit_codes::ExitCode, + hint: Option, }, /// A configuration value is not in the expected syntax. @@ -82,10 +83,12 @@ pub fn abort( explanation: impl Into, exit_code: exit_codes::ExitCode, + hint: Option, ) -> Self { HgError::Abort { message: explanation.into(), detailed_exit_code: exit_code, + hint, } } } diff -r 192949b68159 -r a3356ab610fc rust/hg-core/src/exit_codes.rs --- a/rust/hg-core/src/exit_codes.rs Thu Oct 20 12:05:17 2022 -0400 +++ b/rust/hg-core/src/exit_codes.rs Mon Oct 24 15:32:14 2022 +0200 @@ -9,6 +9,10 @@ // Abort when there is a config related error pub const CONFIG_ERROR_ABORT: ExitCode = 30; +/// Indicates that the operation might work if retried in a different state. +/// Examples: Unresolved merge conflicts, unfinished operations +pub const STATE_ERROR: ExitCode = 20; + // Abort when there is an error while parsing config pub const CONFIG_PARSE_ERROR_ABORT: ExitCode = 10; diff -r 192949b68159 -r a3356ab610fc rust/hg-core/src/filepatterns.rs --- a/rust/hg-core/src/filepatterns.rs Thu Oct 20 12:05:17 2022 -0400 +++ b/rust/hg-core/src/filepatterns.rs Mon Oct 24 15:32:14 2022 +0200 @@ -314,6 +314,8 @@ m.insert(b"rootglob".as_ref(), b"rootglob:".as_ref()); m.insert(b"include".as_ref(), b"include:".as_ref()); m.insert(b"subinclude".as_ref(), b"subinclude:".as_ref()); + m.insert(b"path".as_ref(), b"path:".as_ref()); + m.insert(b"rootfilesin".as_ref(), b"rootfilesin:".as_ref()); m }; } @@ -329,6 +331,7 @@ pub fn parse_pattern_file_contents( lines: &[u8], file_path: &Path, + default_syntax_override: Option<&[u8]>, warn: bool, ) -> Result<(Vec, Vec), PatternError> { let comment_regex = Regex::new(r"((?:^|[^\\])(?:\\\\)*)#.*").unwrap(); @@ -338,7 +341,8 @@ let mut inputs: Vec = vec![]; let mut warnings: Vec = vec![]; - let mut current_syntax = b"relre:".as_ref(); + let mut current_syntax = + default_syntax_override.unwrap_or(b"relre:".as_ref()); for (line_number, mut line) in lines.split(|c| *c == b'\n').enumerate() { let line_number = line_number + 1; @@ -413,7 +417,7 @@ match std::fs::read(file_path) { Ok(contents) => { inspect_pattern_bytes(&contents); - parse_pattern_file_contents(&contents, file_path, warn) + parse_pattern_file_contents(&contents, file_path, None, warn) } Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(( vec![], @@ -601,9 +605,14 @@ let lines = b"syntax: glob\n*.elc"; assert_eq!( - parse_pattern_file_contents(lines, Path::new("file_path"), false) - .unwrap() - .0, + parse_pattern_file_contents( + lines, + Path::new("file_path"), + None, + false + ) + .unwrap() + .0, vec![IgnorePattern::new( PatternSyntax::RelGlob, b"*.elc", @@ -614,16 +623,26 @@ let lines = b"syntax: include\nsyntax: glob"; assert_eq!( - parse_pattern_file_contents(lines, Path::new("file_path"), false) - .unwrap() - .0, + parse_pattern_file_contents( + lines, + Path::new("file_path"), + None, + false + ) + .unwrap() + .0, vec![] ); let lines = b"glob:**.o"; assert_eq!( - parse_pattern_file_contents(lines, Path::new("file_path"), false) - .unwrap() - .0, + parse_pattern_file_contents( + lines, + Path::new("file_path"), + None, + false + ) + .unwrap() + .0, vec![IgnorePattern::new( PatternSyntax::RelGlob, b"**.o", diff -r 192949b68159 -r a3356ab610fc rust/hg-core/src/lib.rs --- a/rust/hg-core/src/lib.rs Thu Oct 20 12:05:17 2022 -0400 +++ b/rust/hg-core/src/lib.rs Mon Oct 24 15:32:14 2022 +0200 @@ -7,6 +7,8 @@ mod ancestors; pub mod dagops; pub mod errors; +pub mod narrow; +pub mod sparse; pub use ancestors::{AncestorsIterator, MissingAncestors}; pub mod dirstate; pub mod dirstate_tree; diff -r 192949b68159 -r a3356ab610fc rust/hg-core/src/matchers.rs --- a/rust/hg-core/src/matchers.rs Thu Oct 20 12:05:17 2022 -0400 +++ b/rust/hg-core/src/matchers.rs Mon Oct 24 15:32:14 2022 +0200 @@ -46,7 +46,7 @@ Recursive, } -pub trait Matcher { +pub trait Matcher: core::fmt::Debug { /// Explicitly listed files fn file_set(&self) -> Option<&HashSet>; /// Returns whether `filename` is in `file_set` @@ -283,6 +283,18 @@ parents: HashSet, } +impl core::fmt::Debug for IncludeMatcher<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("IncludeMatcher") + .field("patterns", &String::from_utf8_lossy(&self.patterns)) + .field("prefix", &self.prefix) + .field("roots", &self.roots) + .field("dirs", &self.dirs) + .field("parents", &self.parents) + .finish() + } +} + impl<'a> Matcher for IncludeMatcher<'a> { fn file_set(&self) -> Option<&HashSet> { None @@ -330,6 +342,7 @@ } /// The union of multiple matchers. Will match if any of the matchers match. +#[derive(Debug)] pub struct UnionMatcher { matchers: Vec>, } @@ -393,6 +406,7 @@ } } +#[derive(Debug)] pub struct IntersectionMatcher { m1: Box, m2: Box, @@ -474,6 +488,91 @@ } } +#[derive(Debug)] +pub struct DifferenceMatcher { + base: Box, + excluded: Box, + files: Option>, +} + +impl Matcher for DifferenceMatcher { + fn file_set(&self) -> Option<&HashSet> { + self.files.as_ref() + } + + fn exact_match(&self, filename: &HgPath) -> bool { + self.files.as_ref().map_or(false, |f| f.contains(filename)) + } + + fn matches(&self, filename: &HgPath) -> bool { + self.base.matches(filename) && !self.excluded.matches(filename) + } + + fn visit_children_set(&self, directory: &HgPath) -> VisitChildrenSet { + let excluded_set = self.excluded.visit_children_set(directory); + if excluded_set == VisitChildrenSet::Recursive { + return VisitChildrenSet::Empty; + } + let base_set = self.base.visit_children_set(directory); + // Possible values for base: 'recursive', 'this', set(...), set() + // Possible values for excluded: 'this', set(...), set() + // If excluded has nothing under here that we care about, return base, + // even if it's 'recursive'. + if excluded_set == VisitChildrenSet::Empty { + return base_set; + } + match base_set { + VisitChildrenSet::This | VisitChildrenSet::Recursive => { + // Never return 'recursive' here if excluded_set is any kind of + // non-empty (either 'this' or set(foo)), since excluded might + // return set() for a subdirectory. + VisitChildrenSet::This + } + set => { + // Possible values for base: set(...), set() + // Possible values for excluded: 'this', set(...) + // We ignore excluded set results. They're possibly incorrect: + // base = path:dir/subdir + // excluded=rootfilesin:dir, + // visit_children_set(''): + // base returns {'dir'}, excluded returns {'dir'}, if we + // subtracted we'd return set(), which is *not* correct, we + // still need to visit 'dir'! + set + } + } + } + + fn matches_everything(&self) -> bool { + false + } + + fn is_exact(&self) -> bool { + self.base.is_exact() + } +} + +impl DifferenceMatcher { + pub fn new( + base: Box, + excluded: Box, + ) -> Self { + let base_is_exact = base.is_exact(); + let base_files = base.file_set().map(ToOwned::to_owned); + let mut new = Self { + base, + excluded, + files: None, + }; + if base_is_exact { + new.files = base_files.map(|files| { + files.iter().cloned().filter(|f| new.matches(f)).collect() + }); + } + new + } +} + /// Returns a function that matches an `HgPath` against the given regex /// pattern. /// @@ -1489,4 +1588,101 @@ VisitChildrenSet::Empty ); } + + #[test] + fn test_differencematcher() { + // Two alwaysmatchers should function like a nevermatcher + let m1 = AlwaysMatcher; + let m2 = AlwaysMatcher; + let matcher = DifferenceMatcher::new(Box::new(m1), Box::new(m2)); + + for case in &[ + &b""[..], + b"dir", + b"dir/subdir", + b"dir/subdir/z", + b"dir/foo", + b"dir/subdir/x", + b"folder", + ] { + assert_eq!( + matcher.visit_children_set(HgPath::new(case)), + VisitChildrenSet::Empty + ); + } + + // One always and one never should behave the same as an always + let m1 = AlwaysMatcher; + let m2 = NeverMatcher; + let matcher = DifferenceMatcher::new(Box::new(m1), Box::new(m2)); + + for case in &[ + &b""[..], + b"dir", + b"dir/subdir", + b"dir/subdir/z", + b"dir/foo", + b"dir/subdir/x", + b"folder", + ] { + assert_eq!( + matcher.visit_children_set(HgPath::new(case)), + VisitChildrenSet::Recursive + ); + } + + // Two include matchers + let m1 = Box::new( + IncludeMatcher::new(vec![IgnorePattern::new( + PatternSyntax::RelPath, + b"dir/subdir", + Path::new("/repo"), + )]) + .unwrap(), + ); + let m2 = Box::new( + IncludeMatcher::new(vec![IgnorePattern::new( + PatternSyntax::RootFiles, + b"dir", + Path::new("/repo"), + )]) + .unwrap(), + ); + + let matcher = DifferenceMatcher::new(m1, m2); + + let mut set = HashSet::new(); + set.insert(HgPathBuf::from_bytes(b"dir")); + assert_eq!( + matcher.visit_children_set(HgPath::new(b"")), + VisitChildrenSet::Set(set) + ); + + let mut set = HashSet::new(); + set.insert(HgPathBuf::from_bytes(b"subdir")); + assert_eq!( + matcher.visit_children_set(HgPath::new(b"dir")), + VisitChildrenSet::Set(set) + ); + assert_eq!( + matcher.visit_children_set(HgPath::new(b"dir/subdir")), + VisitChildrenSet::Recursive + ); + assert_eq!( + matcher.visit_children_set(HgPath::new(b"dir/foo")), + VisitChildrenSet::Empty + ); + assert_eq!( + matcher.visit_children_set(HgPath::new(b"folder")), + VisitChildrenSet::Empty + ); + assert_eq!( + matcher.visit_children_set(HgPath::new(b"dir/subdir/z")), + VisitChildrenSet::This + ); + assert_eq!( + matcher.visit_children_set(HgPath::new(b"dir/subdir/x")), + VisitChildrenSet::This + ); + } } diff -r 192949b68159 -r a3356ab610fc rust/hg-core/src/narrow.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/hg-core/src/narrow.rs Mon Oct 24 15:32:14 2022 +0200 @@ -0,0 +1,111 @@ +use std::path::Path; + +use crate::{ + errors::HgError, + exit_codes, + filepatterns::parse_pattern_file_contents, + matchers::{ + AlwaysMatcher, DifferenceMatcher, IncludeMatcher, Matcher, + NeverMatcher, + }, + repo::Repo, + requirements::NARROW_REQUIREMENT, + sparse::{self, SparseConfigError, SparseWarning}, +}; + +/// The file in .hg/store/ that indicates which paths exit in the store +const FILENAME: &str = "narrowspec"; +/// The file in .hg/ that indicates which paths exit in the dirstate +const DIRSTATE_FILENAME: &str = "narrowspec.dirstate"; + +/// Pattern prefixes that are allowed in narrow patterns. This list MUST +/// only contain patterns that are fast and safe to evaluate. Keep in mind +/// that patterns are supplied by clients and executed on remote servers +/// as part of wire protocol commands. That means that changes to this +/// data structure influence the wire protocol and should not be taken +/// lightly - especially removals. +const VALID_PREFIXES: [&str; 2] = ["path:", "rootfilesin:"]; + +/// Return the matcher for the current narrow spec, and all configuration +/// warnings to display. +pub fn matcher( + repo: &Repo, +) -> Result<(Box, Vec), SparseConfigError> { + let mut warnings = vec![]; + if !repo.requirements().contains(NARROW_REQUIREMENT) { + return Ok((Box::new(AlwaysMatcher), warnings)); + } + // Treat "narrowspec does not exist" the same as "narrowspec file exists + // and is empty". + let store_spec = repo.store_vfs().try_read(FILENAME)?.unwrap_or(vec![]); + let working_copy_spec = + repo.hg_vfs().try_read(DIRSTATE_FILENAME)?.unwrap_or(vec![]); + if store_spec != working_copy_spec { + return Err(HgError::abort( + "working copy's narrowspec is stale", + exit_codes::STATE_ERROR, + Some("run 'hg tracked --update-working-copy'".into()), + ) + .into()); + } + + let config = sparse::parse_config( + &store_spec, + sparse::SparseConfigContext::Narrow, + )?; + + warnings.extend(config.warnings); + + if !config.profiles.is_empty() { + // TODO (from Python impl) maybe do something with profiles? + return Err(SparseConfigError::IncludesInNarrow); + } + validate_patterns(&config.includes)?; + validate_patterns(&config.excludes)?; + + if config.includes.is_empty() { + return Ok((Box::new(NeverMatcher), warnings)); + } + + let (patterns, subwarnings) = parse_pattern_file_contents( + &config.includes, + Path::new(""), + None, + false, + )?; + warnings.extend(subwarnings.into_iter().map(From::from)); + + let mut m: Box = + Box::new(IncludeMatcher::new(patterns)?); + + let (patterns, subwarnings) = parse_pattern_file_contents( + &config.excludes, + Path::new(""), + None, + false, + )?; + if !patterns.is_empty() { + warnings.extend(subwarnings.into_iter().map(From::from)); + let exclude_matcher = Box::new(IncludeMatcher::new(patterns)?); + m = Box::new(DifferenceMatcher::new(m, exclude_matcher)); + } + + Ok((m, warnings)) +} + +fn validate_patterns(patterns: &[u8]) -> Result<(), SparseConfigError> { + for pattern in patterns.split(|c| *c == b'\n') { + if pattern.is_empty() { + continue; + } + for prefix in VALID_PREFIXES.iter() { + if pattern.starts_with(prefix.as_bytes()) { + break; + } + return Err(SparseConfigError::InvalidNarrowPrefix( + pattern.to_owned(), + )); + } + } + Ok(()) +} diff -r 192949b68159 -r a3356ab610fc rust/hg-core/src/revlog/filelog.rs --- a/rust/hg-core/src/revlog/filelog.rs Thu Oct 20 12:05:17 2022 -0400 +++ b/rust/hg-core/src/revlog/filelog.rs Mon Oct 24 15:32:14 2022 +0200 @@ -17,18 +17,21 @@ } impl Filelog { - pub fn open(repo: &Repo, file_path: &HgPath) -> Result { + pub fn open_vfs( + store_vfs: &crate::vfs::Vfs<'_>, + file_path: &HgPath, + ) -> Result { let index_path = store_path(file_path, b".i"); let data_path = store_path(file_path, b".d"); - let revlog = Revlog::open( - &repo.store_vfs(), - index_path, - Some(&data_path), - false, - )?; + let revlog = + Revlog::open(store_vfs, index_path, Some(&data_path), false)?; Ok(Self { revlog }) } + pub fn open(repo: &Repo, file_path: &HgPath) -> Result { + Self::open_vfs(&repo.store_vfs(), file_path) + } + /// The given node ID is that of the file as found in a filelog, not of a /// changeset. pub fn data_for_node( diff -r 192949b68159 -r a3356ab610fc rust/hg-core/src/sparse.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/hg-core/src/sparse.rs Mon Oct 24 15:32:14 2022 +0200 @@ -0,0 +1,338 @@ +use std::{collections::HashSet, path::Path}; + +use format_bytes::{write_bytes, DisplayBytes}; + +use crate::{ + errors::HgError, + filepatterns::parse_pattern_file_contents, + matchers::{ + AlwaysMatcher, DifferenceMatcher, IncludeMatcher, Matcher, + UnionMatcher, + }, + operations::cat, + repo::Repo, + requirements::SPARSE_REQUIREMENT, + utils::{hg_path::HgPath, SliceExt}, + IgnorePattern, PatternError, PatternFileWarning, PatternSyntax, Revision, + NULL_REVISION, +}; + +/// Command which is triggering the config read +#[derive(Copy, Clone, Debug)] +pub enum SparseConfigContext { + Sparse, + Narrow, +} + +impl DisplayBytes for SparseConfigContext { + fn display_bytes( + &self, + output: &mut dyn std::io::Write, + ) -> std::io::Result<()> { + match self { + SparseConfigContext::Sparse => write_bytes!(output, b"sparse"), + SparseConfigContext::Narrow => write_bytes!(output, b"narrow"), + } + } +} + +/// Possible warnings when reading sparse configuration +#[derive(Debug, derive_more::From)] +pub enum SparseWarning { + /// Warns about improper paths that start with "/" + RootWarning { + context: SparseConfigContext, + line: Vec, + }, + /// Warns about a profile missing from the given changelog revision + ProfileNotFound { profile: Vec, rev: Revision }, + #[from] + Pattern(PatternFileWarning), +} + +/// Parsed sparse config +#[derive(Debug, Default)] +pub struct SparseConfig { + // Line-separated + pub(crate) includes: Vec, + // Line-separated + pub(crate) excludes: Vec, + pub(crate) profiles: HashSet>, + pub(crate) warnings: Vec, +} + +/// All possible errors when reading sparse/narrow config +#[derive(Debug, derive_more::From)] +pub enum SparseConfigError { + IncludesAfterExcludes { + context: SparseConfigContext, + }, + EntryOutsideSection { + context: SparseConfigContext, + line: Vec, + }, + /// Narrow config does not support '%include' directives + IncludesInNarrow, + /// An invalid pattern prefix was given to the narrow spec. Includes the + /// entire pattern for context. + InvalidNarrowPrefix(Vec), + #[from] + HgError(HgError), + #[from] + PatternError(PatternError), +} + +/// Parse sparse config file content. +pub(crate) fn parse_config( + raw: &[u8], + context: SparseConfigContext, +) -> Result { + let mut includes = vec![]; + let mut excludes = vec![]; + let mut profiles = HashSet::new(); + let mut warnings = vec![]; + + #[derive(PartialEq, Eq)] + enum Current { + Includes, + Excludes, + None, + } + + let mut current = Current::None; + let mut in_section = false; + + for line in raw.split(|c| *c == b'\n') { + let line = line.trim(); + if line.is_empty() || line[0] == b'#' { + // empty or comment line, skip + continue; + } + if line.starts_with(b"%include ") { + let profile = line[b"%include ".len()..].trim(); + if !profile.is_empty() { + profiles.insert(profile.into()); + } + } else if line == b"[include]" { + if in_section && current == Current::Includes { + return Err(SparseConfigError::IncludesAfterExcludes { + context, + }); + } + in_section = true; + current = Current::Includes; + continue; + } else if line == b"[exclude]" { + in_section = true; + current = Current::Excludes; + } else { + if current == Current::None { + return Err(SparseConfigError::EntryOutsideSection { + context, + line: line.into(), + }); + } + if line.trim().starts_with(b"/") { + warnings.push(SparseWarning::RootWarning { + context, + line: line.into(), + }); + continue; + } + match current { + Current::Includes => { + includes.push(b'\n'); + includes.extend(line.iter()); + } + Current::Excludes => { + excludes.push(b'\n'); + excludes.extend(line.iter()); + } + Current::None => unreachable!(), + } + } + } + + Ok(SparseConfig { + includes, + excludes, + profiles, + warnings, + }) +} + +fn read_temporary_includes( + repo: &Repo, +) -> Result>, SparseConfigError> { + let raw = repo.hg_vfs().try_read("tempsparse")?.unwrap_or(vec![]); + if raw.is_empty() { + return Ok(vec![]); + } + Ok(raw.split(|c| *c == b'\n').map(ToOwned::to_owned).collect()) +} + +/// Obtain sparse checkout patterns for the given revision +fn patterns_for_rev( + repo: &Repo, + rev: Revision, +) -> Result, SparseConfigError> { + if !repo.has_sparse() { + return Ok(None); + } + let raw = repo.hg_vfs().try_read("sparse")?.unwrap_or(vec![]); + + if raw.is_empty() { + return Ok(None); + } + + let mut config = parse_config(&raw, SparseConfigContext::Sparse)?; + + if !config.profiles.is_empty() { + let mut profiles: Vec> = config.profiles.into_iter().collect(); + let mut visited = HashSet::new(); + + while let Some(profile) = profiles.pop() { + if visited.contains(&profile) { + continue; + } + visited.insert(profile.to_owned()); + + let output = + cat(repo, &rev.to_string(), vec![HgPath::new(&profile)]) + .map_err(|_| { + HgError::corrupted(format!( + "dirstate points to non-existent parent node" + )) + })?; + if output.results.is_empty() { + config.warnings.push(SparseWarning::ProfileNotFound { + profile: profile.to_owned(), + rev, + }) + } + + let subconfig = parse_config( + &output.results[0].1, + SparseConfigContext::Sparse, + )?; + if !subconfig.includes.is_empty() { + config.includes.push(b'\n'); + config.includes.extend(&subconfig.includes); + } + if !subconfig.includes.is_empty() { + config.includes.push(b'\n'); + config.excludes.extend(&subconfig.excludes); + } + config.warnings.extend(subconfig.warnings.into_iter()); + profiles.extend(subconfig.profiles.into_iter()); + } + + config.profiles = visited; + } + + if !config.includes.is_empty() { + config.includes.extend(b"\n.hg*"); + } + + Ok(Some(config)) +} + +/// Obtain a matcher for sparse working directories. +pub fn matcher( + repo: &Repo, +) -> Result<(Box, Vec), SparseConfigError> { + let mut warnings = vec![]; + if !repo.requirements().contains(SPARSE_REQUIREMENT) { + return Ok((Box::new(AlwaysMatcher), warnings)); + } + + let parents = repo.dirstate_parents()?; + let mut revs = vec![]; + let p1_rev = + repo.changelog()? + .rev_from_node(parents.p1.into()) + .map_err(|_| { + HgError::corrupted(format!( + "dirstate points to non-existent parent node" + )) + })?; + if p1_rev != NULL_REVISION { + revs.push(p1_rev) + } + let p2_rev = + repo.changelog()? + .rev_from_node(parents.p2.into()) + .map_err(|_| { + HgError::corrupted(format!( + "dirstate points to non-existent parent node" + )) + })?; + if p2_rev != NULL_REVISION { + revs.push(p2_rev) + } + let mut matchers = vec![]; + + for rev in revs.iter() { + let config = patterns_for_rev(repo, *rev); + if let Ok(Some(config)) = config { + warnings.extend(config.warnings); + let mut m: Box = Box::new(AlwaysMatcher); + if !config.includes.is_empty() { + let (patterns, subwarnings) = parse_pattern_file_contents( + &config.includes, + Path::new(""), + Some(b"relglob:".as_ref()), + false, + )?; + warnings.extend(subwarnings.into_iter().map(From::from)); + m = Box::new(IncludeMatcher::new(patterns)?); + } + if !config.excludes.is_empty() { + let (patterns, subwarnings) = parse_pattern_file_contents( + &config.excludes, + Path::new(""), + Some(b"relglob:".as_ref()), + false, + )?; + warnings.extend(subwarnings.into_iter().map(From::from)); + m = Box::new(DifferenceMatcher::new( + m, + Box::new(IncludeMatcher::new(patterns)?), + )); + } + matchers.push(m); + } + } + let result: Box = match matchers.len() { + 0 => Box::new(AlwaysMatcher), + 1 => matchers.pop().expect("1 is equal to 0"), + _ => Box::new(UnionMatcher::new(matchers)), + }; + + let matcher = + force_include_matcher(result, &read_temporary_includes(repo)?)?; + Ok((matcher, warnings)) +} + +/// Returns a matcher that returns true for any of the forced includes before +/// testing against the actual matcher +fn force_include_matcher( + result: Box, + temp_includes: &[Vec], +) -> Result, PatternError> { + if temp_includes.is_empty() { + return Ok(result); + } + let forced_include_matcher = IncludeMatcher::new( + temp_includes + .into_iter() + .map(|include| { + IgnorePattern::new(PatternSyntax::Path, include, Path::new("")) + }) + .collect(), + )?; + Ok(Box::new(UnionMatcher::new(vec![ + Box::new(forced_include_matcher), + result, + ]))) +} diff -r 192949b68159 -r a3356ab610fc rust/hg-core/src/vfs.rs --- a/rust/hg-core/src/vfs.rs Thu Oct 20 12:05:17 2022 -0400 +++ b/rust/hg-core/src/vfs.rs Mon Oct 24 15:32:14 2022 +0200 @@ -40,6 +40,23 @@ std::fs::read(&path).when_reading_file(&path) } + /// Returns `Ok(None)` if the file does not exist. + pub fn try_read( + &self, + relative_path: impl AsRef, + ) -> Result>, HgError> { + match self.read(relative_path) { + Err(e) => match &e { + HgError::IoError { error, .. } => match error.kind() { + ErrorKind::NotFound => return Ok(None), + _ => Err(e), + }, + _ => Err(e), + }, + Ok(v) => Ok(Some(v)), + } + } + fn mmap_open_gen( &self, relative_path: impl AsRef, diff -r 192949b68159 -r a3356ab610fc rust/hg-cpython/src/dirstate/status.rs --- a/rust/hg-cpython/src/dirstate/status.rs Thu Oct 20 12:05:17 2022 -0400 +++ b/rust/hg-cpython/src/dirstate/status.rs Mon Oct 24 15:32:14 2022 +0200 @@ -15,7 +15,10 @@ PyResult, PyTuple, Python, PythonObject, ToPyObject, }; use hg::dirstate::status::StatusPath; -use hg::matchers::{IntersectionMatcher, Matcher, NeverMatcher, UnionMatcher}; +use hg::matchers::{ + DifferenceMatcher, IntersectionMatcher, Matcher, NeverMatcher, + UnionMatcher, +}; use hg::{ matchers::{AlwaysMatcher, FileMatcher, IncludeMatcher}, parse_pattern_syntax, @@ -233,6 +236,12 @@ Ok(Box::new(IntersectionMatcher::new(m1, m2))) } + "differencematcher" => { + let m1 = extract_matcher(py, matcher.getattr(py, "_m1")?)?; + let m2 = extract_matcher(py, matcher.getattr(py, "_m2")?)?; + + Ok(Box::new(DifferenceMatcher::new(m1, m2))) + } e => Err(PyErr::new::( py, format!("Unsupported matcher {}", e), diff -r 192949b68159 -r a3356ab610fc rust/rhg/Cargo.toml --- a/rust/rhg/Cargo.toml Thu Oct 20 12:05:17 2022 -0400 +++ b/rust/rhg/Cargo.toml Mon Oct 24 15:32:14 2022 +0200 @@ -22,3 +22,4 @@ format-bytes = "0.3.0" users = "0.11.0" which = "4.2.5" +rayon = "1.5.1" diff -r 192949b68159 -r a3356ab610fc rust/rhg/src/commands/debugdata.rs --- a/rust/rhg/src/commands/debugdata.rs Thu Oct 20 12:05:17 2022 -0400 +++ b/rust/rhg/src/commands/debugdata.rs Mon Oct 24 15:32:14 2022 +0200 @@ -55,6 +55,11 @@ }; let repo = invocation.repo?; + if repo.has_narrow() { + return Err(CommandError::unsupported( + "support for ellipsis nodes is missing and repo has narrow enabled", + )); + } let data = debug_data(repo, rev, kind).map_err(|e| (e, rev))?; let mut stdout = invocation.ui.stdout_buffer(); diff -r 192949b68159 -r a3356ab610fc rust/rhg/src/commands/debugrhgsparse.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/rhg/src/commands/debugrhgsparse.rs Mon Oct 24 15:32:14 2022 +0200 @@ -0,0 +1,43 @@ +use std::os::unix::prelude::OsStrExt; + +use crate::error::CommandError; +use clap::SubCommand; +use hg::{self, utils::hg_path::HgPath}; + +pub const HELP_TEXT: &str = ""; + +pub fn args() -> clap::App<'static, 'static> { + SubCommand::with_name("debugrhgsparse") + .arg( + clap::Arg::with_name("files") + .required(true) + .multiple(true) + .empty_values(false) + .value_name("FILES") + .help("Files to check against sparse profile"), + ) + .about(HELP_TEXT) +} + +pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> { + let repo = invocation.repo?; + + let (matcher, _warnings) = hg::sparse::matcher(&repo).unwrap(); + let files = invocation.subcommand_args.values_of_os("files"); + if let Some(files) = files { + for file in files { + invocation.ui.write_stdout(b"matches: ")?; + invocation.ui.write_stdout( + if matcher.matches(HgPath::new(file.as_bytes())) { + b"yes" + } else { + b"no" + }, + )?; + invocation.ui.write_stdout(b" | file: ")?; + invocation.ui.write_stdout(file.as_bytes())?; + invocation.ui.write_stdout(b"\n")?; + } + } + Ok(()) +} diff -r 192949b68159 -r a3356ab610fc rust/rhg/src/commands/status.rs --- a/rust/rhg/src/commands/status.rs Thu Oct 20 12:05:17 2022 -0400 +++ b/rust/rhg/src/commands/status.rs Mon Oct 24 15:32:14 2022 +0200 @@ -10,7 +10,6 @@ use crate::utils::path_utils::RelativizePaths; use clap::{Arg, SubCommand}; use format_bytes::format_bytes; -use hg; use hg::config::Config; use hg::dirstate::has_exec_bit; use hg::dirstate::status::StatusPath; @@ -18,7 +17,7 @@ use hg::errors::{HgError, IoResultExt}; use hg::lock::LockError; use hg::manifest::Manifest; -use hg::matchers::AlwaysMatcher; +use hg::matchers::{AlwaysMatcher, IntersectionMatcher}; use hg::repo::Repo; use hg::utils::files::get_bytes_from_os_string; use hg::utils::files::get_bytes_from_path; @@ -28,7 +27,9 @@ use hg::PatternFileWarning; use hg::StatusError; use hg::StatusOptions; +use hg::{self, narrow, sparse}; use log::info; +use rayon::prelude::*; use std::io; use std::path::PathBuf; @@ -104,6 +105,12 @@ .short("-n") .long("--no-status"), ) + .arg( + Arg::with_name("verbose") + .help("enable additional output") + .short("-v") + .long("--verbose"), + ) } /// Pure data type allowing the caller to specify file states to display @@ -150,18 +157,35 @@ } } +fn has_unfinished_merge(repo: &Repo) -> Result { + return Ok(repo.dirstate_parents()?.is_merge()); +} + +fn has_unfinished_state(repo: &Repo) -> Result { + // These are all the known values for the [fname] argument of + // [addunfinished] function in [state.py] + let known_state_files: &[&str] = &[ + "bisect.state", + "graftstate", + "histedit-state", + "rebasestate", + "shelvedstate", + "transplant/journal", + "updatestate", + ]; + if has_unfinished_merge(repo)? { + return Ok(true); + }; + for f in known_state_files { + if repo.hg_vfs().join(f).exists() { + return Ok(true); + } + } + return Ok(false); +} + pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> { // TODO: lift these limitations - if invocation.config.get_bool(b"ui", b"tweakdefaults")? { - return Err(CommandError::unsupported( - "ui.tweakdefaults is not yet supported with rhg status", - )); - } - if invocation.config.get_bool(b"ui", b"statuscopies")? { - return Err(CommandError::unsupported( - "ui.statuscopies is not yet supported with rhg status", - )); - } if invocation .config .get(b"commands", b"status.terse") @@ -176,15 +200,10 @@ let config = invocation.config; let args = invocation.subcommand_args; - let verbose = !ui.plain(None) - && !args.is_present("print0") - && (config.get_bool(b"ui", b"verbose")? + let verbose = !args.is_present("print0") + && (args.is_present("verbose") + || config.get_bool(b"ui", b"verbose")? || config.get_bool(b"commands", b"status.verbose")?); - if verbose { - return Err(CommandError::unsupported( - "verbose status is not supported yet", - )); - } let all = args.is_present("all"); let display_states = if all { @@ -214,10 +233,12 @@ let repo = invocation.repo?; - if repo.has_sparse() || repo.has_narrow() { - return Err(CommandError::unsupported( - "rhg status is not supported for sparse checkouts or narrow clones yet" - )); + if verbose { + if has_unfinished_state(repo)? { + return Err(CommandError::unsupported( + "verbose status output is not supported by rhg (and is needed because we're in an unfinished operation)", + )); + }; } let mut dmap = repo.dirstate_map_mut()?; @@ -239,28 +260,7 @@ let after_status = |res: StatusResult| -> Result<_, CommandError> { let (mut ds_status, pattern_warnings) = res?; for warning in pattern_warnings { - match warning { - hg::PatternFileWarning::InvalidSyntax(path, syntax) => ui - .write_stderr(&format_bytes!( - b"{}: ignoring invalid syntax '{}'\n", - get_bytes_from_path(path), - &*syntax - ))?, - hg::PatternFileWarning::NoSuchFile(path) => { - let path = if let Ok(relative) = - path.strip_prefix(repo.working_directory_path()) - { - relative - } else { - &*path - }; - ui.write_stderr(&format_bytes!( - b"skipping unreadable pattern file '{}': \ - No such file or directory\n", - get_bytes_from_path(path), - ))? - } - } + ui.write_stderr(&print_pattern_file_warning(&warning, &repo))?; } for (path, error) in ds_status.bad { @@ -292,23 +292,37 @@ let manifest = repo.manifest_for_node(p1).map_err(|e| { CommandError::from((e, &*format!("{:x}", p1.short()))) })?; - for to_check in ds_status.unsure { - if unsure_is_modified(repo, &manifest, &to_check.path)? { + let working_directory_vfs = repo.working_directory_vfs(); + let store_vfs = repo.store_vfs(); + let res: Vec<_> = ds_status + .unsure + .into_par_iter() + .map(|to_check| { + unsure_is_modified( + working_directory_vfs, + store_vfs, + &manifest, + &to_check.path, + ) + .map(|modified| (to_check, modified)) + }) + .collect::>()?; + for (status_path, is_modified) in res.into_iter() { + if is_modified { if display_states.modified { - ds_status.modified.push(to_check); + ds_status.modified.push(status_path); } } else { if display_states.clean { - ds_status.clean.push(to_check.clone()); + ds_status.clean.push(status_path.clone()); } - fixup.push(to_check.path.into_owned()) + fixup.push(status_path.path.into_owned()) } } } - let relative_paths = (!ui.plain(None)) - && config - .get_option(b"commands", b"status.relative")? - .unwrap_or(config.get_bool(b"ui", b"relative-paths")?); + let relative_paths = config + .get_option(b"commands", b"status.relative")? + .unwrap_or(config.get_bool(b"ui", b"relative-paths")?); let output = DisplayStatusPaths { ui, no_status, @@ -350,9 +364,45 @@ filesystem_time_at_status_start, )) }; + let (narrow_matcher, narrow_warnings) = narrow::matcher(repo)?; + let (sparse_matcher, sparse_warnings) = sparse::matcher(repo)?; + let matcher = match (repo.has_narrow(), repo.has_sparse()) { + (true, true) => { + Box::new(IntersectionMatcher::new(narrow_matcher, sparse_matcher)) + } + (true, false) => narrow_matcher, + (false, true) => sparse_matcher, + (false, false) => Box::new(AlwaysMatcher), + }; + + for warning in narrow_warnings.into_iter().chain(sparse_warnings) { + match &warning { + sparse::SparseWarning::RootWarning { context, line } => { + let msg = format_bytes!( + b"warning: {} profile cannot use paths \" + starting with /, ignoring {}\n", + context, + line + ); + ui.write_stderr(&msg)?; + } + sparse::SparseWarning::ProfileNotFound { profile, rev } => { + let msg = format_bytes!( + b"warning: sparse profile '{}' not found \" + in rev {} - ignoring it\n", + profile, + rev + ); + ui.write_stderr(&msg)?; + } + sparse::SparseWarning::Pattern(e) => { + ui.write_stderr(&print_pattern_file_warning(e, &repo))?; + } + } + } let (fixup, mut dirstate_write_needed, filesystem_time_at_status_start) = dmap.with_status( - &AlwaysMatcher, + matcher.as_ref(), repo.working_directory_path().to_owned(), ignore_files(repo, config), options, @@ -491,11 +541,12 @@ /// This meant to be used for those that the dirstate cannot resolve, due /// to time resolution limits. fn unsure_is_modified( - repo: &Repo, + working_directory_vfs: hg::vfs::Vfs, + store_vfs: hg::vfs::Vfs, manifest: &Manifest, hg_path: &HgPath, ) -> Result { - let vfs = repo.working_directory_vfs(); + let vfs = working_directory_vfs; let fs_path = hg_path_to_path_buf(hg_path).expect("HgPath conversion"); let fs_metadata = vfs.symlink_metadata(&fs_path)?; let is_symlink = fs_metadata.file_type().is_symlink(); @@ -515,7 +566,7 @@ if entry.flags != fs_flags { return Ok(true); } - let filelog = repo.filelog(hg_path)?; + let filelog = hg::filelog::Filelog::open_vfs(&store_vfs, hg_path)?; let fs_len = fs_metadata.len(); let file_node = entry.node_id()?; let filelog_entry = filelog.entry_for_node(file_node).map_err(|_| { @@ -545,3 +596,30 @@ }; Ok(p1_contents != &*fs_contents) } + +fn print_pattern_file_warning( + warning: &PatternFileWarning, + repo: &Repo, +) -> Vec { + match warning { + PatternFileWarning::InvalidSyntax(path, syntax) => format_bytes!( + b"{}: ignoring invalid syntax '{}'\n", + get_bytes_from_path(path), + &*syntax + ), + PatternFileWarning::NoSuchFile(path) => { + let path = if let Ok(relative) = + path.strip_prefix(repo.working_directory_path()) + { + relative + } else { + &*path + }; + format_bytes!( + b"skipping unreadable pattern file '{}': \ + No such file or directory\n", + get_bytes_from_path(path), + ) + } + } +} diff -r 192949b68159 -r a3356ab610fc rust/rhg/src/error.rs --- a/rust/rhg/src/error.rs Thu Oct 20 12:05:17 2022 -0400 +++ b/rust/rhg/src/error.rs Mon Oct 24 15:32:14 2022 +0200 @@ -8,6 +8,7 @@ use hg::exit_codes; use hg::repo::RepoError; use hg::revlog::revlog::RevlogError; +use hg::sparse::SparseConfigError; use hg::utils::files::get_bytes_from_path; use hg::{DirstateError, DirstateMapError, StatusError}; use std::convert::From; @@ -19,6 +20,7 @@ Abort { message: Vec, detailed_exit_code: exit_codes::ExitCode, + hint: Option>, }, /// Exit with a failure exit code but no message. @@ -49,6 +51,32 @@ // https://www.mercurial-scm.org/wiki/EncodingStrategy#Mixing_output message: utf8_to_local(message.as_ref()).into(), detailed_exit_code: detailed_exit_code, + hint: None, + } + } + + pub fn abort_with_exit_code_and_hint( + message: impl AsRef, + detailed_exit_code: exit_codes::ExitCode, + hint: Option>, + ) -> Self { + CommandError::Abort { + message: utf8_to_local(message.as_ref()).into(), + detailed_exit_code, + hint: hint.map(|h| utf8_to_local(h.as_ref()).into()), + } + } + + pub fn abort_with_exit_code_bytes( + message: impl AsRef<[u8]>, + detailed_exit_code: exit_codes::ExitCode, + ) -> Self { + // TODO: use this everywhere it makes sense instead of the string + // version. + CommandError::Abort { + message: message.as_ref().into(), + detailed_exit_code, + hint: None, } } @@ -79,9 +107,12 @@ HgError::Abort { message, detailed_exit_code, - } => { - CommandError::abort_with_exit_code(message, detailed_exit_code) - } + hint, + } => CommandError::abort_with_exit_code_and_hint( + message, + detailed_exit_code, + hint, + ), _ => CommandError::abort(error.to_string()), } } @@ -108,13 +139,15 @@ impl From for CommandError { fn from(error: RepoError) -> Self { match error { - RepoError::NotFound { at } => CommandError::Abort { - message: format_bytes!( - b"abort: repository {} not found", - get_bytes_from_path(at) - ), - detailed_exit_code: exit_codes::ABORT, - }, + RepoError::NotFound { at } => { + CommandError::abort_with_exit_code_bytes( + format_bytes!( + b"abort: repository {} not found", + get_bytes_from_path(at) + ), + exit_codes::ABORT, + ) + } RepoError::ConfigParseError(error) => error.into(), RepoError::Other(error) => error.into(), } @@ -124,13 +157,13 @@ impl<'a> From<&'a NoRepoInCwdError> for CommandError { fn from(error: &'a NoRepoInCwdError) -> Self { let NoRepoInCwdError { cwd } = error; - CommandError::Abort { - message: format_bytes!( + CommandError::abort_with_exit_code_bytes( + format_bytes!( b"abort: no repository found in '{}' (.hg not found)!", get_bytes_from_path(cwd) ), - detailed_exit_code: exit_codes::ABORT, - } + exit_codes::ABORT, + ) } } @@ -155,15 +188,15 @@ } else { Vec::new() }; - CommandError::Abort { - message: format_bytes!( + CommandError::abort_with_exit_code_bytes( + format_bytes!( b"config error at {}{}: {}", origin, line_message, message ), - detailed_exit_code: exit_codes::CONFIG_ERROR_ABORT, - } + exit_codes::CONFIG_ERROR_ABORT, + ) } } @@ -212,3 +245,46 @@ HgError::from(error).into() } } + +impl From for CommandError { + fn from(e: SparseConfigError) -> Self { + match e { + SparseConfigError::IncludesAfterExcludes { context } => { + Self::abort_with_exit_code_bytes( + format_bytes!( + b"{} config cannot have includes after excludes", + context + ), + exit_codes::CONFIG_PARSE_ERROR_ABORT, + ) + } + SparseConfigError::EntryOutsideSection { context, line } => { + Self::abort_with_exit_code_bytes( + format_bytes!( + b"{} config entry outside of section: {}", + context, + &line, + ), + exit_codes::CONFIG_PARSE_ERROR_ABORT, + ) + } + SparseConfigError::InvalidNarrowPrefix(prefix) => { + Self::abort_with_exit_code_bytes( + format_bytes!( + b"invalid prefix on narrow pattern: {}", + &prefix + ), + exit_codes::ABORT, + ) + } + SparseConfigError::IncludesInNarrow => Self::abort( + "including other spec files using '%include' \ + is not supported in narrowspec", + ), + SparseConfigError::HgError(e) => Self::from(e), + SparseConfigError::PatternError(e) => { + Self::unsupported(format!("{}", e)) + } + } + } +} diff -r 192949b68159 -r a3356ab610fc rust/rhg/src/main.rs --- a/rust/rhg/src/main.rs Thu Oct 20 12:05:17 2022 -0400 +++ b/rust/rhg/src/main.rs Mon Oct 24 15:32:14 2022 +0200 @@ -6,11 +6,12 @@ use clap::Arg; use clap::ArgMatches; use format_bytes::{format_bytes, join}; -use hg::config::{Config, ConfigSource}; +use hg::config::{Config, ConfigSource, PlainInfo}; use hg::repo::{Repo, RepoError}; use hg::utils::files::{get_bytes_from_os_str, get_path_from_bytes}; use hg::utils::SliceExt; use hg::{exit_codes, requirements}; +use std::borrow::Cow; use std::collections::HashSet; use std::ffi::OsString; use std::os::unix::prelude::CommandExt; @@ -300,6 +301,24 @@ } }; + let exit = + |ui: &Ui, config: &Config, result: Result<(), CommandError>| -> ! { + exit( + &argv, + &initial_current_dir, + ui, + OnUnsupported::from_config(config), + result, + // TODO: show a warning or combine with original error if + // `get_bool` returns an error + non_repo_config + .get_bool(b"ui", b"detailed-exit-code") + .unwrap_or(false), + ) + }; + let early_exit = |config: &Config, error: CommandError| -> ! { + exit(&Ui::new_infallible(config), &config, Err(error)) + }; let repo_result = match Repo::find(&non_repo_config, repo_path.to_owned()) { Ok(repo) => Ok(repo), @@ -307,18 +326,7 @@ // Not finding a repo is not fatal yet, if `-R` was not given Err(NoRepoInCwdError { cwd: at }) } - Err(error) => exit( - &argv, - &initial_current_dir, - &Ui::new_infallible(&non_repo_config), - OnUnsupported::from_config(&non_repo_config), - Err(error.into()), - // TODO: show a warning or combine with original error if - // `get_bool` returns an error - non_repo_config - .get_bool(b"ui", b"detailed-exit-code") - .unwrap_or(false), - ), + Err(error) => early_exit(&non_repo_config, error.into()), }; let config = if let Ok(repo) = &repo_result { @@ -326,20 +334,20 @@ } else { &non_repo_config }; - let ui = Ui::new(&config).unwrap_or_else(|error| { - exit( - &argv, - &initial_current_dir, - &Ui::new_infallible(&config), - OnUnsupported::from_config(&config), - Err(error.into()), - config - .get_bool(b"ui", b"detailed-exit-code") - .unwrap_or(false), - ) - }); - let on_unsupported = OnUnsupported::from_config(config); + let mut config_cow = Cow::Borrowed(config); + config_cow.to_mut().apply_plain(PlainInfo::from_env()); + if !ui::plain(Some("tweakdefaults")) + && config_cow + .as_ref() + .get_bool(b"ui", b"tweakdefaults") + .unwrap_or_else(|error| early_exit(&config, error.into())) + { + config_cow.to_mut().tweakdefaults() + }; + let config = config_cow.as_ref(); + let ui = Ui::new(&config) + .unwrap_or_else(|error| early_exit(&config, error.into())); let result = main_with_result( argv.iter().map(|s| s.to_owned()).collect(), &process_start_time, @@ -347,18 +355,7 @@ repo_result.as_ref(), config, ); - exit( - &argv, - &initial_current_dir, - &ui, - on_unsupported, - result, - // TODO: show a warning or combine with original error if `get_bool` - // returns an error - config - .get_bool(b"ui", b"detailed-exit-code") - .unwrap_or(false), - ) + exit(&ui, &config, result) } fn main() -> ! { @@ -372,8 +369,7 @@ match result { Ok(()) => exit_codes::OK, Err(CommandError::Abort { - message: _, - detailed_exit_code, + detailed_exit_code, .. }) => { if use_detailed_exit_code { *detailed_exit_code @@ -480,15 +476,15 @@ match &result { Ok(_) => {} Err(CommandError::Unsuccessful) => {} - Err(CommandError::Abort { - message, - detailed_exit_code: _, - }) => { + Err(CommandError::Abort { message, hint, .. }) => { + // Ignore errors when writing to stderr, we’re already exiting + // with failure code so there’s not much more we can do. if !message.is_empty() { - // Ignore errors when writing to stderr, we’re already exiting - // with failure code so there’s not much more we can do. let _ = ui.write_stderr(&format_bytes!(b"{}\n", message)); } + if let Some(hint) = hint { + let _ = ui.write_stderr(&format_bytes!(b"({})\n", hint)); + } } Err(CommandError::UnsupportedFeature { message }) => { match on_unsupported { @@ -546,6 +542,7 @@ debugdata debugrequirements debugignorerhg + debugrhgsparse files root config @@ -677,8 +674,15 @@ /// The `*` extension is an edge-case for config sub-options that apply to all /// extensions. For now, only `:required` exists, but that may change in the /// future. -const SUPPORTED_EXTENSIONS: &[&[u8]] = - &[b"blackbox", b"share", b"sparse", b"narrow", b"*"]; +const SUPPORTED_EXTENSIONS: &[&[u8]] = &[ + b"blackbox", + b"share", + b"sparse", + b"narrow", + b"*", + b"strip", + b"rebase", +]; fn check_extensions(config: &Config) -> Result<(), CommandError> { if let Some(b"*") = config.get(b"rhg", b"ignored-extensions") { @@ -687,13 +691,18 @@ } let enabled: HashSet<&[u8]> = config - .get_section_keys(b"extensions") - .into_iter() - .map(|extension| { + .iter_section(b"extensions") + .filter_map(|(extension, value)| { + if value == b"!" { + // Filter out disabled extensions + return None; + } // Ignore extension suboptions. Only `required` exists for now. // `rhg` either supports an extension or doesn't, so it doesn't // make sense to consider the loading of an extension. - extension.split_2(b':').unwrap_or((extension, b"")).0 + let actual_extension = + extension.split_2(b':').unwrap_or((extension, b"")).0; + Some(actual_extension) }) .collect(); diff -r 192949b68159 -r a3356ab610fc rust/rhg/src/ui.rs --- a/rust/rhg/src/ui.rs Thu Oct 20 12:05:17 2022 -0400 +++ b/rust/rhg/src/ui.rs Mon Oct 24 15:32:14 2022 +0200 @@ -3,10 +3,9 @@ use format_bytes::format_bytes; use format_bytes::write_bytes; use hg::config::Config; +use hg::config::PlainInfo; use hg::errors::HgError; -use hg::utils::files::get_bytes_from_os_string; use std::borrow::Cow; -use std::env; use std::io; use std::io::{ErrorKind, Write}; @@ -127,35 +126,15 @@ } stdout.flush() } - - /// Return whether plain mode is active. - /// - /// Plain mode means that all configuration variables which affect - /// the behavior and output of Mercurial should be - /// ignored. Additionally, the output should be stable, - /// reproducible and suitable for use in scripts or applications. - /// - /// The only way to trigger plain mode is by setting either the - /// `HGPLAIN' or `HGPLAINEXCEPT' environment variables. - /// - /// The return value can either be - /// - False if HGPLAIN is not set, or feature is in HGPLAINEXCEPT - /// - False if feature is disabled by default and not included in HGPLAIN - /// - True otherwise - pub fn plain(&self, feature: Option<&str>) -> bool { - plain(feature) - } } +// TODO: pass the PlainInfo to call sites directly and +// delete this function pub fn plain(opt_feature: Option<&str>) -> bool { - if let Some(except) = env::var_os("HGPLAINEXCEPT") { - opt_feature.map_or(true, |feature| { - get_bytes_from_os_string(except) - .split(|&byte| byte == b',') - .all(|exception| exception != feature.as_bytes()) - }) - } else { - env::var_os("HGPLAIN").is_some() + let plain_info = PlainInfo::from_env(); + match opt_feature { + None => plain_info.is_plain(), + Some(feature) => plain_info.is_feature_plain(feature), } } diff -r 192949b68159 -r a3356ab610fc setup.py --- a/setup.py Thu Oct 20 12:05:17 2022 -0400 +++ b/setup.py Mon Oct 24 15:32:14 2022 +0200 @@ -666,30 +666,55 @@ class buildhgexe(build_ext): description = 'compile hg.exe from mercurial/exewrapper.c' - user_options = build_ext.user_options + [ - ( - 'long-paths-support', - None, - 'enable support for long paths on ' - 'Windows (off by default and ' - 'experimental)', - ), - ] - LONG_PATHS_MANIFEST = """ - - - - - true - - - """ + LONG_PATHS_MANIFEST = """\ + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + + + + + + +""" def initialize_options(self): build_ext.initialize_options(self) - self.long_paths_support = False def build_extensions(self): if os.name != 'nt': @@ -700,8 +725,8 @@ pythonlib = None - dir = os.path.dirname(self.get_ext_fullpath('dummy')) - self.hgtarget = os.path.join(dir, 'hg') + dirname = os.path.dirname(self.get_ext_fullpath('dummy')) + self.hgtarget = os.path.join(dirname, 'hg') if getattr(sys, 'dllhandle', None): # Different Python installs can have different Python library @@ -774,22 +799,11 @@ self.compiler.link_executable( objects, self.hgtarget, libraries=[], output_dir=self.build_temp ) - if self.long_paths_support: - self.addlongpathsmanifest() + + self.addlongpathsmanifest() def addlongpathsmanifest(self): - r"""Add manifest pieces so that hg.exe understands long paths - - This is an EXPERIMENTAL feature, use with care. - To enable long paths support, one needs to do two things: - - build Mercurial with --long-paths-support option - - change HKLM\SYSTEM\CurrentControlSet\Control\FileSystem\ - LongPathsEnabled to have value 1. - - Please ignore 'warning 81010002: Unrecognized Element "longPathAware"'; - it happens because Mercurial uses mt.exe circa 2008, which is not - yet aware of long paths support in the manifest (I think so at least). - This does not stop mt.exe from embedding/merging the XML properly. + """Add manifest pieces so that hg.exe understands long paths Why resource #1 should be used for .exe manifests? I don't know and wasn't able to find an explanation for mortals. But it seems to work. @@ -797,21 +811,18 @@ exefname = self.compiler.executable_filename(self.hgtarget) fdauto, manfname = tempfile.mkstemp(suffix='.hg.exe.manifest') os.close(fdauto) - with open(manfname, 'w') as f: + with open(manfname, 'w', encoding="UTF-8") as f: f.write(self.LONG_PATHS_MANIFEST) log.info("long paths manifest is written to '%s'" % manfname) - inputresource = '-inputresource:%s;#1' % exefname outputresource = '-outputresource:%s;#1' % exefname log.info("running mt.exe to update hg.exe's manifest in-place") - # supplying both -manifest and -inputresource to mt.exe makes - # it merge the embedded and supplied manifests in the -outputresource + self.spawn( [ - 'mt.exe', + self.compiler.mt, '-nologo', '-manifest', manfname, - inputresource, outputresource, ] ) diff -r 192949b68159 -r a3356ab610fc tests/run-tests.py --- a/tests/run-tests.py Thu Oct 20 12:05:17 2022 -0400 +++ b/tests/run-tests.py Mon Oct 24 15:32:14 2022 +0200 @@ -48,7 +48,7 @@ import collections import contextlib import difflib -import distutils.version as version + import errno import functools import json @@ -72,6 +72,13 @@ import uuid import xml.dom.minidom as minidom +try: + # PEP 632 recommend the use of `packaging.version` to replace the + # deprecated `distutil.version`. So lets do it. + import packaging.version as version +except ImportError: + import distutils.version as version + if sys.version_info < (3, 5, 0): print( '%s is only supported on Python 3.5+, not %s' @@ -3437,6 +3444,7 @@ if self.options.list_tests: result = runner.listtests(suite) else: + install_start_time = time.monotonic() self._usecorrectpython() if self._installdir: self._installhg() @@ -3450,6 +3458,11 @@ elif self.options.pyoxidized: self._build_pyoxidized() self._use_correct_mercurial() + install_end_time = time.monotonic() + if self._installdir: + msg = 'installed Mercurial in %.2f seconds' + msg %= install_end_time - install_start_time + log(msg) log( 'running %d tests using %d parallel processes' diff -r 192949b68159 -r a3356ab610fc tests/test-bisect2.t --- a/tests/test-bisect2.t Thu Oct 20 12:05:17 2022 -0400 +++ b/tests/test-bisect2.t Mon Oct 24 15:32:14 2022 +0200 @@ -784,7 +784,6 @@ $ hg log -q -r 'bisect(pruned)' 0:33b1f9bc8bc5 1:4ca5088da217 - 2:051e12f87bf1 8:dab8161ac8fc 11:82ca6f06eccd 12:9f259202bbe7 diff -r 192949b68159 -r a3356ab610fc tests/test-bundle.t --- a/tests/test-bundle.t Thu Oct 20 12:05:17 2022 -0400 +++ b/tests/test-bundle.t Mon Oct 24 15:32:14 2022 +0200 @@ -718,7 +718,7 @@ $ hg init empty $ hg -R test bundle --base null -r 0 ../0.hg 1 changesets found - $ hg -R test bundle --base 0 -r 1 ../1.hg + $ hg -R test bundle --exact -r 1 ../1.hg 1 changesets found $ hg -R empty unbundle -u ../0.hg ../1.hg adding changesets diff -r 192949b68159 -r a3356ab610fc tests/test-chg.t --- a/tests/test-chg.t Thu Oct 20 12:05:17 2022 -0400 +++ b/tests/test-chg.t Mon Oct 24 15:32:14 2022 +0200 @@ -432,6 +432,20 @@ YYYY/MM/DD HH:MM:SS (PID)> log -R cached YYYY/MM/DD HH:MM:SS (PID)> loaded repo into cache: $TESTTMP/cached (in ...s) +Test that -R is interpreted relative to --cwd. + + $ hg init repo1 + $ mkdir -p a/b + $ hg init a/b/repo2 + $ printf "[alias]\ntest=repo1\n" >> repo1/.hg/hgrc + $ printf "[alias]\ntest=repo2\n" >> a/b/repo2/.hg/hgrc + $ cd a + $ chg --cwd .. -R repo1 show alias.test + repo1 + $ chg --cwd . -R b/repo2 show alias.test + repo2 + $ cd .. + Test that chg works (sets to the user's actual LC_CTYPE) even when python "coerces" the locale (py3.7+) diff -r 192949b68159 -r a3356ab610fc tests/test-completion.t --- a/tests/test-completion.t Thu Oct 20 12:05:17 2022 -0400 +++ b/tests/test-completion.t Mon Oct 24 15:32:14 2022 +0200 @@ -261,7 +261,7 @@ bookmarks: force, rev, delete, rename, inactive, list, template branch: force, clean, rev branches: active, closed, rev, template - bundle: force, rev, branch, base, all, type, ssh, remotecmd, insecure + bundle: exact, force, rev, branch, base, all, type, ssh, remotecmd, insecure cat: output, rev, decode, include, exclude, template clone: noupdate, updaterev, rev, branch, pull, uncompressed, stream, ssh, remotecmd, insecure commit: addremove, close-branch, amend, secret, edit, force-close-branch, interactive, include, exclude, message, logfile, date, user, subrepos diff -r 192949b68159 -r a3356ab610fc tests/test-contrib-perf.t --- a/tests/test-contrib-perf.t Thu Oct 20 12:05:17 2022 -0400 +++ b/tests/test-contrib-perf.t Mon Oct 24 15:32:14 2022 +0200 @@ -96,6 +96,7 @@ perf::branchmapupdate benchmark branchmap update from for revs to revs + perf::bundle benchmark the creation of a bundle from a repository perf::bundleread Benchmark reading of bundle files. perf::cca (no help text available) @@ -105,6 +106,9 @@ (no help text available) perf::ctxfiles (no help text available) + perf::delta-find + benchmark the process of finding a valid delta for a revlog + revision perf::diffwd Profile diff of working directory changes perf::dirfoldmap benchmap a 'dirstate._map.dirfoldmap.get()' request @@ -187,6 +191,8 @@ perf::tags (no help text available) perf::templating test the rendering time of a given template + perf::unbundle + benchmark application of a bundle in a repository. perf::unidiff benchmark a unified diff between revisions perf::volatilesets @@ -385,6 +391,11 @@ searching for changes searching for changes searching for changes + $ hg perf::bundle 'last(all(), 5)' + $ hg bundle --exact --rev 'last(all(), 5)' last-5.hg + 4 changesets found + $ hg perf::unbundle last-5.hg + test profile-benchmark option ------------------------------ diff -r 192949b68159 -r a3356ab610fc tests/test-contrib-pull-logger.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-contrib-pull-logger.t Mon Oct 24 15:32:14 2022 +0200 @@ -0,0 +1,78 @@ +Check that the pull logger plugins logs pulls +============================================= + +Enable the extension + + $ echo "[extensions]" >> $HGRCPATH + $ echo "pull-logger = $TESTDIR/../contrib/pull_logger.py" >> $HGRCPATH + + +Check the format of the generated log entries, with a bunch of elements in the +common and heads set + + $ hg init server + $ hg -R server debugbuilddag '.*2+2' + $ hg clone ssh://user@dummy/server client --rev 0 + adding changesets + adding manifests + adding file changes + added 1 changesets with 0 changes to 0 files + new changesets 1ea73414a91b + updating to branch default + 0 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ tail -1 server/.hg/pull_log.jsonl + {"common": ["0000000000000000000000000000000000000000"], "heads": ["1ea73414a91b0920940797d8fc6a11e447f8ea1e"], "logger_version": 0, "timestamp": *} (glob) + $ hg -R client pull --rev 1 --rev 2 + pulling from ssh://user@dummy/server + searching for changes + adding changesets + adding manifests + adding file changes + added 2 changesets with 0 changes to 0 files (+1 heads) + new changesets d8736c3a2c84:fa28e81e283b + (run 'hg heads' to see heads, 'hg merge' to merge) + $ tail -1 server/.hg/pull_log.jsonl + {"common": ["1ea73414a91b0920940797d8fc6a11e447f8ea1e"], "heads": ["d8736c3a2c84ee759a2821385804bcb67f266ade", "fa28e81e283b3416de4d48ee0dd2d446e9e38d7c"], "logger_version": 0, "timestamp": *} (glob) + $ hg -R client pull --rev 2 --rev 3 + pulling from ssh://user@dummy/server + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 0 changes to 0 files + new changesets 944641ddcaef + (run 'hg update' to get a working copy) + $ tail -1 server/.hg/pull_log.jsonl + {"common": ["1ea73414a91b0920940797d8fc6a11e447f8ea1e", "fa28e81e283b3416de4d48ee0dd2d446e9e38d7c"], "heads": ["944641ddcaef174df7ce1bc2751a5f165129778b", "fa28e81e283b3416de4d48ee0dd2d446e9e38d7c"], "logger_version": 0, "timestamp": *} (glob) + + +Check the number of entries generated in the log when pulling from multiple +clients at the same time + + $ rm -f server/.hg/pull_log.jsonl + $ for i in $($TESTDIR/seq.py 32); do + > hg clone ssh://user@dummy/server client_$i --rev 0 + > done > /dev/null + $ for i in $($TESTDIR/seq.py 32); do + > hg -R client_$i pull --rev 1 & + > done > /dev/null + $ wait + $ wc -l server/.hg/pull_log.jsonl + \s*64 .* (re) + + +Test log rotation when reaching some size threshold + + $ cat >> $HGRCPATH << EOF + > [pull-logger] + > rotate-size = 1kb + > EOF + + $ rm -f server/.hg/pull_log.jsonl + $ for i in $($TESTDIR/seq.py 10); do + > hg -R client pull --rev 1 + > done > /dev/null + $ wc -l server/.hg/pull_log.jsonl + \s*3 .* (re) + $ wc -l server/.hg/pull_log.jsonl.rotated + \s*7 .* (re) diff -r 192949b68159 -r a3356ab610fc tests/test-http-bad-server.t --- a/tests/test-http-bad-server.t Thu Oct 20 12:05:17 2022 -0400 +++ b/tests/test-http-bad-server.t Mon Oct 24 15:32:14 2022 +0200 @@ -659,7 +659,7 @@ $ hg clone http://localhost:$HGPORT/ clone requesting all changes - abort: HTTP request error (incomplete response) (py3 !) + abort: HTTP request error (incomplete response*) (glob) (this may be an intermittent network failure; if the error persists, consider contacting the network or server operator) [255] @@ -703,7 +703,7 @@ $ hg clone http://localhost:$HGPORT/ clone requesting all changes - abort: HTTP request error (incomplete response) (py3 !) + abort: HTTP request error (incomplete response*) (glob) (this may be an intermittent network failure; if the error persists, consider contacting the network or server operator) [255] @@ -904,7 +904,7 @@ adding changesets transaction abort! rollback completed - abort: HTTP request error (incomplete response) (py3 !) + abort: HTTP request error (incomplete response*) (glob) (this may be an intermittent network failure; if the error persists, consider contacting the network or server operator) [255] @@ -1021,7 +1021,7 @@ adding file changes transaction abort! rollback completed - abort: HTTP request error (incomplete response) (py3 !) + abort: HTTP request error (incomplete response*) (glob) (this may be an intermittent network failure; if the error persists, consider contacting the network or server operator) [255] diff -r 192949b68159 -r a3356ab610fc tests/test-log.t --- a/tests/test-log.t Thu Oct 20 12:05:17 2022 -0400 +++ b/tests/test-log.t Mon Oct 24 15:32:14 2022 +0200 @@ -2157,6 +2157,8 @@ ... '''.encode('utf-8')) and None $ sh < setup.sh +#if no-rhg + test in problematic encoding >>> with open('test.sh', 'wb') as f: ... f.write(u''' @@ -2179,6 +2181,8 @@ 3 1 +#endif + $ cd .. test hg log on non-existent files and on directories diff -r 192949b68159 -r a3356ab610fc tests/test-mq-subrepo-svn.t --- a/tests/test-mq-subrepo-svn.t Thu Oct 20 12:05:17 2022 -0400 +++ b/tests/test-mq-subrepo-svn.t Mon Oct 24 15:32:14 2022 +0200 @@ -38,7 +38,7 @@ A .hgsub $ hg qnew -m0 0.diff $ cd sub - $ echo a > a + $ echo foo > a $ svn add a A a $ svn st diff -r 192949b68159 -r a3356ab610fc tests/test-phase-archived.t --- a/tests/test-phase-archived.t Thu Oct 20 12:05:17 2022 -0400 +++ b/tests/test-phase-archived.t Mon Oct 24 15:32:14 2022 +0200 @@ -4,7 +4,7 @@ $ cat << EOF >> $HGRCPATH > [format] - > internal-phase=yes + > exp-archived-phase=yes > [extensions] > strip= > [experimental] diff -r 192949b68159 -r a3356ab610fc tests/test-phases.t --- a/tests/test-phases.t Thu Oct 20 12:05:17 2022 -0400 +++ b/tests/test-phases.t Mon Oct 24 15:32:14 2022 +0200 @@ -879,7 +879,7 @@ Check we deny its usage on older repository - $ hg init no-internal-phase --config format.internal-phase=no + $ hg init no-internal-phase --config format.use-internal-phase=no $ cd no-internal-phase $ hg debugrequires | grep internal-phase [1] @@ -900,10 +900,10 @@ Check it works fine with repository that supports it. - $ hg init internal-phase --config format.internal-phase=yes + $ hg init internal-phase --config format.use-internal-phase=yes $ cd internal-phase $ hg debugrequires | grep internal-phase - internal-phase + internal-phase-2 $ mkcommit A test-debug-phase: new rev 0: x -> 1 test-hook-close-phase: 4a2df7238c3b48766b5e22fafbb8a2f506ec8256: -> draft @@ -951,21 +951,28 @@ Commit an archived changesets + $ cd .. + $ hg clone --quiet --pull internal-phase archived-phase \ + > --config format.exp-archived-phase=yes \ + > --config extensions.phasereport='!' \ + > --config hooks.txnclose-phase.test= + + $ cd archived-phase + $ echo B > B $ hg add B $ hg status A B $ hg --config "phases.new-commit=archived" commit -m "my test archived commit" - test-debug-phase: new rev 2: x -> 32 + test-debug-phase: new rev 1: x -> 32 test-hook-close-phase: 8df5997c3361518f733d1ae67cd3adb9b0eaf125: -> archived The changeset is a working parent descendant. Per the usual visibility rules, it is made visible. $ hg log -G -l 3 - @ changeset: 2:8df5997c3361 + @ changeset: 1:8df5997c3361 | tag: tip - | parent: 0:4a2df7238c3b | user: test | date: Thu Jan 01 00:00:00 1970 +0000 | summary: my test archived commit diff -r 192949b68159 -r a3356ab610fc tests/test-releasenotes-formatting.t --- a/tests/test-releasenotes-formatting.t Thu Oct 20 12:05:17 2022 -0400 +++ b/tests/test-releasenotes-formatting.t Mon Oct 24 15:32:14 2022 +0200 @@ -387,6 +387,8 @@ $ touch a $ hg -q commit -A -l - << EOF + > commit 2 + > > .. asf:: > > First paragraph under this admonition. @@ -395,7 +397,7 @@ Suggest similar admonition in place of the invalid one. $ hg releasenotes -r . -c - Invalid admonition 'asf' present in changeset 4026fe9e1c20 + Invalid admonition 'asf' present in changeset 99fa3c800c5e $ touch b $ hg -q commit -A -l - << EOF @@ -405,7 +407,7 @@ > EOF $ hg releasenotes -r . -c - Invalid admonition 'fixes' present in changeset 0e7130d2705c + Invalid admonition 'fixes' present in changeset 4737b1b5afd1 (did you mean fix?) $ cd .. diff -r 192949b68159 -r a3356ab610fc tests/test-revset.t --- a/tests/test-revset.t Thu Oct 20 12:05:17 2022 -0400 +++ b/tests/test-revset.t Mon Oct 24 15:32:14 2022 +0200 @@ -2974,6 +2974,25 @@ 1 b11 m12 u111 112 7200 0 b12 m111 u112 111 10800 +random sort + + $ hg log --rev 'sort(all(), "random")' | wc -l + \s*8 (re) + $ hg log --rev 'sort(all(), "-random")' | wc -l + \s*8 (re) + $ hg log --rev 'sort(all(), "random", random.seed=celeste)' + 6 b111 t2 tu 130 0 + 7 b111 t3 tu 130 0 + 4 b111 m112 u111 110 14400 + 3 b112 m111 u11 120 0 + 5 b111 t1 tu 130 0 + 0 b12 m111 u112 111 10800 + 1 b11 m12 u111 112 7200 + 2 b111 m11 u12 111 3600 + $ hg log --rev 'first(sort(all(), "random", random.seed=celeste))' + 6 b111 t2 tu 130 0 + + topographical sorting can't be combined with other sort keys, and you can't use the topo.firstbranch option when topo sort is not active: diff -r 192949b68159 -r a3356ab610fc tests/test-revset2.t --- a/tests/test-revset2.t Thu Oct 20 12:05:17 2022 -0400 +++ b/tests/test-revset2.t Mon Oct 24 15:32:14 2022 +0200 @@ -1481,6 +1481,20 @@ $ hg init namedbranch $ cd namedbranch + $ log 'roots(.)' + -1 + $ log 'roots(. or wdir())' + -1 + $ log 'roots(wdir())' + 2147483647 + $ log 'sort(., -topo)' + -1 + $ log 'sort(. or wdir(), -topo)' + -1 + 2147483647 + $ log 'sort(wdir(), -topo)' + 2147483647 + $ echo default0 >> a $ hg ci -Aqm0 $ echo default1 >> a @@ -1498,6 +1512,17 @@ $ echo default5 >> a $ hg ci -m5 + $ log 'roots(. or wdir())' + 5 + $ log 'roots(wdir())' + 2147483647 + $ log 'sort(. or wdir() or .^, -topo)' + 4 + 5 + 2147483647 + $ log 'sort(wdir(), -topo)' + 2147483647 + "null" revision belongs to "default" branch (issue4683) $ log 'branch(null)' diff -r 192949b68159 -r a3356ab610fc tests/test-rhg-sparse-narrow.t --- a/tests/test-rhg-sparse-narrow.t Thu Oct 20 12:05:17 2022 -0400 +++ b/tests/test-rhg-sparse-narrow.t Mon Oct 24 15:32:14 2022 +0200 @@ -85,15 +85,12 @@ dir1/x dir1/y -Hg status needs to do some filtering based on narrow spec, so we don't -support it in rhg for narrow clones yet. +Hg status needs to do some filtering based on narrow spec $ mkdir dir2 $ touch dir2/q $ "$real_hg" status $ $NO_FALLBACK rhg --config rhg.status=true status - unsupported feature: rhg status is not supported for sparse checkouts or narrow clones yet - [252] Adding "orphaned" index files: diff -r 192949b68159 -r a3356ab610fc tests/test-shelve.t --- a/tests/test-shelve.t Thu Oct 20 12:05:17 2022 -0400 +++ b/tests/test-shelve.t Mon Oct 24 15:32:14 2022 +0200 @@ -24,7 +24,7 @@ $ cat <> $HGRCPATH > [format] - > internal-phase = yes + > use-internal-phase = yes > EOF #endif @@ -253,12 +253,12 @@ (this also tests that same timestamp prevents backups from being removed, even though there are more than 'maxbackups' backups) - $ f -t .hg/shelve-backup/default.patch - .hg/shelve-backup/default.patch: file - $ touch -t 200001010000 .hg/shelve-backup/default.patch - $ f -t .hg/shelve-backup/default-1.patch - .hg/shelve-backup/default-1.patch: file - $ touch -t 200001010000 .hg/shelve-backup/default-1.patch + $ f -t .hg/shelve-backup/default.shelve + .hg/shelve-backup/default.shelve: file + $ touch -t 200001010000 .hg/shelve-backup/default.shelve + $ f -t .hg/shelve-backup/default-1.shelve + .hg/shelve-backup/default-1.shelve: file + $ touch -t 200001010000 .hg/shelve-backup/default-1.shelve $ hg unshelve unshelving change 'default-01' @@ -1544,4 +1544,87 @@ $ hg update -q --clean . $ hg patch -p1 test_patch.patch applying test_patch.patch + + $ hg strip -q -r . #endif + +Check the comment of the last commit for consistency + + $ hg log -r . --template '{desc}\n' + add C to bars + +-- if phasebased, shelve works without patch and bundle + + $ hg update -q --clean . + $ rm -r .hg/shelve* + $ echo import antigravity >> somefile.py + $ hg add somefile.py + $ hg shelve -q +#if phasebased + $ rm .hg/shelved/default.hg + $ rm .hg/shelved/default.patch +#endif + +shelve --list --patch should work even with no patch file. + + $ hg shelve --list --patch + default (*s ago) * changes to: add C to bars (glob) + + diff --git a/somefile.py b/somefile.py + new file mode 100644 + --- /dev/null + +++ b/somefile.py + @@ -0,0 +1,1 @@ + +import antigravity + + $ hg unshelve + unshelving change 'default' + +#if phasebased + $ ls .hg/shelve-backup + default.shelve +#endif + +#if stripbased + $ ls .hg/shelve-backup + default.hg + default.patch + default.shelve +#endif + + +-- allow for phase-based shelves to be disabled + + $ hg update -q --clean . + $ hg strip -q --hidden -r 0 + $ rm -r .hg/shelve* + +#if phasebased + $ cat <> $HGRCPATH + > [shelve] + > store = strip + > EOF +#endif + + $ echo import this >> somefile.py + $ hg add somefile.py + $ hg shelve -q + $ hg log --hidden + $ ls .hg/shelved + default.hg + default.patch + default.shelve + $ hg unshelve -q + +Override the disabling, re-enabling phase-based shelves + + $ hg shelve --config shelve.store=internal -q + +#if phasebased + $ hg log --hidden --template '{user}\n' + shelve@localhost +#endif + +#if stripbased + $ hg log --hidden --template '{user}\n' +#endif diff -r 192949b68159 -r a3356ab610fc tests/test-shelve2.t --- a/tests/test-shelve2.t Thu Oct 20 12:05:17 2022 -0400 +++ b/tests/test-shelve2.t Mon Oct 24 15:32:14 2022 +0200 @@ -26,7 +26,7 @@ $ cat <> $HGRCPATH > [format] - > internal-phase = yes + > use-internal-phase = yes > EOF #endif diff -r 192949b68159 -r a3356ab610fc tests/test-status.t --- a/tests/test-status.t Thu Oct 20 12:05:17 2022 -0400 +++ b/tests/test-status.t Mon Oct 24 15:32:14 2022 +0200 @@ -637,9 +637,16 @@ M a b R b + $ hg st --config ui.statuscopies=true --no-copies + M a + R b $ hg st --config ui.statuscopies=false M a R b + $ hg st --config ui.statuscopies=false --copies + M a + b + R b $ hg st --config ui.tweakdefaults=yes M a b diff -r 192949b68159 -r a3356ab610fc tests/test-subrepo-svn.t --- a/tests/test-subrepo-svn.t Thu Oct 20 12:05:17 2022 -0400 +++ b/tests/test-subrepo-svn.t Mon Oct 24 15:32:14 2022 +0200 @@ -591,7 +591,7 @@ $ cd "$WCROOT" $ svn up > /dev/null $ mkdir trunk/subdir branches - $ echo a > trunk/subdir/a + $ echo foo > trunk/subdir/a $ svn add trunk/subdir branches A trunk/subdir A trunk/subdir/a diff -r 192949b68159 -r a3356ab610fc tests/test-template-functions.t --- a/tests/test-template-functions.t Thu Oct 20 12:05:17 2022 -0400 +++ b/tests/test-template-functions.t Mon Oct 24 15:32:14 2022 +0200 @@ -1718,4 +1718,19 @@ $ hg log -T "{config('templateconfig', 'knob', if(true, 'foo', 'bar'))}\n" foo +reverse filter: + + $ hg log -T "{'abc\ndef\nghi'|splitlines|reverse}\n" + ghi def abc + + $ hg log -T "{'abc'|reverse}\n" + hg: parse error: not reversible + (incompatible use of template filter 'reverse') + [10] + + $ hg log -T "{date|reverse}\n" + hg: parse error: not reversible + (template filter 'reverse' is not compatible with keyword 'date') + [10] + $ cd .. diff -r 192949b68159 -r a3356ab610fc tests/test-update-branches.t --- a/tests/test-update-branches.t Thu Oct 20 12:05:17 2022 -0400 +++ b/tests/test-update-branches.t Mon Oct 24 15:32:14 2022 +0200 @@ -633,6 +633,10 @@ # # To mark files as resolved: hg resolve --mark FILE + $ hg status -T '{status} {path} - {relpath(path)}\n' + M foo - foo + a - a + $ hg status -Tjson [ {