rhg: Add support for ui.ignore and ui.ignore.* config
authorSimon Sapin <simon.sapin@octobus.net>
Fri, 10 Dec 2021 14:27:00 +0100
changeset 48451 4a983b69e519
parent 48450 b5f8d9e55d42
child 48452 2afaa0145584
rhg: Add support for ui.ignore and ui.ignore.* config This fixes some but not all failures in `tests/test-hgignore.t` when running with `rhg status` enabled. Differential Revision: https://phab.mercurial-scm.org/D11907
rust/hg-core/src/config/config.rs
rust/hg-core/src/config/layer.rs
rust/rhg/src/commands/status.rs
--- a/rust/hg-core/src/config/config.rs	Fri Dec 10 17:20:21 2021 +0100
+++ b/rust/hg-core/src/config/config.rs	Fri Dec 10 14:27:00 2021 +0100
@@ -419,6 +419,59 @@
             .any(|layer| layer.has_non_empty_section(section))
     }
 
+    /// Yields (key, value) pairs for everything in the given section
+    pub fn iter_section<'a>(
+        &'a self,
+        section: &'a [u8],
+    ) -> impl Iterator<Item = (&[u8], &[u8])> + 'a {
+        // TODO: Use `Iterator`’s `.peekable()` when its `peek_mut` is
+        // available:
+        // https://doc.rust-lang.org/nightly/std/iter/struct.Peekable.html#method.peek_mut
+        struct Peekable<I: Iterator> {
+            iter: I,
+            /// Remember a peeked value, even if it was None.
+            peeked: Option<Option<I::Item>>,
+        }
+
+        impl<I: Iterator> Peekable<I> {
+            fn new(iter: I) -> Self {
+                Self { iter, peeked: None }
+            }
+
+            fn next(&mut self) {
+                self.peeked = None
+            }
+
+            fn peek_mut(&mut self) -> Option<&mut I::Item> {
+                let iter = &mut self.iter;
+                self.peeked.get_or_insert_with(|| iter.next()).as_mut()
+            }
+        }
+
+        // Deduplicate keys redefined in multiple layers
+        let mut keys_already_seen = HashSet::new();
+        let mut key_is_new =
+            move |&(key, _value): &(&'a [u8], &'a [u8])| -> bool {
+                keys_already_seen.insert(key)
+            };
+        // This is similar to `flat_map` + `filter_map`, except with a single
+        // closure that owns `key_is_new` (and therefore the
+        // `keys_already_seen` set):
+        let mut layer_iters = Peekable::new(
+            self.layers
+                .iter()
+                .rev()
+                .map(move |layer| layer.iter_section(section)),
+        );
+        std::iter::from_fn(move || loop {
+            if let Some(pair) = layer_iters.peek_mut()?.find(&mut key_is_new) {
+                return Some(pair);
+            } else {
+                layer_iters.next();
+            }
+        })
+    }
+
     /// Get raw values bytes from all layers (even untrusted ones) in order
     /// of precedence.
     #[cfg(test)]
--- a/rust/hg-core/src/config/layer.rs	Fri Dec 10 17:20:21 2021 +0100
+++ b/rust/hg-core/src/config/layer.rs	Fri Dec 10 14:27:00 2021 +0100
@@ -127,6 +127,17 @@
             .flat_map(|section| section.keys().map(|vec| &**vec))
     }
 
+    /// Returns the (key, value) pairs defined in the given section
+    pub fn iter_section<'layer>(
+        &'layer self,
+        section: &[u8],
+    ) -> impl Iterator<Item = (&'layer [u8], &'layer [u8])> {
+        self.sections
+            .get(section)
+            .into_iter()
+            .flat_map(|section| section.iter().map(|(k, v)| (&**k, &*v.bytes)))
+    }
+
     /// Returns whether any key is defined in the given section
     pub fn has_non_empty_section(&self, section: &[u8]) -> bool {
         self.sections
--- a/rust/rhg/src/commands/status.rs	Fri Dec 10 17:20:21 2021 +0100
+++ b/rust/rhg/src/commands/status.rs	Fri Dec 10 14:27:00 2021 +0100
@@ -21,10 +21,12 @@
 use hg::matchers::AlwaysMatcher;
 use hg::repo::Repo;
 use hg::utils::files::get_bytes_from_os_string;
+use hg::utils::files::get_path_from_bytes;
 use hg::utils::hg_path::{hg_path_to_path_buf, HgPath};
 use hg::{HgPathCow, StatusOptions};
 use log::{info, warn};
 use std::io;
+use std::path::PathBuf;
 
 pub const HELP_TEXT: &str = "
 Show changed files in the working directory
@@ -213,11 +215,10 @@
         list_ignored: display_states.ignored,
         collect_traversed_dirs: false,
     };
-    let ignore_file = repo.working_directory_vfs().join(".hgignore"); // TODO hardcoded
     let (mut ds_status, pattern_warnings) = dmap.status(
         &AlwaysMatcher,
         repo.working_directory_path().to_owned(),
-        vec![ignore_file],
+        ignore_files(repo, config),
         options,
     )?;
     if !pattern_warnings.is_empty() {
@@ -396,6 +397,25 @@
     Ok(())
 }
 
+fn ignore_files(repo: &Repo, config: &Config) -> Vec<PathBuf> {
+    let mut ignore_files = Vec::new();
+    let repo_ignore = repo.working_directory_vfs().join(".hgignore");
+    if repo_ignore.exists() {
+        ignore_files.push(repo_ignore)
+    }
+    for (key, value) in config.iter_section(b"ui") {
+        if key == b"ignore" || key.starts_with(b"ignore.") {
+            let path = get_path_from_bytes(value);
+            // TODO: expand "~/" and environment variable here, like Python
+            // does with `os.path.expanduser` and `os.path.expandvars`
+
+            let joined = repo.working_directory_path().join(path);
+            ignore_files.push(joined);
+        }
+    }
+    ignore_files
+}
+
 // Probably more elegant to use a Deref or Borrow trait rather than
 // harcode HgPathBuf, but probably not really useful at this point
 fn display_status_paths(