changeset 51600:ee1b648e4453

branching: merge stable into default
author Raphaël Gomès <rgomes@octobus.net>
date Wed, 17 Apr 2024 12:28:48 +0200
parents b0aaffcb6fcf (current diff) 13c004b54cbe (diff)
children ea3343104f07
files mercurial/bundlecaches.py mercurial/exchange.py
diffstat 14 files changed, 566 insertions(+), 103 deletions(-) [+]
line wrap: on
line diff
--- a/mercurial/bundlecaches.py	Wed Mar 13 12:02:06 2024 +0100
+++ b/mercurial/bundlecaches.py	Wed Apr 17 12:28:48 2024 +0200
@@ -179,6 +179,9 @@
     b"obsolescence": param_bool,
     b"obsolescence-mandatory": param_bool,
     b"phases": param_bool,
+    b"changegroup": param_bool,
+    b"tagsfnodescache": param_bool,
+    b"revbranchcache": param_bool,
 }
 
 
--- a/mercurial/exchange.py	Wed Mar 13 12:02:06 2024 +0100
+++ b/mercurial/exchange.py	Wed Apr 17 12:28:48 2024 +0200
@@ -158,9 +158,8 @@
                     params[b'obsolescence-mandatory'] = b'no'
 
         if not version:
-            raise error.Abort(
-                _(b'could not identify changegroup version in bundle')
-            )
+            params[b'changegroup'] = b'no'
+            version = b'v2'
         spec = b'%s-%s' % (comp, version)
         if params:
             spec += b';'
--- a/mercurial/match.py	Wed Mar 13 12:02:06 2024 +0100
+++ b/mercurial/match.py	Wed Apr 17 12:28:48 2024 +0200
@@ -638,7 +638,10 @@
         super(patternmatcher, self).__init__(badfn)
         kindpats.sort()
 
+        roots, dirs, parents = _rootsdirsandparents(kindpats)
         self._files = _explicitfiles(kindpats)
+        self._dirs_explicit = set(dirs)
+        self._dirs = parents
         self._prefix = _prefix(kindpats)
         self._pats, self._matchfn = _buildmatch(kindpats, b'$', root)
 
@@ -647,14 +650,14 @@
             return True
         return self._matchfn(fn)
 
-    @propertycache
-    def _dirs(self):
-        return set(pathutil.dirs(self._fileset))
-
     def visitdir(self, dir):
         if self._prefix and dir in self._fileset:
             return b'all'
-        return dir in self._dirs or path_or_parents_in_set(dir, self._fileset)
+        return (
+            dir in self._dirs
+            or path_or_parents_in_set(dir, self._fileset)
+            or path_or_parents_in_set(dir, self._dirs_explicit)
+        )
 
     def visitchildrenset(self, dir):
         ret = self.visitdir(dir)
@@ -1461,7 +1464,7 @@
         allgroups = []
         regexps = []
         exact = set()
-        for (kind, pattern, _source) in kindpats:
+        for kind, pattern, _source in kindpats:
             if kind == b'filepath':
                 exact.add(pattern)
                 continue
--- a/mercurial/wireprotoserver.py	Wed Mar 13 12:02:06 2024 +0100
+++ b/mercurial/wireprotoserver.py	Wed Apr 17 12:28:48 2024 +0200
@@ -527,24 +527,34 @@
     def __init__(self, ui, repo, logfh=None, accesshidden=False):
         self._ui = ui
         self._repo = repo
-        self._fin, self._fout = ui.protectfinout()
         self._accesshidden = accesshidden
-
-        # Log write I/O to stdout and stderr if configured.
-        if logfh:
-            self._fout = util.makeloggingfileobject(
-                logfh, self._fout, b'o', logdata=True
-            )
-            ui.ferr = util.makeloggingfileobject(
-                logfh, ui.ferr, b'e', logdata=True
-            )
+        self._logfh = logfh
 
     def serve_forever(self):
         self.serveuntil(threading.Event())
-        self._ui.restorefinout(self._fin, self._fout)
 
     def serveuntil(self, ev):
         """Serve until a threading.Event is set."""
-        _runsshserver(
-            self._ui, self._repo, self._fin, self._fout, ev, self._accesshidden
-        )
+        with self._ui.protectedfinout() as (fin, fout):
+            if self._logfh:
+                # Log write I/O to stdout and stderr if configured.
+                fout = util.makeloggingfileobject(
+                    self._logfh,
+                    fout,
+                    b'o',
+                    logdata=True,
+                )
+                self._ui.ferr = util.makeloggingfileobject(
+                    self._logfh,
+                    self._ui.ferr,
+                    b'e',
+                    logdata=True,
+                )
+            _runsshserver(
+                self._ui,
+                self._repo,
+                fin,
+                fout,
+                ev,
+                self._accesshidden,
+            )
--- a/rust/hg-core/src/dirstate/dirs_multiset.rs	Wed Mar 13 12:02:06 2024 +0100
+++ b/rust/hg-core/src/dirstate/dirs_multiset.rs	Wed Apr 17 12:28:48 2024 +0200
@@ -158,14 +158,13 @@
 }
 
 impl<'a> DirsChildrenMultiset<'a> {
-    pub fn new(
+    pub fn new<I: Iterator<Item = &'a HgPathBuf>>(
         paths: impl Iterator<Item = &'a HgPathBuf>,
-        only_include: Option<&'a HashSet<impl AsRef<HgPath> + 'a>>,
+        only_include: Option<I>,
     ) -> Self {
         let mut new = Self {
             inner: HashMap::default(),
-            only_include: only_include
-                .map(|s| s.iter().map(AsRef::as_ref).collect()),
+            only_include: only_include.map(|s| s.map(AsRef::as_ref).collect()),
         };
 
         for path in paths {
--- a/rust/hg-core/src/filepatterns.rs	Wed Mar 13 12:02:06 2024 +0100
+++ b/rust/hg-core/src/filepatterns.rs	Wed Apr 17 12:28:48 2024 +0200
@@ -57,7 +57,7 @@
     RelRegexp,
     /// A path relative to repository root, which is matched non-recursively
     /// (will not match subdirectories)
-    RootFiles,
+    RootFilesIn,
     /// A file of patterns to read and include
     Include,
     /// A file of patterns to match against files under the same directory
@@ -158,7 +158,7 @@
         b"path:" => Ok(PatternSyntax::Path),
         b"filepath:" => Ok(PatternSyntax::FilePath),
         b"relpath:" => Ok(PatternSyntax::RelPath),
-        b"rootfilesin:" => Ok(PatternSyntax::RootFiles),
+        b"rootfilesin:" => Ok(PatternSyntax::RootFilesIn),
         b"relglob:" => Ok(PatternSyntax::RelGlob),
         b"relre:" => Ok(PatternSyntax::RelRegexp),
         b"glob:" => Ok(PatternSyntax::Glob),
@@ -227,7 +227,7 @@
             }
             [escape_pattern(pattern).as_slice(), b"(?:/|$)"].concat()
         }
-        PatternSyntax::RootFiles => {
+        PatternSyntax::RootFilesIn => {
             let mut res = if pattern == b"." {
                 vec![]
             } else {
@@ -316,7 +316,7 @@
         | PatternSyntax::Path
         | PatternSyntax::RelGlob
         | PatternSyntax::RelPath
-        | PatternSyntax::RootFiles => normalize_path_bytes(pattern),
+        | PatternSyntax::RootFilesIn => normalize_path_bytes(pattern),
         PatternSyntax::Include | PatternSyntax::SubInclude => {
             return Err(PatternError::NonRegexPattern(entry.clone()))
         }
@@ -342,7 +342,7 @@
         m.insert(b"path:".as_ref(), PatternSyntax::Path);
         m.insert(b"filepath:".as_ref(), PatternSyntax::FilePath);
         m.insert(b"relpath:".as_ref(), PatternSyntax::RelPath);
-        m.insert(b"rootfilesin:".as_ref(), PatternSyntax::RootFiles);
+        m.insert(b"rootfilesin:".as_ref(), PatternSyntax::RootFilesIn);
         m.insert(b"relglob:".as_ref(), PatternSyntax::RelGlob);
         m.insert(b"relre:".as_ref(), PatternSyntax::RelRegexp);
         m.insert(b"glob:".as_ref(), PatternSyntax::Glob);
@@ -385,7 +385,7 @@
         | PatternSyntax::Glob
         | PatternSyntax::RelGlob
         | PatternSyntax::RelPath
-        | PatternSyntax::RootFiles
+        | PatternSyntax::RootFilesIn
             if normalize =>
         {
             normalize_path_bytes(pattern_bytes)
--- a/rust/hg-core/src/matchers.rs	Wed Mar 13 12:02:06 2024 +0100
+++ b/rust/hg-core/src/matchers.rs	Wed Apr 17 12:28:48 2024 +0200
@@ -17,7 +17,7 @@
         PatternFileWarning, PatternResult,
     },
     utils::{
-        files::find_dirs,
+        files::{dir_ancestors, find_dirs},
         hg_path::{HgPath, HgPathBuf, HgPathError},
         Escaped,
     },
@@ -35,12 +35,14 @@
 pub enum VisitChildrenSet {
     /// Don't visit anything
     Empty,
-    /// Only visit this directory
+    /// Visit this directory and probably its children
     This,
-    /// Visit this directory and these subdirectories
+    /// Only visit the children (both files and directories) if they
+    /// are mentioned in this set. (empty set corresponds to [Empty])
     /// TODO Should we implement a `NonEmptyHashSet`?
     Set(HashSet<HgPathBuf>),
     /// Visit this directory and all subdirectories
+    /// (you can stop asking about the children set)
     Recursive,
 }
 
@@ -297,6 +299,7 @@
     /// Whether all the patterns match a prefix (i.e. recursively)
     prefix: bool,
     files: HashSet<HgPathBuf>,
+    dirs_explicit: HashSet<HgPathBuf>,
     dirs: DirsMultiset,
 }
 
@@ -313,8 +316,13 @@
 
 impl<'a> PatternMatcher<'a> {
     pub fn new(ignore_patterns: Vec<IgnorePattern>) -> PatternResult<Self> {
-        let (files, _) = roots_and_dirs(&ignore_patterns);
-        let dirs = DirsMultiset::from_manifest(&files)?;
+        let RootsDirsAndParents {
+            roots,
+            dirs: dirs_explicit,
+            parents,
+        } = roots_dirs_and_parents(&ignore_patterns)?;
+        let files = roots;
+        let dirs = parents;
         let files: HashSet<HgPathBuf> = HashSet::from_iter(files);
 
         let prefix = ignore_patterns.iter().all(|k| {
@@ -328,6 +336,7 @@
             prefix,
             files,
             dirs,
+            dirs_explicit,
         })
     }
 }
@@ -352,9 +361,13 @@
         if self.prefix && self.files.contains(directory) {
             return VisitChildrenSet::Recursive;
         }
-        let path_or_parents_in_set = find_dirs(directory)
-            .any(|parent_dir| self.files.contains(parent_dir));
-        if self.dirs.contains(directory) || path_or_parents_in_set {
+        if self.dirs.contains(directory) {
+            return VisitChildrenSet::This;
+        }
+        if dir_ancestors(directory).any(|parent_dir| {
+            self.files.contains(parent_dir)
+                || self.dirs_explicit.contains(parent_dir)
+        }) {
             VisitChildrenSet::This
         } else {
             VisitChildrenSet::Empty
@@ -390,7 +403,7 @@
 /// assert_eq!(matcher.matches(HgPath::new(b"but not this")), false);
 /// ///
 /// let ignore_patterns =
-/// vec![IgnorePattern::new(PatternSyntax::RootFiles, b"dir/subdir", Path::new(""))];
+/// vec![IgnorePattern::new(PatternSyntax::RootFilesIn, b"dir/subdir", Path::new(""))];
 /// let matcher = IncludeMatcher::new(ignore_patterns).unwrap();
 /// ///
 /// assert!(!matcher.matches(HgPath::new(b"file")));
@@ -405,7 +418,7 @@
     prefix: bool,
     roots: HashSet<HgPathBuf>,
     dirs: HashSet<HgPathBuf>,
-    parents: HashSet<HgPathBuf>,
+    parents: DirsMultiset,
 }
 
 impl core::fmt::Debug for IncludeMatcher<'_> {
@@ -861,7 +874,7 @@
                 });
                 roots.push(pat.to_owned());
             }
-            PatternSyntax::RootFiles => {
+            PatternSyntax::RootFilesIn => {
                 let pat = if pattern == b"." {
                     &[] as &[u8]
                 } else {
@@ -885,7 +898,7 @@
     /// Directories to match non-recursively
     pub dirs: HashSet<HgPathBuf>,
     /// Implicitly required directories to go to items in either roots or dirs
-    pub parents: HashSet<HgPathBuf>,
+    pub parents: DirsMultiset,
 }
 
 /// Extract roots, dirs and parents from patterns.
@@ -894,18 +907,11 @@
 ) -> PatternResult<RootsDirsAndParents> {
     let (roots, dirs) = roots_and_dirs(ignore_patterns);
 
-    let mut parents = HashSet::new();
+    let mut parents = DirsMultiset::from_manifest(&dirs)?;
 
-    parents.extend(
-        DirsMultiset::from_manifest(&dirs)?
-            .iter()
-            .map(ToOwned::to_owned),
-    );
-    parents.extend(
-        DirsMultiset::from_manifest(&roots)?
-            .iter()
-            .map(ToOwned::to_owned),
-    );
+    for path in &roots {
+        parents.add_path(path)?
+    }
 
     Ok(RootsDirsAndParents {
         roots: HashSet::from_iter(roots),
@@ -958,7 +964,7 @@
         // with a regex.
         if ignore_patterns
             .iter()
-            .all(|k| k.syntax == PatternSyntax::RootFiles)
+            .all(|k| k.syntax == PatternSyntax::RootFilesIn)
         {
             let dirs: HashSet<_> = ignore_patterns
                 .iter()
@@ -1077,7 +1083,7 @@
             .iter()
             .chain(self.roots.iter())
             .chain(self.parents.iter());
-        DirsChildrenMultiset::new(thing, Some(&self.parents))
+        DirsChildrenMultiset::new(thing, Some(self.parents.iter()))
     }
 
     pub fn debug_get_patterns(&self) -> &[u8] {
@@ -1105,6 +1111,9 @@
 mod tests {
     use super::*;
     use pretty_assertions::assert_eq;
+    use std::collections::BTreeMap;
+    use std::collections::BTreeSet;
+    use std::fmt::Debug;
     use std::path::Path;
 
     #[test]
@@ -1141,9 +1150,12 @@
 
         let dirs = HashSet::new();
 
-        let mut parents = HashSet::new();
-        parents.insert(HgPathBuf::new());
-        parents.insert(HgPathBuf::from_bytes(b"g"));
+        let parents = DirsMultiset::from_manifest(&[
+            HgPathBuf::from_bytes(b"x"),
+            HgPathBuf::from_bytes(b"g/x"),
+            HgPathBuf::from_bytes(b"g/y"),
+        ])
+        .unwrap();
 
         assert_eq!(
             roots_dirs_and_parents(&pats).unwrap(),
@@ -1316,61 +1328,60 @@
 
         // VisitdirRootfilesin
         let m = PatternMatcher::new(vec![IgnorePattern::new(
-            PatternSyntax::RootFiles,
+            PatternSyntax::RootFilesIn,
             b"dir/subdir",
             Path::new(""),
         )])
         .unwrap();
         assert_eq!(
             m.visit_children_set(HgPath::new(b"dir/subdir/x")),
-            VisitChildrenSet::Empty
+            VisitChildrenSet::This
         );
         assert_eq!(
             m.visit_children_set(HgPath::new(b"folder")),
             VisitChildrenSet::Empty
         );
-        // FIXME: These should probably be This.
         assert_eq!(
             m.visit_children_set(HgPath::new(b"")),
-            VisitChildrenSet::Empty
+            VisitChildrenSet::This
         );
         assert_eq!(
             m.visit_children_set(HgPath::new(b"dir")),
-            VisitChildrenSet::Empty
+            VisitChildrenSet::This
         );
         assert_eq!(
             m.visit_children_set(HgPath::new(b"dir/subdir")),
-            VisitChildrenSet::Empty
+            VisitChildrenSet::This
         );
 
         // VisitchildrensetRootfilesin
         let m = PatternMatcher::new(vec![IgnorePattern::new(
-            PatternSyntax::RootFiles,
+            PatternSyntax::RootFilesIn,
             b"dir/subdir",
             Path::new(""),
         )])
         .unwrap();
         assert_eq!(
             m.visit_children_set(HgPath::new(b"dir/subdir/x")),
-            VisitChildrenSet::Empty
+            VisitChildrenSet::This
         );
         assert_eq!(
             m.visit_children_set(HgPath::new(b"folder")),
             VisitChildrenSet::Empty
         );
         // FIXME: These should probably be {'dir'}, {'subdir'} and This,
-        // respectively, or at least This for all three.
+        // respectively
         assert_eq!(
             m.visit_children_set(HgPath::new(b"")),
-            VisitChildrenSet::Empty
+            VisitChildrenSet::This
         );
         assert_eq!(
             m.visit_children_set(HgPath::new(b"dir")),
-            VisitChildrenSet::Empty
+            VisitChildrenSet::This
         );
         assert_eq!(
             m.visit_children_set(HgPath::new(b"dir/subdir")),
-            VisitChildrenSet::Empty
+            VisitChildrenSet::This
         );
 
         // VisitdirGlob
@@ -1384,10 +1395,9 @@
             m.visit_children_set(HgPath::new(b"")),
             VisitChildrenSet::This
         );
-        // FIXME: This probably should be This
         assert_eq!(
             m.visit_children_set(HgPath::new(b"dir")),
-            VisitChildrenSet::Empty
+            VisitChildrenSet::This
         );
         assert_eq!(
             m.visit_children_set(HgPath::new(b"folder")),
@@ -1418,10 +1428,9 @@
             m.visit_children_set(HgPath::new(b"folder")),
             VisitChildrenSet::Empty
         );
-        // FIXME: This probably should be This
         assert_eq!(
             m.visit_children_set(HgPath::new(b"dir")),
-            VisitChildrenSet::Empty
+            VisitChildrenSet::This
         );
         // OPT: these should probably be Empty
         assert_eq!(
@@ -1529,7 +1538,7 @@
 
         // VisitchildrensetRootfilesin
         let matcher = IncludeMatcher::new(vec![IgnorePattern::new(
-            PatternSyntax::RootFiles,
+            PatternSyntax::RootFilesIn,
             b"dir/subdir",
             Path::new(""),
         )])
@@ -1664,7 +1673,7 @@
         )])
         .unwrap();
         let m2 = IncludeMatcher::new(vec![IgnorePattern::new(
-            PatternSyntax::RootFiles,
+            PatternSyntax::RootFilesIn,
             b"dir",
             Path::new(""),
         )])
@@ -1825,7 +1834,7 @@
         );
         let m2 = Box::new(
             IncludeMatcher::new(vec![IgnorePattern::new(
-                PatternSyntax::RootFiles,
+                PatternSyntax::RootFilesIn,
                 b"dir",
                 Path::new(""),
             )])
@@ -2076,7 +2085,7 @@
         );
         let m2 = Box::new(
             IncludeMatcher::new(vec![IgnorePattern::new(
-                PatternSyntax::RootFiles,
+                PatternSyntax::RootFilesIn,
                 b"dir",
                 Path::new("/repo"),
             )])
@@ -2119,4 +2128,323 @@
             VisitChildrenSet::This
         );
     }
+
+    mod invariants {
+        pub mod visit_children_set {
+
+            use crate::{
+                matchers::{tests::Tree, Matcher, VisitChildrenSet},
+                utils::hg_path::HgPath,
+            };
+
+            #[allow(dead_code)]
+            #[derive(Debug)]
+            struct Error<'a, M> {
+                matcher: &'a M,
+                path: &'a HgPath,
+                matching: &'a Tree,
+                visit_children_set: &'a VisitChildrenSet,
+            }
+
+            fn holds(
+                matching: &Tree,
+                not_matching: &Tree,
+                vcs: &VisitChildrenSet,
+            ) -> bool {
+                match vcs {
+                    VisitChildrenSet::Empty => matching.is_empty(),
+                    VisitChildrenSet::This => {
+                        // `This` does not come with any obligations.
+                        true
+                    }
+                    VisitChildrenSet::Recursive => {
+                        // `Recursive` requires that *everything* in the
+                        // subtree matches. This
+                        // requirement is relied on for example in
+                        // DifferenceMatcher implementation.
+                        not_matching.is_empty()
+                    }
+                    VisitChildrenSet::Set(allowed_children) => {
+                        // `allowed_children` does not distinguish between
+                        // files and directories: if it's not included, it
+                        // must not be matched.
+                        for k in matching.dirs.keys() {
+                            if !(allowed_children.contains(k)) {
+                                return false;
+                            }
+                        }
+                        for k in matching.files.iter() {
+                            if !(allowed_children.contains(k)) {
+                                return false;
+                            }
+                        }
+                        true
+                    }
+                }
+            }
+
+            pub fn check<M: Matcher + std::fmt::Debug>(
+                matcher: &M,
+                path: &HgPath,
+                matching: &Tree,
+                not_matching: &Tree,
+                visit_children_set: &VisitChildrenSet,
+            ) {
+                if !holds(matching, not_matching, visit_children_set) {
+                    panic!(
+                        "{:#?}",
+                        Error {
+                            matcher,
+                            path,
+                            visit_children_set,
+                            matching
+                        }
+                    )
+                }
+            }
+        }
+    }
+
+    #[derive(Debug, Clone)]
+    pub struct Tree {
+        files: BTreeSet<HgPathBuf>,
+        dirs: BTreeMap<HgPathBuf, Tree>,
+    }
+
+    impl Tree {
+        fn len(&self) -> usize {
+            let mut n = 0;
+            n += self.files.len();
+            for d in self.dirs.values() {
+                n += d.len();
+            }
+            n
+        }
+
+        fn is_empty(&self) -> bool {
+            self.files.is_empty() && self.dirs.is_empty()
+        }
+
+        fn make(
+            files: BTreeSet<HgPathBuf>,
+            dirs: BTreeMap<HgPathBuf, Tree>,
+        ) -> Self {
+            Self {
+                files,
+                dirs: dirs
+                    .into_iter()
+                    .filter(|(_k, v)| (!(v.is_empty())))
+                    .collect(),
+            }
+        }
+
+        fn filter_and_check<M: Matcher + Debug>(
+            &self,
+            m: &M,
+            path: &HgPath,
+        ) -> (Self, Self) {
+            let (files1, files2): (BTreeSet<HgPathBuf>, BTreeSet<HgPathBuf>) =
+                self.files
+                    .iter()
+                    .map(|v| v.to_owned())
+                    .partition(|v| m.matches(&path.join(v)));
+            let (dirs1, dirs2): (
+                BTreeMap<HgPathBuf, Tree>,
+                BTreeMap<HgPathBuf, Tree>,
+            ) = self
+                .dirs
+                .iter()
+                .map(|(k, v)| {
+                    let path = path.join(k);
+                    let (t1, t2) = v.filter_and_check(m, &path);
+                    ((k.clone(), t1), (k.clone(), t2))
+                })
+                .unzip();
+            let matching = Self::make(files1, dirs1);
+            let not_matching = Self::make(files2, dirs2);
+            let vcs = m.visit_children_set(path);
+            invariants::visit_children_set::check(
+                m,
+                path,
+                &matching,
+                &not_matching,
+                &vcs,
+            );
+            (matching, not_matching)
+        }
+
+        fn check_matcher<M: Matcher + Debug>(
+            &self,
+            m: &M,
+            expect_count: usize,
+        ) {
+            let res = self.filter_and_check(m, &HgPathBuf::new());
+            if expect_count != res.0.len() {
+                eprintln!(
+                    "warning: expected {} matches, got {} for {:#?}",
+                    expect_count,
+                    res.0.len(),
+                    m
+                );
+            }
+        }
+    }
+
+    fn mkdir(children: &[(&[u8], &Tree)]) -> Tree {
+        let p = HgPathBuf::from_bytes;
+        let names = [
+            p(b"a"),
+            p(b"b.txt"),
+            p(b"file.txt"),
+            p(b"c.c"),
+            p(b"c.h"),
+            p(b"dir1"),
+            p(b"dir2"),
+            p(b"subdir"),
+        ];
+        let files: BTreeSet<HgPathBuf> = BTreeSet::from(names);
+        let dirs = children
+            .iter()
+            .map(|(name, t)| (p(name), (*t).clone()))
+            .collect();
+        Tree { files, dirs }
+    }
+
+    fn make_example_tree() -> Tree {
+        let leaf = mkdir(&[]);
+        let abc = mkdir(&[(b"d", &leaf)]);
+        let ab = mkdir(&[(b"c", &abc)]);
+        let a = mkdir(&[(b"b", &ab)]);
+        let dir = mkdir(&[(b"subdir", &leaf), (b"subdir.c", &leaf)]);
+        mkdir(&[(b"dir", &dir), (b"dir1", &dir), (b"dir2", &dir), (b"a", &a)])
+    }
+
+    #[test]
+    fn test_pattern_matcher_visit_children_set() {
+        let tree = make_example_tree();
+        let pattern_dir1_glob_c =
+            PatternMatcher::new(vec![IgnorePattern::new(
+                PatternSyntax::Glob,
+                b"dir1/*.c",
+                Path::new(""),
+            )])
+            .unwrap();
+        let pattern_dir1 = || {
+            PatternMatcher::new(vec![IgnorePattern::new(
+                PatternSyntax::Path,
+                b"dir1",
+                Path::new(""),
+            )])
+            .unwrap()
+        };
+        let pattern_dir1_a = PatternMatcher::new(vec![IgnorePattern::new(
+            PatternSyntax::Glob,
+            b"dir1/a",
+            Path::new(""),
+        )])
+        .unwrap();
+        let pattern_relglob_c = || {
+            PatternMatcher::new(vec![IgnorePattern::new(
+                PatternSyntax::RelGlob,
+                b"*.c",
+                Path::new(""),
+            )])
+            .unwrap()
+        };
+        let files = vec![HgPathBuf::from_bytes(b"dir/subdir/b.txt")];
+        let file_dir_subdir_b = FileMatcher::new(files).unwrap();
+
+        let files = vec![
+            HgPathBuf::from_bytes(b"file.txt"),
+            HgPathBuf::from_bytes(b"a/file.txt"),
+            HgPathBuf::from_bytes(b"a/b/file.txt"),
+            // No file in a/b/c
+            HgPathBuf::from_bytes(b"a/b/c/d/file.txt"),
+        ];
+        let file_abcdfile = FileMatcher::new(files).unwrap();
+        let rootfilesin_dir = PatternMatcher::new(vec![IgnorePattern::new(
+            PatternSyntax::RootFilesIn,
+            b"dir",
+            Path::new(""),
+        )])
+        .unwrap();
+
+        let pattern_filepath_dir_subdir =
+            PatternMatcher::new(vec![IgnorePattern::new(
+                PatternSyntax::FilePath,
+                b"dir/subdir",
+                Path::new(""),
+            )])
+            .unwrap();
+
+        let include_dir_subdir =
+            IncludeMatcher::new(vec![IgnorePattern::new(
+                PatternSyntax::RelPath,
+                b"dir/subdir",
+                Path::new(""),
+            )])
+            .unwrap();
+
+        let more_includematchers = [
+            IncludeMatcher::new(vec![IgnorePattern::new(
+                PatternSyntax::Glob,
+                b"dir/s*",
+                Path::new(""),
+            )])
+            .unwrap(),
+            // Test multiple patterns
+            IncludeMatcher::new(vec![
+                IgnorePattern::new(
+                    PatternSyntax::RelPath,
+                    b"dir",
+                    Path::new(""),
+                ),
+                IgnorePattern::new(PatternSyntax::Glob, b"s*", Path::new("")),
+            ])
+            .unwrap(),
+            // Test multiple patterns
+            IncludeMatcher::new(vec![IgnorePattern::new(
+                PatternSyntax::Glob,
+                b"**/*.c",
+                Path::new(""),
+            )])
+            .unwrap(),
+        ];
+
+        tree.check_matcher(&pattern_dir1(), 25);
+        tree.check_matcher(&pattern_dir1_a, 1);
+        tree.check_matcher(&pattern_dir1_glob_c, 2);
+        tree.check_matcher(&pattern_relglob_c(), 14);
+        tree.check_matcher(&AlwaysMatcher, 112);
+        tree.check_matcher(&NeverMatcher, 0);
+        tree.check_matcher(
+            &IntersectionMatcher::new(
+                Box::new(pattern_relglob_c()),
+                Box::new(pattern_dir1()),
+            ),
+            3,
+        );
+        tree.check_matcher(
+            &UnionMatcher::new(vec![
+                Box::new(pattern_relglob_c()),
+                Box::new(pattern_dir1()),
+            ]),
+            36,
+        );
+        tree.check_matcher(
+            &DifferenceMatcher::new(
+                Box::new(pattern_relglob_c()),
+                Box::new(pattern_dir1()),
+            ),
+            11,
+        );
+        tree.check_matcher(&file_dir_subdir_b, 1);
+        tree.check_matcher(&file_abcdfile, 4);
+        tree.check_matcher(&rootfilesin_dir, 8);
+        tree.check_matcher(&pattern_filepath_dir_subdir, 1);
+        tree.check_matcher(&include_dir_subdir, 9);
+        tree.check_matcher(&more_includematchers[0], 17);
+        tree.check_matcher(&more_includematchers[1], 25);
+        tree.check_matcher(&more_includematchers[2], 35);
+    }
 }
--- a/rust/hg-core/src/utils/files.rs	Wed Mar 13 12:02:06 2024 +0100
+++ b/rust/hg-core/src/utils/files.rs	Wed Apr 17 12:28:48 2024 +0200
@@ -120,6 +120,10 @@
     dirs
 }
 
+pub fn dir_ancestors(path: &HgPath) -> Ancestors {
+    Ancestors { next: Some(path) }
+}
+
 /// Returns an iterator yielding ancestor directories of the given repository
 /// path.
 ///
--- a/setup.py	Wed Mar 13 12:02:06 2024 +0100
+++ b/setup.py	Wed Apr 17 12:28:48 2024 +0200
@@ -232,6 +232,10 @@
             print("stderr from '%s':" % (' '.join(cmd)), file=sys.stderr)
             print(err, file=sys.stderr)
         if returncode != 0:
+            print(
+                "non zero-return '%s': %d" % (' '.join(cmd), returncode),
+                file=sys.stderr,
+            )
             return b''
         return out
 
--- a/tests/sshprotoext.py	Wed Mar 13 12:02:06 2024 +0100
+++ b/tests/sshprotoext.py	Wed Apr 17 12:28:48 2024 +0200
@@ -30,7 +30,7 @@
 
     def serve_forever(self):
         for i in range(10):
-            self._fout.write(b'banner: line %d\n' % i)
+            self._ui.fout.write(b'banner: line %d\n' % i)
 
         super(bannerserver, self).serve_forever()
 
@@ -45,17 +45,16 @@
     """
 
     def serve_forever(self):
-        l = self._fin.readline()
+        ui = self._ui
+        l = ui.fin.readline()
         assert l == b'hello\n'
         # Respond to unknown commands with an empty reply.
-        wireprotoserver._sshv1respondbytes(self._fout, b'')
-        l = self._fin.readline()
+        wireprotoserver._sshv1respondbytes(ui.fout, b'')
+        l = ui.fin.readline()
         assert l == b'between\n'
-        proto = wireprotoserver.sshv1protocolhandler(
-            self._ui, self._fin, self._fout
-        )
+        proto = wireprotoserver.sshv1protocolhandler(ui, ui.fin, ui.fout)
         rsp = wireprotov1server.dispatch(self._repo, proto, b'between')
-        wireprotoserver._sshv1respondbytes(self._fout, rsp.data)
+        wireprotoserver._sshv1respondbytes(ui.fout, rsp.data)
 
         super(prehelloserver, self).serve_forever()
 
--- a/tests/test-bundle-type.t	Wed Mar 13 12:02:06 2024 +0100
+++ b/tests/test-bundle-type.t	Wed Apr 17 12:28:48 2024 +0200
@@ -620,3 +620,93 @@
       b9f5f740a8cd76700020e3903ee55ecff78bd3e5
   $ hg debugbundle ./v2-cg-03.hg --spec
   bzip2-v2;cg.version=03
+
+tests controlling bundle contents
+=================================
+
+  $ hg debugupdatecache -R t1
+
+default content
+---------------
+
+  $ hg -R t1 bundle --all --quiet --type 'v2' ./v2.hg
+  $ hg debugbundle ./v2.hg --spec
+  bzip2-v2
+  $ hg debugbundle ./v2.hg --quiet
+  Stream params: {Compression: BZ}
+  changegroup -- {nbchanges: 7, version: 02} (mandatory: True)
+  hgtagsfnodes -- {} (mandatory: False)
+  cache:rev-branch-cache -- {} (mandatory: False)
+
+  $ hg -R t1 bundle --all --quiet --type 'v3' ./v3.hg
+  $ hg debugbundle ./v3.hg --spec
+  bzip2-v2;cg.version=03
+  $ hg debugbundle ./v3.hg --quiet
+  Stream params: {Compression: BZ}
+  changegroup -- {nbchanges: 7, targetphase: 2, version: 03} (mandatory: True)
+  hgtagsfnodes -- {} (mandatory: False)
+  cache:rev-branch-cache -- {} (mandatory: False)
+  phase-heads -- {} (mandatory: True)
+
+adding extra parts
+------------------
+
+We should have a "phase-heads" part here that we did not had in the default content
+
+  $ hg -R t1 bundle --all --quiet --type 'v2;phases=1' ./v2-phases.hg
+  $ hg debugbundle ./v2-phases.hg --spec
+  bzip2-v2
+  $ hg debugbundle ./v2-phases.hg --quiet
+  Stream params: {Compression: BZ}
+  changegroup -- {nbchanges: 7, targetphase: 2, version: 02} (mandatory: True)
+  hgtagsfnodes -- {} (mandatory: False)
+  cache:rev-branch-cache -- {} (mandatory: False)
+  phase-heads -- {} (mandatory: True)
+
+skipping default inclusion
+--------------------------
+
+  $ hg -R t1 bundle --all --quiet --type 'v2;tagsfnodescache=false' ./v2-no-tfc.hg
+  $ hg debugbundle ./v2-no-tfc.hg --spec
+  bzip2-v2
+  $ hg debugbundle ./v2-no-tfc.hg --quiet
+  Stream params: {Compression: BZ}
+  changegroup -- {nbchanges: 7, version: 02} (mandatory: True)
+  cache:rev-branch-cache -- {} (mandatory: False)
+
+  $ hg -R t1 bundle --all --quiet --type 'v3;phases=0' ./v3-no-phases.hg
+  $ hg debugbundle ./v3-no-phases.hg --spec
+  bzip2-v2;cg.version=03
+  $ hg debugbundle ./v3-no-phases.hg --quiet
+  Stream params: {Compression: BZ}
+  changegroup -- {nbchanges: 7, version: 03} (mandatory: True)
+  hgtagsfnodes -- {} (mandatory: False)
+  cache:rev-branch-cache -- {} (mandatory: False)
+
+  $ hg -R t1 bundle --all --quiet --type 'v3;phases=no;tagsfnodescache=0' ./v3-multi-no.hg
+  $ hg debugbundle ./v3-multi-no.hg --spec
+  bzip2-v2;cg.version=03
+  $ hg debugbundle ./v3-multi-no.hg --quiet
+  Stream params: {Compression: BZ}
+  changegroup -- {nbchanges: 7, version: 03} (mandatory: True)
+  cache:rev-branch-cache -- {} (mandatory: False)
+
+skipping changegroup
+--------------------
+
+  $ hg -R t1 bundle --all --quiet --type 'v2;changegroup=no' ./v2-no-cg.hg
+  $ hg debugbundle ./v2-no-cg.hg --spec
+  bzip2-v2;changegroup=no
+  $ hg debugbundle ./v2-no-cg.hg --quiet
+  Stream params: {Compression: BZ}
+  hgtagsfnodes -- {} (mandatory: False)
+  cache:rev-branch-cache -- {} (mandatory: False)
+
+  $ hg -R t1 bundle --all --quiet --type 'v3;changegroup=0' ./v3-no-cg.hg
+  $ hg debugbundle ./v3-no-cg.hg --spec
+  bzip2-v2;changegroup=no
+  $ hg debugbundle ./v3-no-cg.hg --quiet
+  Stream params: {Compression: BZ}
+  hgtagsfnodes -- {} (mandatory: False)
+  cache:rev-branch-cache -- {} (mandatory: False)
+  phase-heads -- {} (mandatory: True)
--- a/tests/test-match.py	Wed Mar 13 12:02:06 2024 +0100
+++ b/tests/test-match.py	Wed Apr 17 12:28:48 2024 +0200
@@ -94,12 +94,14 @@
             patterns=[b'rootfilesin:dir/subdir'],
         )
         assert isinstance(m, matchmod.patternmatcher)
-        self.assertFalse(m.visitdir(b'dir/subdir/x'))
+        # OPT: we shouldn't visit [x] as a directory,
+        # but we should still visit it as a file.
+        # Unfortunately, `visitdir` is used for both.
+        self.assertTrue(m.visitdir(b'dir/subdir/x'))
         self.assertFalse(m.visitdir(b'folder'))
-        # FIXME: These should probably be True.
-        self.assertFalse(m.visitdir(b''))
-        self.assertFalse(m.visitdir(b'dir'))
-        self.assertFalse(m.visitdir(b'dir/subdir'))
+        self.assertTrue(m.visitdir(b''))
+        self.assertTrue(m.visitdir(b'dir'))
+        self.assertTrue(m.visitdir(b'dir/subdir'))
 
     def testVisitchildrensetRootfilesin(self):
         m = matchmod.match(
@@ -108,13 +110,13 @@
             patterns=[b'rootfilesin:dir/subdir'],
         )
         assert isinstance(m, matchmod.patternmatcher)
-        self.assertEqual(m.visitchildrenset(b'dir/subdir/x'), set())
+        self.assertEqual(m.visitchildrenset(b'dir/subdir/x'), b'this')
         self.assertEqual(m.visitchildrenset(b'folder'), set())
-        # FIXME: These should probably be {'dir'}, {'subdir'} and 'this',
-        # respectively, or at least 'this' for all three.
-        self.assertEqual(m.visitchildrenset(b''), set())
-        self.assertEqual(m.visitchildrenset(b'dir'), set())
-        self.assertEqual(m.visitchildrenset(b'dir/subdir'), set())
+        # OPT: These should probably be {'dir'}, {'subdir'} and 'this',
+        # respectively
+        self.assertEqual(m.visitchildrenset(b''), b'this')
+        self.assertEqual(m.visitchildrenset(b'dir'), b'this')
+        self.assertEqual(m.visitchildrenset(b'dir/subdir'), b'this')
 
     def testVisitdirGlob(self):
         m = matchmod.match(
--- a/tests/test-sshserver.py	Wed Mar 13 12:02:06 2024 +0100
+++ b/tests/test-sshserver.py	Wed Apr 17 12:28:48 2024 +0200
@@ -25,9 +25,8 @@
 
     def assertparse(self, cmd, input, expected):
         server = mockserver(input)
-        proto = wireprotoserver.sshv1protocolhandler(
-            server._ui, server._fin, server._fout
-        )
+        ui = server._ui
+        proto = wireprotoserver.sshv1protocolhandler(ui, ui.fin, ui.fout)
         _func, spec = wireprotov1server.commands[cmd]
         self.assertEqual(proto.getargs(spec), expected)
 
@@ -35,6 +34,9 @@
 def mockserver(inbytes):
     ui = mockui(inbytes)
     repo = mockrepo(ui)
+    # note: this test unfortunately doesn't really test anything about
+    # `sshserver` class anymore: the entirety of logic of that class lives
+    # in `serveuntil`, and that function is not even called by this test.
     return wireprotoserver.sshserver(ui, repo)
 
 
--- a/tests/test-status.t	Wed Mar 13 12:02:06 2024 +0100
+++ b/tests/test-status.t	Wed Apr 17 12:28:48 2024 +0200
@@ -842,6 +842,26 @@
   C clean
   C subdir/clean
 
+Test various matchers interatction with dirstate code:
+
+  $ hg status path:subdir
+  M subdir/modified
+  R subdir/removed
+  ! subdir/deleted
+  ? subdir/unknown
+
+  $ hg status 'glob:subdir/*'
+  M subdir/modified
+  R subdir/removed
+  ! subdir/deleted
+  ? subdir/unknown
+
+  $ hg status rootfilesin:subdir
+  M subdir/modified
+  R subdir/removed
+  ! subdir/deleted
+  ? subdir/unknown
+
 Note: `hg status some-name` creates a patternmatcher which is not supported
 yet by the Rust implementation of status, but includematcher is supported.
 --include is used below for that reason