Просмотр исходного кода

feat(mercurial): Implemented mercurial cache

PLEASE WAIT FOR LATER COMMITS THIS ONLY WORK ON MY MACHINE

feat(mercurial): displaying the info in columns and feature flagging

feat(mercurial): feat finished

this is not prod ready

feat(ignoring): adding the ability to ignore

docs: add documentation for mercurial options

chore: styling
MartinFillon 2 лет назад
Родитель
Сommit
9ae60d1a2b

+ 9 - 0
Cargo.lock

@@ -394,6 +394,7 @@ dependencies = [
  "criterion",
  "git2",
  "glob",
+ "hgrs",
  "libc",
  "locale",
  "log",
@@ -489,6 +490,14 @@ version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b"
 
+[[package]]
+name = "hgrs"
+version = "0.2.5"
+source = "git+https://github.com/MartinFillon/hgrs.git#b28fef5f9801c4705cd524673fa8bc155d66c1a9"
+dependencies = [
+ "log",
+]
+
 [[package]]
 name = "humantime"
 version = "2.1.0"

+ 6 - 0
Cargo.toml

@@ -92,6 +92,11 @@ timeago = { version = "0.4.2", default-features = false }
 unicode-width = "0.1"
 zoneinfo_compiled = "0.5.1"
 
+[dependencies.hgrs]
+git = "https://github.com/MartinFillon/hgrs.git"
+version = "0.2.5"
+optional = true
+
 [dependencies.git2]
 version = "0.18"
 optional = true
@@ -129,6 +134,7 @@ nix-local = []
 # Shouldn't ever be used in CI (slow!)
 powertest = []
 nix-generated = []
+mercurial = ["hgrs"]
 
 # use LTO for smaller binaries (that take longer to build)
 [profile.release]

+ 3 - 0
README.md

@@ -143,6 +143,9 @@ These options are available when running with `--long` (`-l`):
 - **--no-user**: suppress the user field
 - **--no-time**: suppress the time field
 - **--stdin**: read file names from stdin
+- **--mercurial**: list each file’s Mercurial status, if tracked or ignored
+- **--mercurial-ignore**: ignore files ignored by Mercurial
+- **--no-mercurial**: suppress Mercurial status (always overrides `--mercurial`, `--mercurial-ignore`)
 
 Some of the options accept parameters:
 

+ 3 - 0
completions/fish/eza.fish

@@ -121,3 +121,6 @@ complete -c eza -l git-repos -d "List each git-repos status and branch name"
 complete -c eza -l git-repos-no-status -d "List each git-repos branch name (much faster)"
 complete -c eza -s '@' -l extended -d "List each file's extended attributes and sizes"
 complete -c eza -s Z -l context -d "List each file's security context"
+complete -c eza -l mercurial -d "List each file's Mercurial status, if tracked"
+complete -c eza -l mercurial-ignore -d "Ignore files that are ignored by Mercurial"
+complete -c eza -l no-mercurial -d "Suppress Mercurial status"

+ 3 - 0
completions/nush/eza.nu

@@ -59,4 +59,7 @@ export extern "eza" [
     --context(-Z)              # List each file's security context
     --smart-group              # Only show group if it has a different name from owner
     --stdin                    # When piping to eza. Read file paths from stdin
+    --mercurial                # List each file's Mercurial status, if tracked
+    --mercurial-ignore         # Ignore files ignored ly Mercurial
+    --no-mercurial             # Suppress Mercurial status
 ]

+ 3 - 0
completions/zsh/_eza

@@ -69,6 +69,9 @@ __eza() {
         '*:filename:_files' \
         --smart-group"[Only show group if it has a different name from owner]" \
         --stdin"[When piping to eza. Read file names from stdin]"
+        --mercurial"[List each file's Mercurial status, if tracked]" \
+        --mercurial-ignore"[Ignore files ignored by Mercurial]" \
+        --no-mercurial"[Suppress Mercurial status]" \
 }
 
 __eza

+ 11 - 0
man/eza.1.md

@@ -269,6 +269,17 @@ All Git repository directories will be shown as (themed) `-` without status indi
 `--no-git`
 : Don't show Git status (always overrides `--git`, `--git-repos`, `--git-repos-no-status`)
 
+`--mercurial`  [if eza was built with mercurial support]
+: List each file’s Mercurial status, if tracked.
+hg command Needed !!! This adds a character column indicating status. The status character can be ‘`-`’ for not directories, ‘`M`’ for a modified file, ‘`A`’ for a added file, ‘`R`’ for deleted, ‘`C`’ for cleaned, ‘`!`’ for missing, ‘`I`’ for ignored.
+
+`--mercurial-ignore` [if eza was built with mercurial support]
+: Do not list files that are ignored by Mercurial.
+hg command Needed !!!
+
+`--no-mercurial` [if eza was built with mercurial support]
+: Don't show Mercurial status (always overrides `--mercurial`, `--mercurial-ignore`)
+hg command Needed !!!
 
 ENVIRONMENT VARIABLES
 =====================

+ 17 - 1
src/fs/dir.rs

@@ -1,10 +1,11 @@
 use crate::fs::feature::git::GitCache;
-use crate::fs::fields::GitStatus;
+use crate::fs::fields::{GitStatus, MercurialStatus};
 use std::fs;
 use std::io;
 use std::path::{Path, PathBuf};
 use std::slice::Iter as SliceIter;
 
+use crate::fs::feature::mercurial::MercurialCache;
 use log::*;
 
 use crate::fs::File;
@@ -52,6 +53,8 @@ impl Dir {
         git_ignoring: bool,
         deref_links: bool,
         total_size: bool,
+        mercurial: Option<&'ig MercurialCache>,
+        mercurial_ignore: bool,
     ) -> Files<'dir, 'ig> {
         Files {
             inner: self.contents.iter(),
@@ -62,6 +65,8 @@ impl Dir {
             git_ignoring,
             deref_links,
             total_size,
+            mercurial,
+            mercurial_ignore,
         }
     }
 
@@ -101,6 +106,10 @@ pub struct Files<'dir, 'ig> {
 
     /// Whether to calculate the directory size recursively
     total_size: bool,
+
+    mercurial: Option<&'ig MercurialCache>,
+
+    mercurial_ignore: bool,
 }
 
 impl<'dir, 'ig> Files<'dir, 'ig> {
@@ -137,6 +146,13 @@ impl<'dir, 'ig> Files<'dir, 'ig> {
                     }
                 }
 
+                if self.mercurial_ignore {
+                    let mercurial_status = self.mercurial.map(|g| g.get(path)).unwrap_or_default();
+                    if mercurial_status.status == MercurialStatus::Ignored {
+                        continue;
+                    }
+                }
+
                 let file = File::from_args(
                     path.clone(),
                     self.dir,

+ 80 - 0
src/fs/feature/mercurial.rs

@@ -0,0 +1,80 @@
+use hgrs::{FileStatus, MercurialRepository};
+use std::path::PathBuf;
+
+use crate::fs::fields as f;
+
+#[derive(Debug, Clone)]
+pub struct MercurialCache {
+    pub repos: Vec<MercurialRepo>,
+
+    pub misses: Vec<PathBuf>,
+}
+
+impl MercurialCache {
+    pub fn get(&self, file_path: &PathBuf) -> f::Mercurial {
+        f::Mercurial {
+            status: self
+                .repos
+                .iter()
+                .find(|r| r.has_path(file_path))
+                .map(|r| r.get(file_path))
+                .unwrap_or_default()
+                .into(),
+        }
+    }
+}
+
+impl FromIterator<PathBuf> for MercurialCache {
+    fn from_iter<T: IntoIterator<Item = PathBuf>>(iter: T) -> Self {
+        let iter = iter.into_iter();
+        let mut mercurial = Self {
+            repos: Vec::with_capacity(iter.size_hint().0),
+            misses: Vec::new(),
+        };
+
+        for path in iter {
+            match MercurialRepo::discover(&path) {
+                Ok(repo) => mercurial.repos.push(repo),
+                Err(path) => mercurial.misses.push(path),
+            }
+        }
+        mercurial
+    }
+}
+
+#[derive(Debug, Clone)]
+pub struct MercurialRepo {
+    pub repo: MercurialRepository,
+    pub path: PathBuf,
+    pub extra_paths: Vec<PathBuf>,
+}
+
+impl MercurialRepo {
+    pub fn get(&self, file_path: &PathBuf) -> FileStatus {
+        self.repo.get_status(file_path)
+    }
+
+    pub fn has_path(&self, file_path: &PathBuf) -> bool {
+        let dir = file_path.parent().unwrap();
+
+        if dir == self.path {
+            return true;
+        }
+        if self.extra_paths.contains(&dir.into()) {
+            return true;
+        }
+        false
+    }
+
+    pub fn discover(path: &PathBuf) -> Result<Self, PathBuf> {
+        let r = match hgrs::find_repo_recursively(path, 10) {
+            Some(r) => r,
+            None => return Err(path.clone()),
+        };
+        Ok(Self {
+            repo: r,
+            path: path.into(),
+            extra_paths: Vec::new(),
+        })
+    }
+}

+ 33 - 0
src/fs/feature/mod.rs

@@ -3,6 +3,39 @@ pub mod xattr;
 #[cfg(feature = "git")]
 pub mod git;
 
+#[cfg(feature = "mercurial")]
+pub mod mercurial;
+
+#[cfg(not(feature = "mercurial"))]
+pub mod mercurial {
+
+    use std::iter::FromIterator;
+    use std::path::{Path, PathBuf};
+
+    use crate::fs::fields as f;
+
+    pub struct MercurialCache;
+
+    impl FromIterator<PathBuf> for MercurialCache {
+        fn from_iter<I>(_iter: I) -> Self
+        where
+            I: IntoIterator<Item = PathBuf>,
+        {
+            Self
+        }
+    }
+
+    impl MercurialCache {
+        pub fn has_anything_for(&self, _index: &Path) -> bool {
+            false
+        }
+
+        pub fn get(&self, _index: &Path) -> f::Mercurial {
+            unreachable!();
+        }
+    }
+}
+
 #[cfg(not(feature = "git"))]
 pub mod git {
     use std::iter::FromIterator;

+ 40 - 0
src/fs/fields.rs

@@ -246,6 +246,46 @@ impl Default for Git {
     }
 }
 
+#[derive(PartialEq, Eq, Copy, Clone)]
+pub enum MercurialStatus {
+    Modified,
+    Added,
+    Removed,
+    Clean,
+    Missing,
+    NotTracked,
+    Ignored,
+    Directory,
+}
+
+#[cfg(feature = "mercurial")]
+impl From<hgrs::FileStatus> for MercurialStatus {
+    fn from(value: hgrs::FileStatus) -> Self {
+        match value {
+            hgrs::FileStatus::Modified => MercurialStatus::Modified,
+            hgrs::FileStatus::Added => MercurialStatus::Added,
+            hgrs::FileStatus::Removed => MercurialStatus::Removed,
+            hgrs::FileStatus::Clean => MercurialStatus::Clean,
+            hgrs::FileStatus::Missing => MercurialStatus::Missing,
+            hgrs::FileStatus::NotTracked => MercurialStatus::NotTracked,
+            hgrs::FileStatus::Ignored => MercurialStatus::Ignored,
+            hgrs::FileStatus::Directory => MercurialStatus::Directory,
+        }
+    }
+}
+
+pub struct Mercurial {
+    pub status: MercurialStatus,
+}
+
+impl Default for Mercurial {
+    fn default() -> Self {
+        Self {
+            status: MercurialStatus::NotTracked,
+        }
+    }
+}
+
 pub enum SecurityContextType<'a> {
     SELinux(&'a str),
     None,

+ 18 - 2
src/fs/file.rs

@@ -593,7 +593,15 @@ impl<'dir> File<'dir> {
                 let mut size = 0;
                 let mut blocks = 0;
                 for file in dir
-                    .files(super::DotFilter::Dotfiles, None, false, false, true)
+                    .files(
+                        super::DotFilter::Dotfiles,
+                        None,
+                        false,
+                        false,
+                        true,
+                        None,
+                        false,
+                    )
                     .flatten()
                 {
                     match file.recursive_directory_size() {
@@ -695,7 +703,15 @@ impl<'dir> File<'dir> {
         match Dir::read_dir(self.path.clone()) {
             // . & .. are skipped, if the returned iterator has .next(), it's not empty
             Ok(has_files) => has_files
-                .files(super::DotFilter::Dotfiles, None, false, false, false)
+                .files(
+                    super::DotFilter::Dotfiles,
+                    None,
+                    false,
+                    false,
+                    false,
+                    None,
+                    false,
+                )
                 .next()
                 .is_none(),
             Err(_) => false,

+ 12 - 0
src/fs/filter.rs

@@ -68,6 +68,9 @@ pub struct FileFilter {
 
     /// Whether to ignore Git-ignored patterns.
     pub git_ignore: GitIgnore,
+
+    /// Whether to ignore Mercurial-ignored patterns.
+    pub mercurial_ignore: MercurialIgnore,
 }
 
 impl FileFilter {
@@ -354,6 +357,15 @@ pub enum GitIgnore {
     Off,
 }
 
+#[derive(PartialEq, Eq, Debug, Copy, Clone)]
+pub enum MercurialIgnore {
+    /// Ignore files that Mercurial would ignore.
+    CheckAndIgnore,
+
+    /// Display files, even if Mercurial would ignore them.
+    Off,
+}
+
 #[cfg(test)]
 mod test_ignores {
     use super::*;

+ 1 - 0
src/lib.rs

@@ -1,3 +1,4 @@
+#![allow(clippy::too_many_arguments)]
 #[allow(unused)]
 pub mod fs;
 #[allow(unused)]

+ 35 - 1
src/main.rs

@@ -20,6 +20,8 @@
 #![allow(clippy::unused_self)]
 #![allow(clippy::upper_case_acronyms)]
 #![allow(clippy::wildcard_imports)]
+#![allow(clippy::too_many_arguments)]
+#![allow(clippy::fn_params_excessive_bools)]
 
 use std::env;
 use std::ffi::{OsStr, OsString};
@@ -30,7 +32,8 @@ use std::process::exit;
 use ansiterm::{ANSIStrings, Style};
 
 use crate::fs::feature::git::GitCache;
-use crate::fs::filter::GitIgnore;
+use crate::fs::feature::mercurial::MercurialCache;
+use crate::fs::filter::{GitIgnore, MercurialIgnore};
 use crate::fs::{Dir, File};
 use crate::options::stdin::FilesInput;
 use crate::options::{vars, Options, OptionsResult, Vars};
@@ -87,6 +90,7 @@ fn main() {
             }
 
             let git = git_options(&options, &input_paths);
+            let mercurial = mercurial_options(&options, &input_paths);
             let writer = io::stdout();
             let git_repos = git_repos(&options, &input_paths);
 
@@ -100,6 +104,7 @@ fn main() {
                 console_width,
                 git,
                 git_repos,
+                mercurial,
             };
 
             info!("matching on exa.run");
@@ -169,6 +174,8 @@ pub struct Exa<'args> {
     pub git: Option<GitCache>,
 
     pub git_repos: bool,
+
+    pub mercurial: Option<MercurialCache>,
 }
 
 /// The “real” environment variables type.
@@ -191,6 +198,14 @@ fn git_options(options: &Options, args: &[&OsStr]) -> Option<GitCache> {
     }
 }
 
+fn mercurial_options(options: &Options, args: &[&OsStr]) -> Option<MercurialCache> {
+    if options.should_scan_for_mercurial() {
+        Some(args.iter().map(PathBuf::from).collect())
+    } else {
+        None
+    }
+}
+
 #[cfg(not(feature = "git"))]
 fn git_repos(_options: &Options, _args: &[&OsStr]) -> bool {
     return false;
@@ -332,12 +347,16 @@ impl<'args> Exa<'args> {
 
             let mut children = Vec::new();
             let git_ignore = self.options.filter.git_ignore == GitIgnore::CheckAndIgnore;
+            let mercurial_ignore =
+                self.options.filter.mercurial_ignore == MercurialIgnore::CheckAndIgnore;
             for file in dir.files(
                 self.options.filter.dot_filter,
                 self.git.as_ref(),
                 git_ignore,
                 self.options.view.deref_links,
                 self.options.view.total_size,
+                self.mercurial.as_ref(),
+                mercurial_ignore,
             ) {
                 match file {
                     Ok(file) => children.push(file),
@@ -427,7 +446,10 @@ impl<'args> Exa<'args> {
                 let recurse = self.options.dir_action.recurse_options();
 
                 let git_ignoring = self.options.filter.git_ignore == GitIgnore::CheckAndIgnore;
+                let mercurial_ignoring =
+                    self.options.filter.mercurial_ignore == MercurialIgnore::CheckAndIgnore;
                 let git = self.git.as_ref();
+                let mercurial = self.mercurial.as_ref();
                 let git_repos = self.git_repos;
                 let r = details::Render {
                     dir,
@@ -440,6 +462,8 @@ impl<'args> Exa<'args> {
                     git_ignoring,
                     git,
                     git_repos,
+                    mercurial,
+                    mercurial_ignoring,
                 };
                 r.render(&mut self.writer)
             }
@@ -451,7 +475,10 @@ impl<'args> Exa<'args> {
 
                 let filter = &self.options.filter;
                 let git_ignoring = self.options.filter.git_ignore == GitIgnore::CheckAndIgnore;
+                let mercurial_ignoring =
+                    self.options.filter.mercurial_ignore == MercurialIgnore::CheckAndIgnore;
                 let git = self.git.as_ref();
+                let mercurial = self.mercurial.as_ref();
                 let git_repos = self.git_repos;
 
                 let r = grid_details::Render {
@@ -467,6 +494,8 @@ impl<'args> Exa<'args> {
                     git,
                     console_width,
                     git_repos,
+                    mercurial,
+                    mercurial_ignoring,
                 };
                 r.render(&mut self.writer)
             }
@@ -476,7 +505,10 @@ impl<'args> Exa<'args> {
                 let filter = &self.options.filter;
                 let recurse = self.options.dir_action.recurse_options();
                 let git_ignoring = self.options.filter.git_ignore == GitIgnore::CheckAndIgnore;
+                let mercurial_ignoring =
+                    self.options.filter.mercurial_ignore == MercurialIgnore::CheckAndIgnore;
                 let git = self.git.as_ref();
+                let mercurial = self.mercurial.as_ref();
                 let git_repos = self.git_repos;
 
                 let r = details::Render {
@@ -490,6 +522,8 @@ impl<'args> Exa<'args> {
                     git_ignoring,
                     git,
                     git_repos,
+                    mercurial,
+                    mercurial_ignoring,
                 };
                 r.render(&mut self.writer)
             }

+ 12 - 1
src/options/filter.rs

@@ -1,7 +1,7 @@
 //! Parsing the options for `FileFilter`.
 
 use crate::fs::filter::{
-    FileFilter, FileFilterFlags, GitIgnore, IgnorePatterns, SortCase, SortField,
+    FileFilter, FileFilterFlags, GitIgnore, IgnorePatterns, MercurialIgnore, SortCase, SortField,
 };
 use crate::fs::DotFilter;
 
@@ -32,6 +32,7 @@ impl FileFilter {
             dot_filter:       DotFilter::deduce(matches)?,
             ignore_patterns:  IgnorePatterns::deduce(matches)?,
             git_ignore:       GitIgnore::deduce(matches)?,
+            mercurial_ignore: MercurialIgnore::deduce(matches)?,
         });
     }
 }
@@ -193,6 +194,16 @@ impl GitIgnore {
     }
 }
 
+impl MercurialIgnore {
+    pub fn deduce(matches: &MatchedFlags<'_>) -> Result<Self, OptionsError> {
+        if matches.has(&flags::MERCURIAL_IGNORE)? {
+            Ok(Self::CheckAndIgnore)
+        } else {
+            Ok(Self::Off)
+        }
+    }
+}
+
 #[cfg(test)]
 mod test {
     use super::*;

+ 5 - 1
src/options/flags.rs

@@ -82,6 +82,9 @@ pub static OCTAL:             Arg = Arg { short: Some(b'o'), long: "octal-permis
 pub static SECURITY_CONTEXT:  Arg = Arg { short: Some(b'Z'), long: "context",              takes_value: TakesValue::Forbidden };
 pub static STDIN:             Arg = Arg { short: None,       long: "stdin",                takes_value: TakesValue::Forbidden };
 pub static FILE_FLAGS:        Arg = Arg { short: Some(b'O'), long: "flags",                takes_value: TakesValue::Forbidden };
+pub static MERCURIAL:         Arg = Arg { short: None,       long: "mercurial",            takes_value: TakesValue::Forbidden };
+pub static NO_MERCURIAL:      Arg = Arg { short: None,       long: "no-mercurial",         takes_value: TakesValue::Forbidden };
+pub static MERCURIAL_IGNORE:  Arg = Arg { short: None,       long: "mercurial-ignore",     takes_value: TakesValue::Forbidden };
 
 pub static ALL_ARGS: Args = Args(&[
     &VERSION, &HELP,
@@ -98,5 +101,6 @@ pub static ALL_ARGS: Args = Args(&[
     &NO_PERMISSIONS, &NO_FILESIZE, &NO_USER, &NO_TIME, &SMART_GROUP,
 
     &GIT, &NO_GIT, &GIT_REPOS, &GIT_REPOS_NO_STAT,
-    &EXTENDED, &OCTAL, &SECURITY_CONTEXT, &STDIN, &FILE_FLAGS
+    &EXTENDED, &OCTAL, &SECURITY_CONTEXT, &STDIN, &FILE_FLAGS,
+    &MERCURIAL, &NO_MERCURIAL, &MERCURIAL_IGNORE,
 ]);

+ 9 - 0
src/options/help.rs

@@ -90,6 +90,11 @@ static EXTENDED_HELP: &str = "  \
 static SECATTR_HELP: &str = "  \
   -Z, --context              list each file's security context";
 
+static MERCURIAL_HELP: &str = "  \
+  --mercurial                list each file's Mercurial status (hg command needed) \
+  --no-mercurial             ignore ignored files in Mercurial (hg command needed) \
+  --no-mercurial             suppress Mercurial status (always overrides --mercurial)";
+
 /// All the information needed to display the help text, which depends
 /// on which features are enabled and whether the user only wants to
 /// see one section’s help.
@@ -129,6 +134,10 @@ impl fmt::Display for HelpString {
             write!(f, "\n{GIT_VIEW_HELP}")?;
         }
 
+        if cfg!(feature = "mercurial") {
+            write!(f, "\n{MERCURIAL_HELP}")?;
+        }
+
         if xattr::ENABLED {
             write!(f, "\n{EXTENDED_HELP}")?;
             write!(f, "\n{SECATTR_HELP}")?;

+ 28 - 0
src/options/mod.rs

@@ -188,6 +188,24 @@ impl Options {
         }
     }
 
+    pub fn should_scan_for_mercurial(&self) -> bool {
+        match self.view.mode {
+            Mode::Details(details::Options {
+                table: Some(ref table),
+                ..
+            })
+            | Mode::GridDetails(grid_details::Options {
+                details:
+                    details::Options {
+                        table: Some(ref table),
+                        ..
+                    },
+                ..
+            }) => table.columns.mercurial,
+            _ => false,
+        }
+    }
+
     /// Determines the complete set of options based on the given command-line
     /// arguments, after they’ve been parsed.
     fn deduce<V: Vars>(matches: &MatchedFlags<'_>, vars: &V) -> Result<Self, OptionsError> {
@@ -201,6 +219,16 @@ impl Options {
             )));
         }
 
+        if cfg!(not(feature = "mercurial"))
+            && matches
+                .has_where_any(|f| f.matches(&flags::MERCURIAL) || f.matches(&flags::NO_MERCURIAL))
+                .is_some()
+        {
+            return Err(OptionsError::Unsupported(String::from(
+                "Options --mercurial and --no-mercurial can't be used because `mercurial` feature is not enabled in this build of exa"
+            )));
+        }
+
         let view = View::deduce(matches, vars)?;
         let dir_action = DirAction::deduce(matches, matches!(view.mode, Mode::Details(_)))?;
         let filter = FileFilter::deduce(matches)?;

+ 3 - 0
src/options/view.rs

@@ -266,6 +266,8 @@ impl Columns {
             && !matches.has(&flags::NO_GIT)?
             && !no_git_env;
 
+        let mercurial = matches.has(&flags::MERCURIAL)? && !matches.has(&flags::NO_MERCURIAL)?;
+
         let blocksize = matches.has(&flags::BLOCKSIZE)?;
         let group = matches.has(&flags::GROUP)?;
         let inode = matches.has(&flags::INODE)?;
@@ -285,6 +287,7 @@ impl Columns {
             blocksize,
             group,
             git,
+            mercurial,
             subdir_git_repos,
             subdir_git_repos_no_stat,
             octal,

+ 18 - 1
src/output/color_scale.rs

@@ -2,6 +2,7 @@ use ansiterm::{Colour, Style};
 use log::trace;
 use palette::{FromColor, Oklab, Srgb};
 
+use crate::fs::feature::mercurial::MercurialCache;
 use crate::{
     fs::{dir_action::RecurseOptions, feature::git::GitCache, fields::Size, DotFilter, File},
     output::{table::TimeType, tree::TreeDepth},
@@ -42,6 +43,8 @@ impl ColorScaleInformation {
         git: Option<&GitCache>,
         git_ignoring: bool,
         r: Option<RecurseOptions>,
+        mercurial: Option<&MercurialCache>,
+        mercurial_ignoring: bool,
     ) -> Option<Self> {
         if color_scale.mode == ColorScaleMode::Fixed {
             None
@@ -63,6 +66,8 @@ impl ColorScaleInformation {
                 git_ignoring,
                 TreeDepth::root(),
                 r,
+                mercurial,
+                mercurial_ignoring,
             );
 
             Some(information)
@@ -110,6 +115,8 @@ fn update_information_recursively(
     git_ignoring: bool,
     depth: TreeDepth,
     r: Option<RecurseOptions>,
+    mercurial: Option<&MercurialCache>,
+    mercurial_ignoring: bool,
 ) {
     for file in files {
         if information.options.age {
@@ -143,7 +150,15 @@ fn update_information_recursively(
             match file.to_dir() {
                 Ok(dir) => {
                     let files: Vec<File<'_>> = dir
-                        .files(dot_filter, git, git_ignoring, false, false)
+                        .files(
+                            dot_filter,
+                            git,
+                            git_ignoring,
+                            false,
+                            false,
+                            mercurial,
+                            mercurial_ignoring,
+                        )
                         .flatten()
                         .collect();
 
@@ -155,6 +170,8 @@ fn update_information_recursively(
                         git_ignoring,
                         depth.deeper(),
                         r,
+                        mercurial,
+                        mercurial_ignoring,
                     );
                 }
                 Err(e) => trace!("Unable to access directory {}: {}", file.name, e),

+ 10 - 1
src/output/details.rs

@@ -71,6 +71,7 @@ use log::*;
 
 use crate::fs::dir_action::RecurseOptions;
 use crate::fs::feature::git::GitCache;
+use crate::fs::feature::mercurial::MercurialCache;
 use crate::fs::feature::xattr::Attribute;
 use crate::fs::fields::SecurityContextType;
 use crate::fs::filter::FileFilter;
@@ -139,6 +140,10 @@ pub struct Render<'a> {
     pub git: Option<&'a GitCache>,
 
     pub git_repos: bool,
+
+    pub mercurial: Option<&'a MercurialCache>,
+
+    pub mercurial_ignoring: bool,
 }
 
 #[rustfmt::skip]
@@ -172,6 +177,8 @@ impl<'a> Render<'a> {
             self.git,
             self.git_ignoring,
             self.recurse,
+            self.mercurial,
+            self.mercurial_ignoring,
         );
 
         if let Some(ref table) = self.opts.table {
@@ -189,7 +196,7 @@ impl<'a> Render<'a> {
                 (None, _) => { /* Keep Git how it is */ }
             }
 
-            let mut table = Table::new(table, self.git, self.theme, self.git_repos);
+            let mut table = Table::new(table, self.git, self.theme, self.git_repos, self.mercurial);
 
             if self.opts.header {
                 let header = table.header_row();
@@ -365,6 +372,8 @@ impl<'a> Render<'a> {
                     self.git_ignoring,
                     egg.file.deref_links,
                     egg.file.is_recursive_size(),
+                    self.mercurial,
+                    self.mercurial_ignoring,
                 ) {
                     match file_to_add {
                         Ok(f) => {

+ 38 - 21
src/output/grid_details.rs

@@ -6,6 +6,7 @@ use ansiterm::ANSIStrings;
 use term_grid as grid;
 
 use crate::fs::feature::git::GitCache;
+use crate::fs::feature::mercurial::MercurialCache;
 use crate::fs::filter::FileFilter;
 use crate::fs::{Dir, File};
 use crate::output::cell::{DisplayWidth, TextCell};
@@ -90,6 +91,10 @@ pub struct Render<'a> {
     pub console_width: usize,
 
     pub git_repos: bool,
+
+    pub mercurial: Option<&'a MercurialCache>,
+
+    pub mercurial_ignoring: bool,
 }
 
 impl<'a> Render<'a> {
@@ -102,16 +107,18 @@ impl<'a> Render<'a> {
     fn details_for_column(&self) -> DetailsRender<'a> {
         #[rustfmt::skip]
         return DetailsRender {
-            dir:           self.dir,
-            files:         Vec::new(),
-            theme:         self.theme,
-            file_style:    self.file_style,
-            opts:          self.details,
-            recurse:       None,
-            filter:        self.filter,
-            git_ignoring:  self.git_ignoring,
-            git:           self.git,
-            git_repos:     self.git_repos,
+            dir:                    self.dir,
+            files:                  Vec::new(),
+            theme:                  self.theme,
+            file_style:             self.file_style,
+            opts:                   self.details,
+            recurse:                None,
+            filter:                 self.filter,
+            git_ignoring:           self.git_ignoring,
+            git:                    self.git,
+            git_repos:              self.git_repos,
+            mercurial:              self.mercurial,
+            mercurial_ignoring:     self.mercurial_ignoring,
         };
     }
 
@@ -122,16 +129,18 @@ impl<'a> Render<'a> {
     pub fn give_up(self) -> DetailsRender<'a> {
         #[rustfmt::skip]
         return DetailsRender {
-            dir:           self.dir,
-            files:         self.files,
-            theme:         self.theme,
-            file_style:    self.file_style,
-            opts:          self.details,
-            recurse:       None,
-            filter:        self.filter,
-            git_ignoring:  self.git_ignoring,
-            git:           self.git,
-            git_repos:     self.git_repos,
+            dir:                    self.dir,
+            files:                  self.files,
+            theme:                  self.theme,
+            file_style:             self.file_style,
+            opts:                   self.details,
+            recurse:                None,
+            filter:                 self.filter,
+            git_ignoring:           self.git_ignoring,
+            git:                    self.git,
+            git_repos:              self.git_repos,
+            mercurial:              self.mercurial,
+            mercurial_ignoring:     self.mercurial_ignoring,
         };
     }
 
@@ -162,6 +171,8 @@ impl<'a> Render<'a> {
             self.git,
             self.git_ignoring,
             None,
+            self.mercurial,
+            self.mercurial_ignoring,
         );
 
         let (first_table, _) = self.make_table(options, &drender);
@@ -275,7 +286,13 @@ impl<'a> Render<'a> {
             (None, _) => { /* Keep Git how it is */ }
         }
 
-        let mut table = Table::new(options, self.git, self.theme, self.git_repos);
+        let mut table = Table::new(
+            options,
+            self.git,
+            self.theme,
+            self.git_repos,
+            self.mercurial,
+        );
         let mut rows = Vec::new();
 
         if self.details.header {

+ 34 - 0
src/output/render/mercurial.rs

@@ -0,0 +1,34 @@
+use crate::fs::fields as f;
+use crate::fs::fields::MercurialStatus;
+use crate::output::{DisplayWidth, TextCell};
+use ansiterm::Style;
+
+impl f::Mercurial {
+    pub fn render(self, colours: &dyn MercurialColours) -> TextCell {
+        let status = match self.status {
+            MercurialStatus::Modified => colours.modified().paint("M"),
+            MercurialStatus::Added => colours.added().paint("A"),
+            MercurialStatus::Removed => colours.removed().paint("R"),
+            MercurialStatus::Clean => colours.clean().paint("C"),
+            MercurialStatus::Missing => colours.missing().paint("!"),
+            MercurialStatus::NotTracked => colours.not_tracked().paint("?"),
+            MercurialStatus::Ignored => colours.ignored().paint("I"),
+            MercurialStatus::Directory => colours.ignored().paint("-"),
+        };
+
+        TextCell {
+            width: DisplayWidth::from(1),
+            contents: vec![status].into(),
+        }
+    }
+}
+
+pub trait MercurialColours {
+    fn modified(&self) -> Style;
+    fn added(&self) -> Style;
+    fn removed(&self) -> Style;
+    fn clean(&self) -> Style;
+    fn missing(&self) -> Style;
+    fn not_tracked(&self) -> Style;
+    fn ignored(&self) -> Style;
+}

+ 2 - 0
src/output/render/mod.rs

@@ -67,3 +67,5 @@ mod flags_windows;
     target_os = "windows"
 )))]
 mod flags;
+mod mercurial;
+pub use self::mercurial::MercurialColours;

+ 29 - 2
src/output/table.rs

@@ -11,6 +11,7 @@ use once_cell::sync::Lazy;
 use uzers::UsersCache;
 
 use crate::fs::feature::git::GitCache;
+use crate::fs::feature::mercurial::MercurialCache;
 use crate::fs::{fields as f, File};
 use crate::options::vars::EZA_WINDOWS_ATTRIBUTES;
 use crate::options::Vars;
@@ -48,6 +49,7 @@ pub struct Columns {
     pub blocksize: bool,
     pub group: bool,
     pub git: bool,
+    pub mercurial: bool,
     pub subdir_git_repos: bool,
     pub subdir_git_repos_no_stat: bool,
     pub octal: bool,
@@ -61,7 +63,12 @@ pub struct Columns {
 }
 
 impl Columns {
-    pub fn collect(&self, actually_enable_git: bool, git_repos: bool) -> Vec<Column> {
+    pub fn collect(
+        &self,
+        actually_enable_git: bool,
+        git_repos: bool,
+        actually_enable_mercurial: bool,
+    ) -> Vec<Column> {
         let mut columns = Vec::with_capacity(4);
 
         if self.inode {
@@ -139,6 +146,10 @@ impl Columns {
             columns.push(Column::SubdirGitRepo(false));
         }
 
+        if self.mercurial && actually_enable_mercurial {
+            columns.push(Column::MercurialStatus);
+        }
+
         columns
     }
 }
@@ -161,6 +172,7 @@ pub enum Column {
     Inode,
     GitStatus,
     SubdirGitRepo(bool),
+    MercurialStatus,
     #[cfg(unix)]
     Octal,
     #[cfg(unix)]
@@ -219,6 +231,7 @@ impl Column {
             Self::Inode => "inode",
             Self::GitStatus => "Git",
             Self::SubdirGitRepo(_) => "Repo",
+            Self::MercurialStatus => "Mercurial",
             #[cfg(unix)]
             Self::Octal => "Octal",
             #[cfg(unix)]
@@ -417,6 +430,7 @@ pub struct Table<'a> {
     group_format: GroupFormat,
     flags_format: FlagsFormat,
     git: Option<&'a GitCache>,
+    mercurial: Option<&'a MercurialCache>,
 }
 
 #[derive(Clone)]
@@ -430,8 +444,11 @@ impl<'a> Table<'a> {
         git: Option<&'a GitCache>,
         theme: &'a Theme,
         git_repos: bool,
+        mercurial: Option<&'a MercurialCache>,
     ) -> Table<'a> {
-        let columns = options.columns.collect(git.is_some(), git_repos);
+        let columns = options
+            .columns
+            .collect(git.is_some(), git_repos, mercurial.is_some());
         let widths = TableWidths::zero(columns.len());
         let env = &*ENVIRONMENT;
 
@@ -450,6 +467,7 @@ impl<'a> Table<'a> {
             #[cfg(unix)]
             group_format: options.group_format,
             flags_format: options.flags_format,
+            mercurial,
         }
     }
 
@@ -554,6 +572,7 @@ impl<'a> Table<'a> {
             Column::FileFlags => file.flags().render(self.theme.ui.flags, self.flags_format),
             Column::GitStatus => self.git_status(file).render(self.theme),
             Column::SubdirGitRepo(status) => self.subdir_git_repo(file, status).render(self.theme),
+            Column::MercurialStatus => self.mercurial_status(file).render(self.theme),
             #[cfg(unix)]
             Column::Octal => self.octal_permissions(file).render(self.theme.ui.octal),
 
@@ -582,6 +601,14 @@ impl<'a> Table<'a> {
             .unwrap_or_default()
     }
 
+    fn mercurial_status(&self, file: &File<'_>) -> f::Mercurial {
+        debug!("Getting Mercurial status for file {:?}", file.path);
+
+        self.mercurial
+            .map(|m| m.get(&file.path))
+            .unwrap_or_default()
+    }
+
     fn subdir_git_repo(&self, file: &File<'_>, status: bool) -> f::SubdirGitRepo {
         debug!("Getting subdir repo status for path {:?}", file.path);
 

+ 11 - 0
src/theme/default_theme.rs

@@ -81,6 +81,17 @@ impl UiStyles {
                 git_dirty: Yellow.bold(),
             },
 
+            #[rustfmt::skip]
+            mercurial: Mercurial {
+                modified:    Blue.normal(),
+                added:       Green.normal(),
+                removed:     Red.normal(),
+                clean:       Green.normal(),
+                missing:     Red.normal(),
+                not_tracked: Red.normal(),
+                ignored:     Style::default().dimmed(),
+            },
+
             security_context: SecurityContext {
                 none: Style::default(),
                 #[rustfmt::skip]

+ 31 - 0
src/theme/mod.rs

@@ -275,6 +275,37 @@ impl render::GitColours for Theme {
     fn conflicted(&self)    -> Style { self.ui.git.conflicted }
 }
 
+#[rustfmt::skip]
+impl render::MercurialColours for Theme {
+    fn modified(&self) -> Style {
+        self.ui.mercurial.modified
+    }
+
+    fn added(&self) -> Style {
+        self.ui.mercurial.added
+    }
+
+    fn removed(&self) -> Style {
+        self.ui.mercurial.removed
+    }
+
+    fn clean(&self) -> Style {
+        self.ui.mercurial.clean
+    }
+
+    fn missing(&self) -> Style {
+        self.ui.mercurial.missing
+    }
+
+    fn not_tracked(&self) -> Style {
+        self.ui.mercurial.not_tracked
+    }
+
+    fn ignored(&self) -> Style {
+        self.ui.mercurial.ignored
+    }
+}
+
 #[rustfmt::skip]
 impl render::GitRepoColours for Theme {
     fn branch_main(&self)  -> Style { self.ui.git_repo.branch_main }

+ 21 - 0
src/theme/ui_styles.rs

@@ -16,6 +16,7 @@ pub struct UiStyles {
     pub git_repo:         GitRepo,
     pub security_context: SecurityContext,
     pub file_type:        FileType,
+    pub mercurial:        Mercurial,
 
     pub punctuation:  Style,          // xx
     pub date:         Style,          // da
@@ -117,6 +118,18 @@ pub struct Git {
     pub conflicted: Style,  // gc
 }
 
+#[rustfmt::skip]
+#[derive(Clone, Copy, Debug, Default, PartialEq)]
+pub struct Mercurial {
+    pub modified: Style,    // mm
+    pub added: Style,       // ma
+    pub removed: Style,     // mr
+    pub clean: Style,       // mc
+    pub missing: Style,     // mx
+    pub not_tracked: Style, // mn
+    pub ignored: Style,     // mi
+}
+
 #[rustfmt::skip]
 #[derive(Clone, Copy, Debug, Default, PartialEq)]
 pub struct GitRepo {
@@ -243,6 +256,14 @@ impl UiStyles {
             "gi" => self.git.ignored                    = pair.to_style(),
             "gc" => self.git.conflicted                 = pair.to_style(),
 
+            "mm" => self.mercurial.modified             = pair.to_style(),
+            "ma" => self.mercurial.added                = pair.to_style(),
+            "mr" => self.mercurial.removed              = pair.to_style(),
+            "mc" => self.mercurial.clean                = pair.to_style(),
+            "mx" => self.mercurial.missing              = pair.to_style(),
+            "mn" => self.mercurial.not_tracked          = pair.to_style(),
+            "mi" => self.mercurial.ignored              = pair.to_style(),
+
             "Gm" => self.git_repo.branch_main           = pair.to_style(),
             "Go" => self.git_repo.branch_other          = pair.to_style(),
             "Gc" => self.git_repo.git_clean             = pair.to_style(),