# HG changeset patch # User Raphaël Gomès # Date 1664873787 -7200 # Node ID 44bc045a43ca4e33587dce816b141bdf6e86faf3 # Parent a6efc1f1a73b9ed9f7b4a0a07bac99433d591030# Parent b6c6ac6ef3599ee017786b6fc3854c172dfa863a branching: merge stable into default diff -r b6c6ac6ef359 -r 44bc045a43ca Makefile --- a/Makefile Tue Oct 04 10:24:56 2022 +0200 +++ b/Makefile Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca contrib/heptapod-ci.yml --- a/contrib/heptapod-ci.yml Tue Oct 04 10:24:56 2022 +0200 +++ b/contrib/heptapod-ci.yml Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca contrib/packaging/Makefile --- a/contrib/packaging/Makefile Tue Oct 04 10:24:56 2022 +0200 +++ b/contrib/packaging/Makefile Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca contrib/packaging/docker/rhel9 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/contrib/packaging/docker/rhel9 Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca contrib/packaging/packagelib.sh --- a/contrib/packaging/packagelib.sh Tue Oct 04 10:24:56 2022 +0200 +++ b/contrib/packaging/packagelib.sh Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca contrib/perf-utils/compare-discovery-case --- a/contrib/perf-utils/compare-discovery-case Tue Oct 04 10:24:56 2022 +0200 +++ b/contrib/perf-utils/compare-discovery-case Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca contrib/perf.py --- a/contrib/perf.py Tue Oct 04 10:24:56 2022 +0200 +++ b/contrib/perf.py Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca hgext/fsmonitor/pywatchman/pybser.py --- a/hgext/fsmonitor/pywatchman/pybser.py Tue Oct 04 10:24:56 2022 +0200 +++ b/hgext/fsmonitor/pywatchman/pybser.py Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca hgext/rebase.py --- a/hgext/rebase.py Tue Oct 04 10:24:56 2022 +0200 +++ b/hgext/rebase.py Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca mercurial/cext/revlog.c --- a/mercurial/cext/revlog.c Tue Oct 04 10:24:56 2022 +0200 +++ b/mercurial/cext/revlog.c Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca mercurial/cmdutil.py --- a/mercurial/cmdutil.py Tue Oct 04 10:24:56 2022 +0200 +++ b/mercurial/cmdutil.py Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca mercurial/commands.py --- a/mercurial/commands.py Tue Oct 04 10:24:56 2022 +0200 +++ b/mercurial/commands.py Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca mercurial/configitems.py --- a/mercurial/configitems.py Tue Oct 04 10:24:56 2022 +0200 +++ b/mercurial/configitems.py Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca mercurial/debugcommands.py --- a/mercurial/debugcommands.py Tue Oct 04 10:24:56 2022 +0200 +++ b/mercurial/debugcommands.py Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca mercurial/defaultrc/mergetools.rc --- a/mercurial/defaultrc/mergetools.rc Tue Oct 04 10:24:56 2022 +0200 +++ b/mercurial/defaultrc/mergetools.rc Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca mercurial/dirstate.py --- a/mercurial/dirstate.py Tue Oct 04 10:24:56 2022 +0200 +++ b/mercurial/dirstate.py Tue Oct 04 10:56:27 2022 +0200 @@ -1287,6 +1287,7 @@ allowed_matchers = ( matchmod.alwaysmatcher, + matchmod.differencematcher, matchmod.exactmatcher, matchmod.includematcher, matchmod.intersectionmatcher, diff -r b6c6ac6ef359 -r 44bc045a43ca mercurial/dispatch.py --- a/mercurial/dispatch.py Tue Oct 04 10:24:56 2022 +0200 +++ b/mercurial/dispatch.py Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca mercurial/hbisect.py --- a/mercurial/hbisect.py Tue Oct 04 10:24:56 2022 +0200 +++ b/mercurial/hbisect.py Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca mercurial/helptext/bundlespec.txt --- a/mercurial/helptext/bundlespec.txt Tue Oct 04 10:24:56 2022 +0200 +++ b/mercurial/helptext/bundlespec.txt Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca mercurial/hgweb/__init__.py --- a/mercurial/hgweb/__init__.py Tue Oct 04 10:24:56 2022 +0200 +++ b/mercurial/hgweb/__init__.py Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca mercurial/localrepo.py --- a/mercurial/localrepo.py Tue Oct 04 10:24:56 2022 +0200 +++ b/mercurial/localrepo.py Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca mercurial/obsolete.py --- a/mercurial/obsolete.py Tue Oct 04 10:24:56 2022 +0200 +++ b/mercurial/obsolete.py Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca mercurial/phases.py --- a/mercurial/phases.py Tue Oct 04 10:24:56 2022 +0200 +++ b/mercurial/phases.py Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca mercurial/requirements.py --- a/mercurial/requirements.py Tue Oct 04 10:24:56 2022 +0200 +++ b/mercurial/requirements.py Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca mercurial/revlog.py --- a/mercurial/revlog.py Tue Oct 04 10:24:56 2022 +0200 +++ b/mercurial/revlog.py Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca mercurial/revset.py --- a/mercurial/revset.py Tue Oct 04 10:24:56 2022 +0200 +++ b/mercurial/revset.py Tue Oct 04 10:56:27 2022 +0200 @@ -7,7 +7,10 @@ import binascii +import functools +import random import re +import sys from .i18n import _ from .pycompat import getattr @@ -2347,6 +2350,15 @@ 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 +2367,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 +2417,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 +2450,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) @@ -2448,7 +2482,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 b6c6ac6ef359 -r 44bc045a43ca mercurial/scmutil.py --- a/mercurial/scmutil.py Tue Oct 04 10:24:56 2022 +0200 +++ b/mercurial/scmutil.py Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca mercurial/shelve.py --- a/mercurial/shelve.py Tue Oct 04 10:24:56 2022 +0200 +++ b/mercurial/shelve.py Tue Oct 04 10:56:27 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) diff -r b6c6ac6ef359 -r 44bc045a43ca mercurial/subrepo.py --- a/mercurial/subrepo.py Tue Oct 04 10:24:56 2022 +0200 +++ b/mercurial/subrepo.py Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca mercurial/templatefilters.py --- a/mercurial/templatefilters.py Tue Oct 04 10:24:56 2022 +0200 +++ b/mercurial/templatefilters.py Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca mercurial/url.py --- a/mercurial/url.py Tue Oct 04 10:24:56 2022 +0200 +++ b/mercurial/url.py Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca relnotes/next --- a/relnotes/next Tue Oct 04 10:24:56 2022 +0200 +++ b/relnotes/next Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca rust/hg-core/src/config/layer.rs --- a/rust/hg-core/src/config/layer.rs Tue Oct 04 10:24:56 2022 +0200 +++ b/rust/hg-core/src/config/layer.rs Tue Oct 04 10:56:27 2022 +0200 @@ -81,6 +81,7 @@ String::from_utf8_lossy(arg), ), CONFIG_PARSE_ERROR_ABORT, + None, ))? } } diff -r b6c6ac6ef359 -r 44bc045a43ca rust/hg-core/src/dirstate.rs --- a/rust/hg-core/src/dirstate.rs Tue Oct 04 10:24:56 2022 +0200 +++ b/rust/hg-core/src/dirstate.rs Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca rust/hg-core/src/errors.rs --- a/rust/hg-core/src/errors.rs Tue Oct 04 10:24:56 2022 +0200 +++ b/rust/hg-core/src/errors.rs Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca rust/hg-core/src/exit_codes.rs --- a/rust/hg-core/src/exit_codes.rs Tue Oct 04 10:24:56 2022 +0200 +++ b/rust/hg-core/src/exit_codes.rs Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca rust/hg-core/src/filepatterns.rs --- a/rust/hg-core/src/filepatterns.rs Tue Oct 04 10:24:56 2022 +0200 +++ b/rust/hg-core/src/filepatterns.rs Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca rust/hg-core/src/lib.rs --- a/rust/hg-core/src/lib.rs Tue Oct 04 10:24:56 2022 +0200 +++ b/rust/hg-core/src/lib.rs Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca rust/hg-core/src/matchers.rs --- a/rust/hg-core/src/matchers.rs Tue Oct 04 10:24:56 2022 +0200 +++ b/rust/hg-core/src/matchers.rs Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca rust/hg-core/src/narrow.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/hg-core/src/narrow.rs Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca rust/hg-core/src/sparse.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/hg-core/src/sparse.rs Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca rust/hg-core/src/vfs.rs --- a/rust/hg-core/src/vfs.rs Tue Oct 04 10:24:56 2022 +0200 +++ b/rust/hg-core/src/vfs.rs Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca rust/hg-cpython/src/dirstate/status.rs --- a/rust/hg-cpython/src/dirstate/status.rs Tue Oct 04 10:24:56 2022 +0200 +++ b/rust/hg-cpython/src/dirstate/status.rs Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca rust/rhg/src/commands/debugdata.rs --- a/rust/rhg/src/commands/debugdata.rs Tue Oct 04 10:24:56 2022 +0200 +++ b/rust/rhg/src/commands/debugdata.rs Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca rust/rhg/src/commands/debugrhgsparse.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/rhg/src/commands/debugrhgsparse.rs Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca rust/rhg/src/commands/status.rs --- a/rust/rhg/src/commands/status.rs Tue Oct 04 10:24:56 2022 +0200 +++ b/rust/rhg/src/commands/status.rs Tue Oct 04 10:56:27 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,6 +27,7 @@ use hg::PatternFileWarning; use hg::StatusError; use hg::StatusOptions; +use hg::{self, narrow, sparse}; use log::info; use std::io; use std::path::PathBuf; @@ -104,6 +104,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,6 +156,33 @@ } } +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")? { @@ -178,13 +211,9 @@ let verbose = !ui.plain(None) && !args.is_present("print0") - && (config.get_bool(b"ui", b"verbose")? + && (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 +243,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 +270,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 { @@ -350,9 +360,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, @@ -545,3 +591,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 b6c6ac6ef359 -r 44bc045a43ca rust/rhg/src/error.rs --- a/rust/rhg/src/error.rs Tue Oct 04 10:24:56 2022 +0200 +++ b/rust/rhg/src/error.rs Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca rust/rhg/src/main.rs --- a/rust/rhg/src/main.rs Tue Oct 04 10:24:56 2022 +0200 +++ b/rust/rhg/src/main.rs Tue Oct 04 10:56:27 2022 +0200 @@ -372,8 +372,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 +479,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 +545,7 @@ debugdata debugrequirements debugignorerhg + debugrhgsparse files root config @@ -677,8 +677,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 +694,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 b6c6ac6ef359 -r 44bc045a43ca setup.py --- a/setup.py Tue Oct 04 10:24:56 2022 +0200 +++ b/setup.py Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca tests/run-tests.py --- a/tests/run-tests.py Tue Oct 04 10:24:56 2022 +0200 +++ b/tests/run-tests.py Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca tests/test-bisect2.t --- a/tests/test-bisect2.t Tue Oct 04 10:24:56 2022 +0200 +++ b/tests/test-bisect2.t Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca tests/test-bundle.t --- a/tests/test-bundle.t Tue Oct 04 10:24:56 2022 +0200 +++ b/tests/test-bundle.t Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca tests/test-chg.t --- a/tests/test-chg.t Tue Oct 04 10:24:56 2022 +0200 +++ b/tests/test-chg.t Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca tests/test-completion.t --- a/tests/test-completion.t Tue Oct 04 10:24:56 2022 +0200 +++ b/tests/test-completion.t Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca tests/test-contrib-perf.t --- a/tests/test-contrib-perf.t Tue Oct 04 10:24:56 2022 +0200 +++ b/tests/test-contrib-perf.t Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca tests/test-http-bad-server.t --- a/tests/test-http-bad-server.t Tue Oct 04 10:24:56 2022 +0200 +++ b/tests/test-http-bad-server.t Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca tests/test-log.t --- a/tests/test-log.t Tue Oct 04 10:24:56 2022 +0200 +++ b/tests/test-log.t Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca tests/test-mq-subrepo-svn.t --- a/tests/test-mq-subrepo-svn.t Tue Oct 04 10:24:56 2022 +0200 +++ b/tests/test-mq-subrepo-svn.t Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca tests/test-phase-archived.t --- a/tests/test-phase-archived.t Tue Oct 04 10:24:56 2022 +0200 +++ b/tests/test-phase-archived.t Tue Oct 04 10:56:27 2022 +0200 @@ -4,7 +4,7 @@ $ cat << EOF >> $HGRCPATH > [format] - > internal-phase=yes + > exp-archived-phase=yes > [extensions] > strip= > [experimental] diff -r b6c6ac6ef359 -r 44bc045a43ca tests/test-phases.t --- a/tests/test-phases.t Tue Oct 04 10:24:56 2022 +0200 +++ b/tests/test-phases.t Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca tests/test-revset.t --- a/tests/test-revset.t Tue Oct 04 10:24:56 2022 +0200 +++ b/tests/test-revset.t Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca tests/test-rhg-sparse-narrow.t --- a/tests/test-rhg-sparse-narrow.t Tue Oct 04 10:24:56 2022 +0200 +++ b/tests/test-rhg-sparse-narrow.t Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca tests/test-shelve.t --- a/tests/test-shelve.t Tue Oct 04 10:24:56 2022 +0200 +++ b/tests/test-shelve.t Tue Oct 04 10:56:27 2022 +0200 @@ -14,7 +14,7 @@ $ cat <> $HGRCPATH > [format] - > internal-phase = yes + > use-internal-phase = yes > EOF #endif @@ -243,12 +243,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' @@ -1534,4 +1534,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 b6c6ac6ef359 -r 44bc045a43ca tests/test-shelve2.t --- a/tests/test-shelve2.t Tue Oct 04 10:24:56 2022 +0200 +++ b/tests/test-shelve2.t Tue Oct 04 10:56:27 2022 +0200 @@ -16,7 +16,7 @@ $ cat <> $HGRCPATH > [format] - > internal-phase = yes + > use-internal-phase = yes > EOF #endif diff -r b6c6ac6ef359 -r 44bc045a43ca tests/test-status.t --- a/tests/test-status.t Tue Oct 04 10:24:56 2022 +0200 +++ b/tests/test-status.t Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca tests/test-subrepo-svn.t --- a/tests/test-subrepo-svn.t Tue Oct 04 10:24:56 2022 +0200 +++ b/tests/test-subrepo-svn.t Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca tests/test-template-functions.t --- a/tests/test-template-functions.t Tue Oct 04 10:24:56 2022 +0200 +++ b/tests/test-template-functions.t Tue Oct 04 10:56:27 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 b6c6ac6ef359 -r 44bc045a43ca tests/test-update-branches.t --- a/tests/test-update-branches.t Tue Oct 04 10:24:56 2022 +0200 +++ b/tests/test-update-branches.t Tue Oct 04 10:56:27 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 [ {