branching: merge stable into default
authorRaphaël Gomès <rgomes@octobus.net>
Mon, 20 Mar 2023 23:16:14 +0100
changeset 50335 c5e93c915ab6
parent 50324 dd42156b6441 (current diff)
parent 50334 972f3e5c94b8 (diff)
child 50351 4341c2271d67
branching: merge stable into default
mercurial/commands.py
mercurial/dirstate.py
mercurial/repair.py
tests/common-pattern.py
tests/test-ssh.t
--- a/.hgsigs	Thu Mar 09 13:02:13 2023 -0600
+++ b/.hgsigs	Mon Mar 20 23:16:14 2023 +0100
@@ -239,3 +239,4 @@
 c890d8b8bc59b18e5febf60caada629df5356ee2 0 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
+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
--- a/.hgtags	Thu Mar 09 13:02:13 2023 -0600
+++ b/.hgtags	Mon Mar 20 23:16:14 2023 +0100
@@ -255,3 +255,4 @@
 c890d8b8bc59b18e5febf60caada629df5356ee2 6.3.1
 59466b13a3ae0e29a5d4f485393e516cfbb057d0 6.3.2
 8830004967ad865ead89c28a410405a6e71e0796 6.3.3
+05de4896508e8ec387b33eb30d8aab78d1c8e9e4 6.4rc0
--- a/contrib/import-checker.py	Thu Mar 09 13:02:13 2023 -0600
+++ b/contrib/import-checker.py	Mon Mar 20 23:16:14 2023 +0100
@@ -232,6 +232,7 @@
     yield 'importlib.abc'  # python3 only
     yield 'importlib.machinery'  # python3 only
     yield 'importlib.util'  # python3 only
+    yield 'packaging.version'
     for m in 'fcntl', 'grp', 'pwd', 'termios':  # Unix only
         yield m
     for m in 'cPickle', 'datetime':  # in Python (not C) on PyPy
--- a/hgext/histedit.py	Thu Mar 09 13:02:13 2023 -0600
+++ b/hgext/histedit.py	Mon Mar 20 23:16:14 2023 +0100
@@ -1427,11 +1427,11 @@
         for y in range(0, length):
             line = output[y]
             if diffcolors:
-                if line and line[0] == b'+':
+                if line.startswith(b'+'):
                     win.addstr(
                         y, 0, line, curses.color_pair(COLOR_DIFF_ADD_LINE)
                     )
-                elif line and line[0] == b'-':
+                elif line.startswith(b'-'):
                     win.addstr(
                         y, 0, line, curses.color_pair(COLOR_DIFF_DEL_LINE)
                     )
--- a/hgext/narrow/narrowbundle2.py	Thu Mar 09 13:02:13 2023 -0600
+++ b/hgext/narrow/narrowbundle2.py	Mon Mar 20 23:16:14 2023 +0100
@@ -6,7 +6,6 @@
 # GNU General Public License version 2 or any later version.
 
 
-import errno
 import struct
 
 from mercurial.i18n import _
@@ -20,10 +19,10 @@
     repair,
     requirements,
     scmutil,
+    transaction,
     util,
     wireprototypes,
 )
-from mercurial.utils import stringutil
 
 _NARROWACL_SECTION = b'narrowacl'
 _CHANGESPECPART = b'narrow:changespec'
@@ -295,16 +294,7 @@
     finally:
         f.close()
 
-    # remove undo files
-    for undovfs, undofile in repo.undofiles():
-        try:
-            undovfs.unlink(undofile)
-        except OSError as e:
-            if e.errno != errno.ENOENT:
-                ui.warn(
-                    _(b'error removing %s: %s\n')
-                    % (undovfs.join(undofile), stringutil.forcebytestr(e))
-                )
+    transaction.cleanup_undo_files(repo.ui.warn, repo.vfs_map)
 
     # Remove partial backup only if there were no exceptions
     op._widen_uninterr.__exit__(None, None, None)
--- a/mercurial/cext/dirs.c	Thu Mar 09 13:02:13 2023 -0600
+++ b/mercurial/cext/dirs.c	Mon Mar 20 23:16:14 2023 +0100
@@ -13,7 +13,11 @@
 
 #include "util.h"
 
+#if PY_VERSION_HEX >= 0x030C00A5
+#define PYLONG_VALUE(o) ((PyLongObject *)o)->long_value.ob_digit[0]
+#else
 #define PYLONG_VALUE(o) ((PyLongObject *)o)->ob_digit[0]
+#endif
 
 /*
  * This is a multiset of directory names, built from the files that
--- a/mercurial/commands.py	Thu Mar 09 13:02:13 2023 -0600
+++ b/mercurial/commands.py	Mon Mar 20 23:16:14 2023 +0100
@@ -2507,7 +2507,7 @@
     """
     opts = pycompat.byteskwargs(opts)
 
-    context = repo.dirstate.changing_files
+    context = lambda repo: repo.dirstate.changing_files(repo)
     rev = opts.get(b'at_rev')
     ctx = None
     if rev:
@@ -6019,7 +6019,7 @@
     Returns 0 on success, 1 if errors are encountered.
     """
     opts = pycompat.byteskwargs(opts)
-    context = repo.dirstate.changing_files
+    context = lambda repo: repo.dirstate.changing_files(repo)
     rev = opts.get(b'at_rev')
     ctx = None
     if rev:
--- a/mercurial/dirstate.py	Thu Mar 09 13:02:13 2023 -0600
+++ b/mercurial/dirstate.py	Mon Mar 20 23:16:14 2023 +0100
@@ -200,6 +200,12 @@
         self._cwd
 
     def refresh(self):
+        # XXX if this happens, you likely did not enter the `changing_xxx`
+        # using `repo.dirstate`, so a later `repo.dirstate` accesss might call
+        # `refresh`.
+        if self.is_changing_any:
+            msg = "refreshing the dirstate in the middle of a change"
+            raise error.ProgrammingError(msg)
         if '_branch' in vars(self):
             del self._branch
         if '_map' in vars(self) and self._map.may_need_refresh():
--- a/mercurial/interfaces/repository.py	Thu Mar 09 13:02:13 2023 -0600
+++ b/mercurial/interfaces/repository.py	Mon Mar 20 23:16:14 2023 +0100
@@ -1524,6 +1524,10 @@
         """Name of the repoview that is active on this repo."""
     )
 
+    vfs_map = interfaceutil.Attribute(
+        """a bytes-key → vfs mapping used by transaction and others"""
+    )
+
     wvfs = interfaceutil.Attribute(
         """VFS used to access the working directory."""
     )
--- a/mercurial/localrepo.py	Thu Mar 09 13:02:13 2023 -0600
+++ b/mercurial/localrepo.py	Mon Mar 20 23:16:14 2023 +0100
@@ -1565,6 +1565,14 @@
 
         return checksvfs
 
+    @property
+    def vfs_map(self):
+        return {
+            b'': self.svfs,
+            b'plain': self.vfs,
+            b'store': self.svfs,
+        }
+
     def close(self):
         self._writecaches()
 
@@ -2410,12 +2418,11 @@
         self.hook(b'pretxnopen', throw=True, txnname=desc, txnid=txnid)
 
         self._writejournal(desc)
-        renames = [(vfs, x, undoname(x)) for vfs, x in self._journalfiles()]
         if report:
             rp = report
         else:
             rp = self.ui.warn
-        vfsmap = {b'plain': self.vfs, b'store': self.svfs}  # root of .hg/
+        vfsmap = self.vfs_map
         # we must avoid cyclic reference between repo and transaction.
         reporef = weakref.ref(self)
         # Code to track tag movement
@@ -2568,13 +2575,15 @@
             vfsmap,
             b"journal",
             b"undo",
-            aftertrans(renames),
+            lambda: None,
             self.store.createmode,
             validator=validate,
             releasefn=releasefn,
             checkambigfiles=_cachedfiles,
             name=desc,
         )
+        for vfs_id, path in self._journalfiles():
+            tr.add_journal(vfs_id, path)
         tr.changes[b'origrepolen'] = len(self)
         tr.changes[b'obsmarkers'] = set()
         tr.changes[b'phases'] = []
@@ -2704,10 +2713,7 @@
         with self.lock():
             if self.svfs.exists(b"journal"):
                 self.ui.status(_(b"rolling back interrupted transaction\n"))
-                vfsmap = {
-                    b'': self.svfs,
-                    b'plain': self.vfs,
-                }
+                vfsmap = self.vfs_map
                 transaction.rollback(
                     self.svfs,
                     vfsmap,
@@ -2775,7 +2781,7 @@
             return 0
 
         self.destroying()
-        vfsmap = {b'plain': self.vfs, b'': self.svfs}
+        vfsmap = self.vfs_map
         skip_journal_pattern = None
         if not parentgone:
             skip_journal_pattern = RE_SKIP_DIRSTATE_ROLLBACK
@@ -2945,6 +2951,7 @@
         known good state)."""
         unfi = self.unfiltered()
         if 'dirstate' in unfi.__dict__:
+            assert not self.dirstate.is_changing_any
             del unfi.__dict__['dirstate']
 
     def invalidate(self, clearfilecache=False):
@@ -3542,24 +3549,6 @@
         self._sidedata_computers[kind][category] = (keys, computer, flags)
 
 
-# used to avoid circular references so destructors work
-def aftertrans(files):
-    renamefiles = [tuple(t) for t in files]
-
-    def a():
-        for vfs, src, dest in renamefiles:
-            # if src and dest refer to a same file, vfs.rename is a no-op,
-            # leaving both src and dest on disk. delete dest to make sure
-            # the rename couldn't be such a no-op.
-            vfs.tryunlink(dest)
-            try:
-                vfs.rename(src, dest)
-            except FileNotFoundError:  # journal file does not yet exist
-                pass
-
-    return a
-
-
 def undoname(fn: bytes) -> bytes:
     base, name = os.path.split(fn)
     assert name.startswith(b'journal')
--- a/mercurial/match.py	Thu Mar 09 13:02:13 2023 -0600
+++ b/mercurial/match.py	Mon Mar 20 23:16:14 2023 +0100
@@ -196,14 +196,14 @@
     ...     return match(util.localpath(root), *args, **kwargs)
 
     Usually a patternmatcher is returned:
-    >>> _match(b'/foo', b'.', [b're:.*\.c$', b'path:foo/a', b'*.py'])
+    >>> _match(b'/foo', b'.', [br're:.*\.c$', b'path:foo/a', b'*.py'])
     <patternmatcher patterns='.*\\.c$|foo/a(?:/|$)|[^/]*\\.py$'>
 
     Combining 'patterns' with 'include' (resp. 'exclude') gives an
     intersectionmatcher (resp. a differencematcher):
-    >>> type(_match(b'/foo', b'.', [b're:.*\.c$'], include=[b'path:lib']))
+    >>> type(_match(b'/foo', b'.', [br're:.*\.c$'], include=[b'path:lib']))
     <class 'mercurial.match.intersectionmatcher'>
-    >>> type(_match(b'/foo', b'.', [b're:.*\.c$'], exclude=[b'path:build']))
+    >>> type(_match(b'/foo', b'.', [br're:.*\.c$'], exclude=[b'path:build']))
     <class 'mercurial.match.differencematcher'>
 
     Notice that, if 'patterns' is empty, an alwaysmatcher is returned:
@@ -212,7 +212,7 @@
 
     The 'default' argument determines which kind of pattern is assumed if a
     pattern has no prefix:
-    >>> _match(b'/foo', b'.', [b'.*\.c$'], default=b're')
+    >>> _match(b'/foo', b'.', [br'.*\.c$'], default=b're')
     <patternmatcher patterns='.*\\.c$'>
     >>> _match(b'/foo', b'.', [b'main.py'], default=b'relpath')
     <patternmatcher patterns='main\\.py(?:/|$)'>
@@ -223,7 +223,7 @@
     name) matches againset one of the patterns given at initialization. There
     are two ways of doing this check.
 
-    >>> m = _match(b'/foo', b'', [b're:.*\.c$', b'relpath:a'])
+    >>> m = _match(b'/foo', b'', [br're:.*\.c$', b'relpath:a'])
 
     1. Calling the matcher with a file name returns True if any pattern
     matches that file name:
--- a/mercurial/repair.py	Thu Mar 09 13:02:13 2023 -0600
+++ b/mercurial/repair.py	Mon Mar 20 23:16:14 2023 +0100
@@ -7,8 +7,6 @@
 # GNU General Public License version 2 or any later version.
 
 
-import errno
-
 from .i18n import _
 from .node import (
     hex,
@@ -26,11 +24,11 @@
     phases,
     requirements,
     scmutil,
+    transaction,
     util,
 )
 from .utils import (
     hashutil,
-    stringutil,
     urlutil,
 )
 
@@ -270,19 +268,7 @@
                 bmchanges = [(m, repo[newbmtarget].node()) for m in updatebm]
                 repo._bookmarks.applychanges(repo, tr, bmchanges)
 
-            # remove undo files
-            for undovfs, undofile in repo.undofiles():
-                try:
-                    undovfs.unlink(undofile)
-                except OSError as e:
-                    if e.errno != errno.ENOENT:
-                        ui.warn(
-                            _(b'error removing %s: %s\n')
-                            % (
-                                undovfs.join(undofile),
-                                stringutil.forcebytestr(e),
-                            )
-                        )
+            transaction.cleanup_undo_files(repo.ui.warn, repo.vfs_map)
 
         except:  # re-raises
             if backupfile:
--- a/mercurial/statprof.py	Thu Mar 09 13:02:13 2023 -0600
+++ b/mercurial/statprof.py	Mon Mar 20 23:16:14 2023 +0100
@@ -540,7 +540,11 @@
 
     for stat in stats:
         site = stat.site
-        sitelabel = b'%s:%d:%s' % (site.filename(), site.lineno, site.function)
+        sitelabel = b'%s:%d:%s' % (
+            site.filename(),
+            site.lineno or -1,
+            site.function,
+        )
         fp.write(
             b'%6.2f %9.2f %9.2f  %s\n'
             % (
@@ -613,7 +617,7 @@
                 stattuple = (
                     stat.selfpercent(),
                     stat.selfseconds(),
-                    stat.site.lineno,
+                    stat.site.lineno or -1,
                     source,
                 )
 
--- a/mercurial/streamclone.py	Thu Mar 09 13:02:13 2023 -0600
+++ b/mercurial/streamclone.py	Mon Mar 20 23:16:14 2023 +0100
@@ -7,7 +7,6 @@
 
 
 import contextlib
-import errno
 import os
 import struct
 
@@ -24,14 +23,12 @@
     requirements as requirementsmod,
     scmutil,
     store,
+    transaction,
     util,
 )
 from .revlogutils import (
     nodemap,
 )
-from .utils import (
-    stringutil,
-)
 
 
 def new_stream_clone_requirements(default_requirements, streamed_requirements):
@@ -935,15 +932,4 @@
             dest_repo.store.write(tr)
 
         # clean up transaction file as they do not make sense
-        undo_files = [(dest_repo.svfs, b'undo.backupfiles')]
-        undo_files.extend(dest_repo.undofiles())
-        for undovfs, undofile in undo_files:
-            try:
-                undovfs.unlink(undofile)
-            except OSError as e:
-                if e.errno != errno.ENOENT:
-                    msg = _(b'error removing %s: %s\n')
-                    path = undovfs.join(undofile)
-                    e_msg = stringutil.forcebytestr(e)
-                    msg %= (path, e_msg)
-                    dest_repo.ui.warn(msg)
+        transaction.cleanup_undo_files(dest_repo.ui.warn, dest_repo.vfs_map)
--- a/mercurial/transaction.py	Thu Mar 09 13:02:13 2023 -0600
+++ b/mercurial/transaction.py	Mon Mar 20 23:16:14 2023 +0100
@@ -11,6 +11,8 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
+import errno
+import os
 
 from .i18n import _
 from . import (
@@ -38,6 +40,61 @@
     return _active
 
 
+UNDO_BACKUP = b'%s.backupfiles'
+
+UNDO_FILES_MAY_NEED_CLEANUP = [
+    # legacy entries that might exists on disk from previous version:
+    (b'store', b'%s.narrowspec'),
+    (b'plain', b'%s.narrowspec.dirstate'),
+    (b'plain', b'%s.branch'),
+    (b'plain', b'%s.bookmarks'),
+    (b'store', b'%s.phaseroots'),
+    (b'plain', b'%s.dirstate'),
+    # files actually in uses today:
+    (b'plain', b'%s.desc'),
+    # Always delete undo last to make sure we detect that a clean up is needed if
+    # the process is interrupted.
+    (b'store', b'%s'),
+]
+
+
+def cleanup_undo_files(report, vfsmap, undo_prefix=b'undo'):
+    """remove "undo" files used by the rollback logic
+
+    This is useful to prevent rollback running in situation were it does not
+    make sense. For example after a strip.
+    """
+    backup_listing = UNDO_BACKUP % undo_prefix
+
+    backup_entries = []
+    undo_files = []
+    svfs = vfsmap[b'store']
+    try:
+        with svfs(backup_listing) as f:
+            backup_entries = read_backup_files(report, f)
+    except OSError as e:
+        if e.errno != errno.ENOENT:
+            msg = _(b'could not read %s: %s\n')
+            msg %= (svfs.join(backup_listing), stringutil.forcebytestr(e))
+            report(msg)
+
+    for location, f, backup_path, c in backup_entries:
+        if location in vfsmap and backup_path:
+            undo_files.append((vfsmap[location], backup_path))
+
+    undo_files.append((svfs, backup_listing))
+    for location, undo_path in UNDO_FILES_MAY_NEED_CLEANUP:
+        undo_files.append((vfsmap[location], undo_path % undo_prefix))
+    for undovfs, undofile in undo_files:
+        try:
+            undovfs.unlink(undofile)
+        except OSError as e:
+            if e.errno != errno.ENOENT:
+                msg = _(b'error removing %s: %s\n')
+                msg %= (undovfs.join(undofile), stringutil.forcebytestr(e))
+                report(msg)
+
+
 def _playback(
     journal,
     report,
@@ -152,6 +209,7 @@
         self._offsetmap = {}
         self._newfiles = set()
         self._journal = journalname
+        self._journal_files = []
         self._undoname = undoname
         self._queue = []
         # A callback to do something just after releasing transaction.
@@ -632,10 +690,25 @@
         scope)"""
         self._abort()
 
+    @active
+    def add_journal(self, vfs_id, path):
+        self._journal_files.append((vfs_id, path))
+
     def _writeundo(self):
         """write transaction data for possible future undo call"""
         if self._undoname is None:
             return
+        cleanup_undo_files(
+            self._report,
+            self._vfsmap,
+            undo_prefix=self._undoname,
+        )
+
+        def undoname(fn: bytes) -> bytes:
+            base, name = os.path.split(fn)
+            assert name.startswith(self._journal)
+            new_name = name.replace(self._journal, self._undoname, 1)
+            return os.path.join(base, new_name)
 
         undo_backup_path = b"%s.backupfiles" % self._undoname
         undobackupfile = self._opener.open(undo_backup_path, b'w')
@@ -653,13 +726,20 @@
                     )
                     continue
                 vfs = self._vfsmap[l]
-                base, name = vfs.split(b)
-                assert name.startswith(self._journal), name
-                uname = name.replace(self._journal, self._undoname, 1)
-                u = vfs.reljoin(base, uname)
+                u = undoname(b)
                 util.copyfile(vfs.join(b), vfs.join(u), hardlink=True)
             undobackupfile.write(b"%s\0%s\0%s\0%d\n" % (l, f, u, c))
         undobackupfile.close()
+        for vfs, src in self._journal_files:
+            dest = undoname(src)
+            # if src and dest refer to a same file, vfs.rename is a no-op,
+            # leaving both src and dest on disk. delete dest to make sure
+            # the rename couldn't be such a no-op.
+            vfs.tryunlink(dest)
+            try:
+                vfs.rename(src, dest)
+            except FileNotFoundError:  # journal file does not yet exist
+                pass
 
     def _abort(self):
         entries = self.readjournal()
@@ -737,6 +817,32 @@
 )
 
 
+def read_backup_files(report, fp):
+    """parse an (already open) backup file an return contained backup entries
+
+    entries are in the form: (location, file, backupfile, xxx)
+
+    :location:   the vfs identifier (vfsmap's key)
+    :file:       original file path (in the vfs)
+    :backupfile: path of the backup (in the vfs)
+    :cache:      a boolean currently always set to False
+    """
+    lines = fp.readlines()
+    backupentries = []
+    if lines:
+        ver = lines[0][:-1]
+        if ver != (b'%d' % version):
+            report(BAD_VERSION_MSG)
+        else:
+            for line in lines[1:]:
+                if line:
+                    # Shave off the trailing newline
+                    line = line[:-1]
+                    l, f, b, c = line.split(b'\0')
+                    backupentries.append((l, f, b, bool(c)))
+    return backupentries
+
+
 def rollback(
     opener,
     vfsmap,
@@ -776,19 +882,8 @@
 
     backupjournal = b"%s.backupfiles" % file
     if opener.exists(backupjournal):
-        fp = opener.open(backupjournal)
-        lines = fp.readlines()
-        if lines:
-            ver = lines[0][:-1]
-            if ver != (b'%d' % version):
-                report(BAD_VERSION_MSG)
-            else:
-                for line in lines[1:]:
-                    if line:
-                        # Shave off the trailing newline
-                        line = line[:-1]
-                        l, f, b, c = line.split(b'\0')
-                        backupentries.append((l, f, b, bool(c)))
+        with opener.open(backupjournal) as fp:
+            backupentries = read_backup_files(report, fp)
     if skip_journal_pattern is not None:
         keep = lambda x: not skip_journal_pattern.match(x[1])
         backupentries = [x for x in backupentries if keep(x)]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/relnotes/6.4	Mon Mar 20 23:16:14 2023 +0100
@@ -0,0 +1,144 @@
+= Mercurial 6.4rc0 =
+
+== New Features ==
+
+ * There is a new internal merge tool called `internal:union-other-first`.
+   It works like `internal:union` but add other side on top of local.
+ * Pullbundles are enabled by default
+ * delta-find: add a way to control the number of bases tested at the same time
+ * changelog-v2: add a configuration to disable rank computation
+ * debug: add an option to display statistic about a bundling operation
+ * debug: add an option to display statistic about a unbundling operation
+ * delta-find: add a delta-reuse policy that blindly accepts incoming deltas
+ * debug: add debug-revlog-stats command
+ * dirstate: add narrow support to `verify`
+ * verify: also check dirstate
+ * commit: add --draft option to use draft phase
+ * amend: add a --draft option to set phase to draft
+ * debug: add a config to abort update early
+ * rhg: implement checkexec to support weird filesystems
+ * debugshell: allow commands to be specified as a CLI argument
+ * rhg-files: add support for narrow when specifying a revision
+ * rust-narrow: enable narrow support for plain `rhg files`
+
+== Bug Fixes ==
+
+Aside from the following (unordered) commits which made it through a manual filter, there are a bunch of typing improvements and fixes, removal of deprecated code and general code cleanup.
+
+ * lfs: improve an exception message for blob corruption detected on transfer
+ * revlog: use the user facing filename as the display_id for filelogs
+ * rust-status: query fs traversal metadata lazily
+ * shelve: add Shelf.changed_files for resolving changed files in a plugin
+ * demandimport: ensure lazyloaderex sets loader attributes (issue6725)
+ * typing: fix a syntax error in mercurial/cext/bdiff.pyi
+ * cffi: fix a bytes vs str issue on macOS when listing directories
+ * changelog-v2: fix the docket `struct`
+ * schemes: fix a broken check for drive letter conflicts
+ * worker: avoid reading 1 byte at a time from the OS pipe
+ * rust-narrow: fix loop that never loops
+ * setup: Ensure target directory exists with building rust extension
+ * dirstate: invalidate changes when parent-change fails
+ * dirstate: warn about non-explicitly rolledback parent-change
+ * dirstate: write dirstate on successful exit of changing_parents context
+ * largefile: make sure we hold the lock when updating the second dirstate
+ * dirstate: enforce holding the lock while doing any changes
+ * run-tests: stop ignoring venv-installed packages
+ * transaction: run abort callback in all cases
+ * transaction: quietly rollback if no other changes than temporary files
+ * debugrebuilddirstate: double check that no transaction is open
+ * dirstate: do not write an empty dirstate just for backup
+ * locking: take the `wlock` for the full `hg add` duration
+ * locking: take the `wlock` for the full `hg remove` duration
+ * locking: take the `wlock` for the full `hg forget` duration
+ * locking: take the `wlock` for the full `hg addremove` duration
+ * locking: grab the wlock before touching the dirstate in `perfdirstatewrite`
+ * locking: hold the wlock for the full duration of the "keyword demo"
+ * mq: properly take the wlock during the full qfold operation
+ * dirstate: invalidate the dirstate change on transaction failure
+ * status: fix post status writing
+ * status: fix post status invalidation
+ * dirstate: avoid transaction backup/restore if we do not hold the lock
+ * rollback: explicitly skip dirstate rollback when applicable
+ * dirstate-guard: remove the feature
+ * dirstate: make `restorebackup` more robust when it is a noop
+ * dirstate: generalize the dirstate's invalidation on transaction abort
+ * dirstate: detect potential fishy transaction patterns while changing
+ * mq: write the dirstate before stripping
+ * dirstate: explicitly backup the datafile
+ * localrepo: enforce a clean dirstate when the transaction open
+ * localrepo: "blindly" do a dirstate backup at the end of the transaction
+ * dirstate: remove the dedicated backup logic
+ * rhg: fix a bug in path_encode
+ * dirstate: invalidate on all exceptions
+ * large-files: make sure we write newly initialized standin file early
+ * dirstate: warn if dirty when starting an edition
+ * dirstate: track that changes are pending in a transaction
+ * dirstate: distinct transaction callback from largefile
+ * automv: lock the repository before searching for renames
+ * dirstate: only reload the dirstate when it may have changed
+ * dirstate: cleanup the `_map` property cache
+ * status: invalidate dirstate  on LockError
+ * dirstate: check that dirstate is clean at the initial context opening
+ * dirstate: have `running_status` write the dirstate when holding the lock
+ * dirstate: have `running_status` warn when exiting with a dirty dirstate
+ * narrow: widden the lock context in `tracking`
+ * narrow: enforce that narrow spec is written within a transaction
+ * transaction: no longer explicitly cache phaseroots
+ * transaction: no longer explicitly cache bookmarks
+ * transaction: use the standard transaction mechanism to backup branch
+ * bundlerepo: handle changegroup induced phase movement in the associated method
+ * bundlerepo: apply phase data stored in the bundle instead of assuming `draft`
+ * config-item: declare undeclared path suboption
+ * narrow: read pending file when applicable
+
+== Backwards Compatibility Changes ==
+ * rust: upgrade supported Rust toolchain version
+ * rust: move all crates in the main workspace to edition 2021
+ * hg-core: upgrade `zstd` dependency
+ * hg-core: upgrade `clap` dependency
+ * hg-core: upgrade all remaining dependencies
+ * hg-cpython: upgrade dependencies
+ * rhg: upgrade `clap` dependency
+ * rhg: upgrade the remainder of the dependencies
+
+== Internal API Changes ==
+
+ * Many APIs around the dirstate have been made much stricter with regards to
+   locking and transaction handling
+ * Some dirstate APIs have been renamed/removed
+ * In both cases, you should get loud complaints in your tests if you do
+   something wrong.
+
+== Miscellaneous ==
+
+ * pullbundle support no longer requires setting a server-side option,
+   providing a .hg/pullbundles.manifest according to the syntax specified in
+   'hg help -e clonebundles' is enough.
+ * debug-delta-find: add a --source option
+ * delta-find: add debug information about reuse of cached data
+ * delta-find: set the default candidate chunk size to 10
+ * attr: vendor 22.1.0
+ * configitems: add a default value for "merge-tools.xxx.regappend"
+ * debugrevlog: display total stored information
+ * emitrevision: if we need to compute a delta on the fly, try p1 or p2 first
+ * emitrevision: consider ancestors revision to emit as available base
+ * find-delta: pass the cache-delta usage policy alongside the cache-delta
+ * delta-find: use a smarter object for snapshot caching
+ * delta-find: use sets instead of list in the snapshot cache
+ * delta-find: make sure we only use newer full snapshot as candidate
+ * delta-find: use a single snapshot cache when applying a group to an object
+ * bundleoperation: optionnaly record the `remote` that produced the bundle
+ * bundle: when forcing acceptance of incoming delta also accept snapshot
+ * bundle: emit full snapshot as is, without doing a redelta
+ * pathutil: slightly faster path audit in the common case
+ * merge: don't pay for pathconflicts if there are none
+ * merge: short-circuit the _checkfs loop upon getting ENOENT
+ * merge: disable the whole filesystem access loop if [_realfs] is false
+ * merge: cache the fs checks made during [_checkunknownfiles]
+ * rust: use `logging_timer` instead of `micro_timer`
+ * rust: run `cargo clippy`
+ * makefile: add `cargo clippy` to tests if cargo is available
+ * heptapod-ci: add `clippy` to the CI
+ * convert: use a priority queue for sorting commits, to make sorting faster
+ * delta-find: adjust the default candidate group chunk size
+ * delta-find: declare the "paths..*:pulled-delta-reuse-policy option
\ No newline at end of file
--- a/relnotes/next	Thu Mar 09 13:02:13 2023 -0600
+++ b/relnotes/next	Mon Mar 20 23:16:14 2023 +0100
@@ -2,9 +2,6 @@
 
 == New Features ==
 
- * There is a new internal merge tool called `internal:union-other-first`.
-   It works like `internal:union` but add other side on top of local.
-
 == Default Format Change ==
 
 These changes affect newly created repositories (or new clones) done with
@@ -19,7 +16,3 @@
 == Internal API Changes ==
 
 == Miscellaneous ==
-
- * pullbundle support no longer requires setting a server-side option,
-   providing a .hg/pullbundles.manifest according to the syntax specified in
-   'hg help -e clonebundles' is enough.
--- a/rust/Cargo.lock	Thu Mar 09 13:02:13 2023 -0600
+++ b/rust/Cargo.lock	Mon Mar 20 23:16:14 2023 +0100
@@ -1023,9 +1023,9 @@
 
 [[package]]
 name = "rayon"
-version = "1.6.1"
+version = "1.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6db3a213adf02b3bcfd2d3846bb41cb22857d131789e01df434fb7e7bc0759b7"
+checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b"
 dependencies = [
  "either",
  "rayon-core",
@@ -1033,9 +1033,9 @@
 
 [[package]]
 name = "rayon-core"
-version = "1.10.2"
+version = "1.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "356a0625f1954f730c0201cdab48611198dc6ce21f4acff55089b5a78e6e835b"
+checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d"
 dependencies = [
  "crossbeam-channel",
  "crossbeam-deque",
@@ -1425,18 +1425,18 @@
 
 [[package]]
 name = "zstd"
-version = "0.11.2+zstd.1.5.2"
+version = "0.12.3+zstd.1.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4"
+checksum = "76eea132fb024e0e13fd9c2f5d5d595d8a967aa72382ac2f9d39fcc95afd0806"
 dependencies = [
  "zstd-safe",
 ]
 
 [[package]]
 name = "zstd-safe"
-version = "5.0.2+zstd.1.5.2"
+version = "6.0.4+zstd.1.5.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db"
+checksum = "7afb4b54b8910cf5447638cb54bf4e8a65cbedd783af98b98c62ffe91f185543"
 dependencies = [
  "libc",
  "zstd-sys",
@@ -1444,10 +1444,11 @@
 
 [[package]]
 name = "zstd-sys"
-version = "2.0.1+zstd.1.5.2"
+version = "2.0.7+zstd.1.5.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9fd07cbbc53846d9145dbffdf6dd09a7a0aa52be46741825f5c97bdd4f73f12b"
+checksum = "94509c3ba2fe55294d752b79842c530ccfab760192521df74a081a78d2b3c7f5"
 dependencies = [
  "cc",
  "libc",
+ "pkg-config",
 ]
--- a/rust/hg-core/Cargo.toml	Thu Mar 09 13:02:13 2023 -0600
+++ b/rust/hg-core/Cargo.toml	Mon Mar 20 23:16:14 2023 +0100
@@ -24,7 +24,7 @@
 rand = "0.8.5"
 rand_pcg = "0.3.1"
 rand_distr = "0.4.3"
-rayon = "1.6.1"
+rayon = "1.7.0"
 regex = "1.7.0"
 sha-1 = "0.10.0"
 twox-hash = "1.6.3"
@@ -34,10 +34,8 @@
 crossbeam-channel = "0.5.6"
 log = "0.4.17"
 memmap2 = { version = "0.5.8", features = ["stable_deref_trait"] }
-zstd = "0.11.2"
+zstd = "0.12"
 format-bytes = "0.3.0"
-# once_cell 1.15 uses edition 2021, while the heptapod CI
-# uses an old version of Cargo that doesn't support it.
 once_cell = "1.16.0"
 
 # We don't use the `miniz-oxide` backend to not change rhg benchmarks and until
--- a/rust/hg-core/src/dirstate_tree/on_disk.rs	Thu Mar 09 13:02:13 2023 -0600
+++ b/rust/hg-core/src/dirstate_tree/on_disk.rs	Mon Mar 20 23:16:14 2023 +0100
@@ -415,9 +415,9 @@
 
     fn synthesize_unix_mode(&self) -> u32 {
         let file_type = if self.flags().contains(Flags::MODE_IS_SYMLINK) {
-            libc::S_IFLNK
+            libc::S_IFLNK as u32
         } else {
-            libc::S_IFREG
+            libc::S_IFREG as u32
         };
         let permissions = if self.flags().contains(Flags::MODE_EXEC_PERM) {
             0o755
--- a/rust/hg-core/src/dirstate_tree/status.rs	Thu Mar 09 13:02:13 2023 -0600
+++ b/rust/hg-core/src/dirstate_tree/status.rs	Mon Mar 20 23:16:14 2023 +0100
@@ -244,7 +244,7 @@
         match self.parent {
             None => false,
             Some(parent) => {
-                *(parent.cache.get_or_init(|| {
+                *(self.cache.get_or_init(|| {
                     parent.force(ignore_fn) || ignore_fn(self.path)
                 }))
             }
@@ -433,16 +433,21 @@
             return Ok(children_all_have_dirstate_node_or_are_ignored);
         }
 
+        let readdir_succeeded;
         let mut fs_entries = if let Ok(entries) = self.read_dir(
             directory_hg_path,
             &directory_entry.fs_path,
             is_at_repo_root,
         ) {
+            readdir_succeeded = true;
             entries
         } else {
             // Treat an unreadable directory (typically because of insufficient
             // permissions) like an empty directory. `self.read_dir` has
             // already called `self.io_error` so a warning will be emitted.
+            // We still need to remember that there was an error so that we
+            // know not to cache this result.
+            readdir_succeeded = false;
             Vec::new()
         };
 
@@ -495,6 +500,7 @@
             Ok(has_dirstate_node_or_is_ignored)
         })
         .try_reduce(|| true, |a, b| Ok(a && b))
+        .map(|res| res && readdir_succeeded)
     }
 
     fn traverse_fs_and_dirstate<'ancestor>(
--- a/rust/hg-core/src/vfs.rs	Thu Mar 09 13:02:13 2023 -0600
+++ b/rust/hg-core/src/vfs.rs	Mon Mar 20 23:16:14 2023 +0100
@@ -193,3 +193,13 @@
         r == 0 && buf.f_type as u32 == libc::NFS_SUPER_MAGIC as u32
     }
 }
+
+/// Similar to what Cargo does; although detecting NFS (or non-local
+/// file systems) _should_ be possible on other operating systems,
+/// we'll just assume that mmap() works there, for now; after all,
+/// _some_ functionality is better than a compile error, i.e. none at
+/// all
+#[cfg(not(target_os = "linux"))]
+pub(crate) fn is_on_nfs_mount(_path: impl AsRef<Path>) -> bool {
+    false
+}
--- a/rust/rhg/Cargo.toml	Thu Mar 09 13:02:13 2023 -0600
+++ b/rust/rhg/Cargo.toml	Mon Mar 20 23:16:14 2023 +0100
@@ -22,4 +22,4 @@
 format-bytes = "0.3.0"
 users = "0.11.0"
 which = "4.3.0"
-rayon = "1.6.1"
+rayon = "1.7.0"
--- a/tests/common-pattern.py	Thu Mar 09 13:02:13 2023 -0600
+++ b/tests/common-pattern.py	Mon Mar 20 23:16:14 2023 +0100
@@ -136,6 +136,11 @@
         # FormatMessage(ERROR_FILE_NOT_FOUND)
         br'The system cannot find the file specified',
     ),
+    br'$EACCES$': (
+        br'Permission denied \(os error 13\)',
+        # strerror
+        br'Permission denied',
+    ),
     br'$ENOTDIR$': (
         # strerror()
         br'Not a directory',
--- a/tests/run-tests.py	Thu Mar 09 13:02:13 2023 -0600
+++ b/tests/run-tests.py	Mon Mar 20 23:16:14 2023 +0100
@@ -54,6 +54,7 @@
 import json
 import multiprocessing
 import os
+import packaging.version as version
 import platform
 import queue
 import random
@@ -72,12 +73,6 @@
 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(
@@ -799,8 +794,8 @@
         try:
             import coverage
 
-            covver = version.StrictVersion(coverage.__version__).version
-            if covver < (3, 3):
+            covver = version.Version(coverage.__version__)
+            if covver < version.Version("3.3"):
                 parser.error('coverage options require coverage 3.3 or later')
         except ImportError:
             parser.error('coverage options now require the coverage package')
--- a/tests/test-blackbox.t	Thu Mar 09 13:02:13 2023 -0600
+++ b/tests/test-blackbox.t	Mon Mar 20 23:16:14 2023 +0100
@@ -322,8 +322,8 @@
 #if unix-permissions
   $ chmod -w .hg
   $ hg log -r. -T '{rev}\n' --config blackbox.maxsize=1 --debug
-  warning: cannot rename '$TESTTMP/blackboxtest3/.hg/blackbox.log.1' to '$TESTTMP/blackboxtest3/.hg/blackbox.log': Permission denied
-  warning: cannot write to blackbox.log: Permission denied
+  warning: cannot rename '$TESTTMP/blackboxtest3/.hg/blackbox.log.1' to '$TESTTMP/blackboxtest3/.hg/blackbox.log': $EACCES$
+  warning: cannot write to blackbox.log: $EACCES$
   1
   $ chmod +w .hg
 #endif
@@ -470,15 +470,16 @@
   >     raise RuntimeError('raise')
   > EOF
 
-  $ cat >> $HGRCPATH << EOF
+
+  $ hg init $TESTTMP/blackbox-exception-only --config blackbox.track=commandexception
+  $ cat >> $TESTTMP/blackbox-exception-only/.hg/hgrc << EOF
   > [blackbox]
   > track = commandexception
   > [extensions]
   > raise=$TESTTMP/raise.py
   > EOF
+  $ cd $TESTTMP/blackbox-exception-only
 
-  $ hg init $TESTTMP/blackbox-exception-only
-  $ cd $TESTTMP/blackbox-exception-only
 
 #if chg
  (chg exits 255 because it fails to receive an exit code)
@@ -495,3 +496,25 @@
   $ tail -2 .hg/blackbox.log
   RuntimeError: raise
   
+  $ cd ..
+
+Check we did not broke `hg mv`
+------------------------------
+(we did in 6.4rc)
+
+basic setup
+
+  $ hg init blackbox-file-move
+  $ cd blackbox-file-move
+  $ echo foo > foo
+  $ hg add foo
+  $ hg commit -m 'foo'
+
+copy a file
+
+  $ hg copy foo bar
+
+move a file
+
+  $ hg mv foo goo
+
--- a/tests/test-clone.t	Thu Mar 09 13:02:13 2023 -0600
+++ b/tests/test-clone.t	Mon Mar 20 23:16:14 2023 +0100
@@ -633,7 +633,7 @@
   $ mkdir a
   $ chmod 000 a
   $ hg clone a b
-  abort: Permission denied: *$TESTTMP/fail/a/.hg* (glob)
+  abort: $EACCES$: *$TESTTMP/fail/a/.hg* (glob)
   [255]
 
 Inaccessible destination
@@ -641,7 +641,7 @@
   $ hg init b
   $ cd b
   $ hg clone . ../a
-  abort: Permission denied: *../a* (glob)
+  abort: $EACCES$: *../a* (glob)
   [255]
   $ cd ..
   $ chmod 700 a
--- a/tests/test-convert.t	Thu Mar 09 13:02:13 2023 -0600
+++ b/tests/test-convert.t	Mon Mar 20 23:16:14 2023 +0100
@@ -468,7 +468,7 @@
   $ chmod 000 bogusdir
 
   $ hg convert a bogusdir
-  abort: Permission denied: *bogusdir* (glob)
+  abort: $EACCES$: *bogusdir* (glob)
   [255]
 
 user permissions should succeed
--- a/tests/test-empty.t	Thu Mar 09 13:02:13 2023 -0600
+++ b/tests/test-empty.t	Mon Mar 20 23:16:14 2023 +0100
@@ -40,7 +40,6 @@
   hgrc
   requires
   store
-  undo.backup.branch
   wcache
 
 Should be empty (except for the "basic" requires):
--- a/tests/test-hardlinks.t	Thu Mar 09 13:02:13 2023 -0600
+++ b/tests/test-hardlinks.t	Mon Mar 20 23:16:14 2023 +0100
@@ -141,8 +141,6 @@
   1 r3/.hg/store/phaseroots
   1 r3/.hg/store/requires
   1 r3/.hg/store/undo
-  1 r3/.hg/store/undo.backup.fncache (repofncache !)
-  1 r3/.hg/store/undo.backup.phaseroots
   1 r3/.hg/store/undo.backupfiles
 
 Push to repo r1 should break up most hardlinks in r2:
@@ -253,8 +251,6 @@
   2 r4/.hg/store/phaseroots
   2 r4/.hg/store/requires
   2 r4/.hg/store/undo
-  2 r4/.hg/store/undo.backup.fncache (repofncache !)
-  2 r4/.hg/store/undo.backup.phaseroots
   2 r4/.hg/store/undo.backupfiles
   [24] r4/.hg/undo.backup.branch (re)
   2 r4/\.hg/undo\.backup\.dirstate (re)
@@ -308,8 +304,6 @@
   2 r4/.hg/store/phaseroots
   2 r4/.hg/store/requires
   2 r4/.hg/store/undo
-  2 r4/.hg/store/undo.backup.fncache (repofncache !)
-  2 r4/.hg/store/undo.backup.phaseroots
   2 r4/.hg/store/undo.backupfiles
   [23] r4/.hg/undo.backup.branch (re)
   2 r4/\.hg/undo\.backup\.dirstate (re)
--- a/tests/test-help.t	Thu Mar 09 13:02:13 2023 -0600
+++ b/tests/test-help.t	Mon Mar 20 23:16:14 2023 +0100
@@ -1988,7 +1988,7 @@
 
   $ "$PYTHON" <<EOF
   > def escape(s):
-  >     return b''.join(b'\\u%x' % ord(uc) for uc in s.decode('cp932'))
+  >     return b''.join(br'\\u%x' % ord(uc) for uc in s.decode('cp932'))
   > # translation of "record" in ja_JP.cp932
   > upper = b"\x8bL\x98^"
   > # str.lower()-ed section name should be treated as different one
--- a/tests/test-hgrc.t	Thu Mar 09 13:02:13 2023 -0600
+++ b/tests/test-hgrc.t	Mon Mar 20 23:16:14 2023 +0100
@@ -59,7 +59,7 @@
 #if unix-permissions no-root
   $ chmod u-r $TESTTMP/included
   $ hg showconfig section
-  config error at $TESTTMP/hgrc:2: cannot include $TESTTMP/included (Permission denied*) (glob)
+  config error at $TESTTMP/hgrc:2: cannot include $TESTTMP/included ($EACCES$*) (glob)
   [255]
 #endif
 
--- a/tests/test-journal-exists.t	Thu Mar 09 13:02:13 2023 -0600
+++ b/tests/test-journal-exists.t	Mon Mar 20 23:16:14 2023 +0100
@@ -47,7 +47,7 @@
 
   $ hg -R foo unbundle repo.hg
   adding changesets
-  abort: Permission denied: '$TESTTMP/repo/foo/.hg/store/.00changelog.i-*' (glob)
+  abort: $EACCES$: '$TESTTMP/repo/foo/.hg/store/.00changelog.i-*' (glob)
   [255]
 
   $ if test -f foo/.hg/store/journal; then echo 'journal exists :-('; fi
--- a/tests/test-largefiles-cache.t	Thu Mar 09 13:02:13 2023 -0600
+++ b/tests/test-largefiles-cache.t	Mon Mar 20 23:16:14 2023 +0100
@@ -189,7 +189,6 @@
   $ find src/.hg/largefiles/* | egrep "(dirstate|$hash)" | sort
   src/.hg/largefiles/dirstate
   src/.hg/largefiles/e2fb5f2139d086ded2cb600d5a91a196e76bf020
-  src/.hg/largefiles/undo.backup.dirstate
 
 Verify that backwards compatibility is maintained for old storage layout
   $ mv src/.hg/largefiles/$hash share_dst/.hg/largefiles
--- a/tests/test-lock-badness.t	Thu Mar 09 13:02:13 2023 -0600
+++ b/tests/test-lock-badness.t	Mon Mar 20 23:16:14 2023 +0100
@@ -135,7 +135,7 @@
   $ hg -R b push a
   pushing to a
   searching for changes
-  abort: could not lock repository a: Permission denied
+  abort: could not lock repository a: $EACCES$
   [20]
 
   $ chmod 700 a/.hg/store
--- a/tests/test-minirst.py	Thu Mar 09 13:02:13 2023 -0600
+++ b/tests/test-minirst.py	Mon Mar 20 23:16:14 2023 +0100
@@ -154,7 +154,7 @@
 
 debugformats('options', options)
 
-fields = b"""
+fields = br"""
 :a: First item.
 :ab: Second item. Indentation and wrapping
      is handled automatically.
--- a/tests/test-permissions.t	Thu Mar 09 13:02:13 2023 -0600
+++ b/tests/test-permissions.t	Mon Mar 20 23:16:14 2023 +0100
@@ -24,7 +24,7 @@
   $ chmod -r .hg/store/data/a.i
 
   $ hg verify -q
-  abort: Permission denied: '$TESTTMP/t/.hg/store/data/a.i'
+  abort: $EACCES$: '$TESTTMP/t/.hg/store/data/a.i'
   [255]
 
   $ chmod +r .hg/store/data/a.i
@@ -36,7 +36,7 @@
   $ echo barber > a
   $ hg commit -m "2"
   trouble committing a!
-  abort: Permission denied: '$TESTTMP/t/.hg/store/data/a.i'
+  abort: $EACCES$: '$TESTTMP/t/.hg/store/data/a.i'
   [255]
 
   $ chmod -w .
@@ -64,7 +64,7 @@
 (fsmonitor makes "hg status" avoid accessing to "dir")
 
   $ hg status
-  dir: Permission denied* (glob)
+  dir: $EACCES$* (glob)
   M a
 
 #endif
--- a/tests/test-persistent-nodemap.t	Thu Mar 09 13:02:13 2023 -0600
+++ b/tests/test-persistent-nodemap.t	Mon Mar 20 23:16:14 2023 +0100
@@ -811,8 +811,7 @@
     - manifest
   
   $ ls -1 .hg/store/ | egrep '00(changelog|manifest)(\.n|-.*\.nd)'
-  undo.backup.00changelog.n
-  undo.backup.00manifest.n
+  [1]
   $ hg debugnodemap --metadata
 
 
@@ -858,8 +857,6 @@
   00changelog.n
   00manifest-*.nd (glob)
   00manifest.n
-  undo.backup.00changelog.n
-  undo.backup.00manifest.n
 
   $ hg debugnodemap --metadata
   uid: * (glob)
--- a/tests/test-push-race.t	Thu Mar 09 13:02:13 2023 -0600
+++ b/tests/test-push-race.t	Mon Mar 20 23:16:14 2023 +0100
@@ -50,6 +50,10 @@
   >         limit = 100
   >         test_default_timeout = os.environ.get('HGTEST_TIMEOUT_DEFAULT')
   >         test_timeout = os.environ.get('HGTEST_TIMEOUT')
+  >         if test_default_timeout is not None:
+  >            test_default_timeout = int(test_default_timeout)
+  >         if test_timeout is not None:
+  >            test_timeout = int(test_timeout)
   >         if (
   >             test_default_timeout is not None
   >             and test_timeout is not None
--- a/tests/test-repair-strip.t	Thu Mar 09 13:02:13 2023 -0600
+++ b/tests/test-repair-strip.t	Mon Mar 20 23:16:14 2023 +0100
@@ -53,9 +53,9 @@
   transaction abort!
   failed to truncate data/b.i
   rollback failed - please run hg recover
-  (failure reason: [Errno *] Permission denied .hg/store/data/b.i') (glob)
+  (failure reason: [Errno *] $EACCES$ .hg/store/data/b.i') (glob)
   strip failed, backup bundle
-  abort: Permission denied .hg/store/data/b.i'
+  abort: $EACCES$ .hg/store/data/b.i'
   % after update 0, strip 2
   abandoned transaction found - run hg recover
   checking changesets
@@ -89,7 +89,7 @@
   date:        Thu Jan 01 00:00:00 1970 +0000
   summary:     a
   
-  abort: Permission denied .hg/store/data/b.i'
+  abort: $EACCES$ .hg/store/data/b.i'
   % after update 0, strip 2
   checking changesets
   checking manifests
@@ -110,9 +110,9 @@
   transaction abort!
   failed to truncate 00manifest.i
   rollback failed - please run hg recover
-  (failure reason: [Errno *] Permission denied .hg/store/00manifest.i') (glob)
+  (failure reason: [Errno *] $EACCES$ .hg/store/00manifest.i') (glob)
   strip failed, backup bundle
-  abort: Permission denied .hg/store/00manifest.i'
+  abort: $EACCES$ .hg/store/00manifest.i'
   % after update 0, strip 2
   abandoned transaction found - run hg recover
   checking changesets
--- a/tests/test-ssh-bundle1.t	Thu Mar 09 13:02:13 2023 -0600
+++ b/tests/test-ssh-bundle1.t	Mon Mar 20 23:16:14 2023 +0100
@@ -403,9 +403,9 @@
   $ hg push --ssh "sh ../ssh.sh"
   pushing to ssh://user@dummy/*/remote (glob)
   searching for changes
-  remote: Permission denied
+  remote: $EACCES$
   remote: abort: pretxnopen.hg-ssh hook failed
-  remote: Permission denied
+  remote: $EACCES$
   remote: pushkey-abort: prepushkey.hg-ssh hook failed
   updating 6c0482d977a3 to public failed!
   [1]
--- a/tests/test-ssh-repoerror.t	Thu Mar 09 13:02:13 2023 -0600
+++ b/tests/test-ssh-repoerror.t	Mon Mar 20 23:16:14 2023 +0100
@@ -13,7 +13,7 @@
   $ chmod a-rx no-read
 
   $ hg id ssh://user@dummy/no-read
-  remote: abort: Permission denied: *$TESTTMP/no-read/.hg* (glob)
+  remote: abort: $EACCES$: *$TESTTMP/no-read/.hg* (glob)
   abort: no suitable response from remote hg
   [255]
 
@@ -31,7 +31,7 @@
   > done
 
   $ hg id ssh://user@dummy/other
-  remote: abort: Permission denied: '$TESTTMP/other/.hg/requires'
+  remote: abort: $EACCES$: '$TESTTMP/other/.hg/requires'
   abort: no suitable response from remote hg
   [255]
 
@@ -47,7 +47,7 @@
   $ chmod a-rx deep
 
   $ hg id ssh://user@dummy/deep/nested
-  remote: abort: Permission denied: *$TESTTMP/deep/nested/.hg* (glob)
+  remote: abort: $EACCES$: *$TESTTMP/deep/nested/.hg* (glob)
   abort: no suitable response from remote hg
   [255]
 
--- a/tests/test-ssh.t	Thu Mar 09 13:02:13 2023 -0600
+++ b/tests/test-ssh.t	Mon Mar 20 23:16:14 2023 +0100
@@ -456,7 +456,7 @@
   $ hg push --ssh "sh ../ssh.sh"
   pushing to ssh://user@dummy/*/remote (glob)
   searching for changes
-  remote: Permission denied
+  remote: $EACCES$
   remote: pretxnopen.hg-ssh hook failed
   abort: push failed on remote
   [100]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-status-committed-and-ignored.t	Mon Mar 20 23:16:14 2023 +0100
@@ -0,0 +1,33 @@
+#testcases dirstate-v1 dirstate-v2
+
+#if dirstate-v2
+  $ cat >> $HGRCPATH << EOF
+  > [format]
+  > use-dirstate-v2=1
+  > [storage]
+  > dirstate-v2.slow-path=allow
+  > EOF
+#endif
+
+  $ rm -rf r
+
+  $ hg init r
+  $ cd r
+  $ mkdir d1
+  $ mkdir d2
+  $ touch d1/f d2/f
+  $ hg commit -Am '.'
+  adding d1/f
+  adding d2/f
+  $ echo 'syntax:re' >> .hgignore
+  $ echo '^d1$' >> .hgignore
+  $ hg commit -Am "ignore d1"
+  adding .hgignore
+
+Now d1 is a directory that's both committed and ignored.
+Untracked files in d2 are still shown, but ones in d1 are ignored:
+
+  $ touch d1/g
+  $ touch d2/g
+  $ RAYON_NUM_THREADS=1 hg status
+  ? d2/g
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-status-eacces.t	Mon Mar 20 23:16:14 2023 +0100
@@ -0,0 +1,45 @@
+#testcases dirstate-v1 dirstate-v2
+
+#if dirstate-v2
+  $ cat >> $HGRCPATH << EOF
+  > [format]
+  > use-dirstate-v2=1
+  > [storage]
+  > dirstate-v2.slow-path=allow
+  > EOF
+#endif
+
+
+The proliferation of status implementations can be confusing:
+- The pure python implementation:
+(no-rhg pure !)
+- The C implementation:
+(no-rhg no-rust no-pure !)
+- The two rust implementations:
+(rhg !)
+(no-rhg rust !)
+
+  $ hg init repo1
+  $ cd repo1
+  $ mkdir d1
+  $ touch d1/x
+  $ hg commit -Am.
+  adding d1/x
+  $ touch d1/y
+  $ chmod -r d1
+  $ hg status
+  d1: $EACCES$
+  ! d1/x (rhg !)
+  ! d1/x (no-rhg rust !)
+  $ hg status
+  d1: $EACCES$
+  ! d1/x (rust !)
+  ! d1/x (no-rust rhg !)
+  $ chmod +r d1
+  $ hg status
+  ? d1/y
+
+  $ touch d1/z
+  $ hg status
+  ? d1/y
+  ? d1/z
--- a/tests/test-status.t	Thu Mar 09 13:02:13 2023 -0600
+++ b/tests/test-status.t	Mon Mar 20 23:16:14 2023 +0100
@@ -849,7 +849,7 @@
 
   $ chmod 0 subdir
   $ hg status --include subdir
-  subdir: Permission denied
+  subdir: $EACCES$
   R subdir/removed
   ! subdir/clean
   ! subdir/deleted
--- a/tests/test-template-map.t	Thu Mar 09 13:02:13 2023 -0600
+++ b/tests/test-template-map.t	Mon Mar 20 23:16:14 2023 +0100
@@ -1260,7 +1260,7 @@
   $ touch q
   $ chmod 0 q
   $ hg log --style ./q
-  abort: Permission denied: './q'
+  abort: $EACCES$: './q'
   [255]
 #endif
 
@@ -1309,7 +1309,7 @@
   $ echo 'changeset = q' >> t
 #if unix-permissions no-root
   $ hg log --style ./t
-  abort: template file ./q: Permission denied
+  abort: template file ./q: $EACCES$
   [255]
   $ rm -f q
 #endif
--- a/tests/testlib/persistent-nodemap-race-ext.py	Thu Mar 09 13:02:13 2023 -0600
+++ b/tests/testlib/persistent-nodemap-race-ext.py	Mon Mar 20 23:16:14 2023 +0100
@@ -1,4 +1,4 @@
-"""Create the race condition for issue6554
+r"""Create the race condition for issue6554
 
 The persistent nodemap issues had an issue where a second writer could
 overwrite the data that a previous write just wrote. The would break the append