diff --git a/README.md b/README.md index 63fab5bee..8f52df774 100644 --- a/README.md +++ b/README.md @@ -201,6 +201,12 @@ icons: # ignore-globs: # - .git +# == Use .gitignore == +# Whether to ignore files in git's .gitignore +# Possible values: false, true +# Do not specify this for the default behavior (false) +# gitignore: false + # == Indicators == # Whether to add indicator characters to certain listed files. # Possible values: false, true diff --git a/doc/lsd.md b/doc/lsd.md index 78a120465..50722d6c7 100644 --- a/doc/lsd.md +++ b/doc/lsd.md @@ -41,6 +41,9 @@ lsd is a ls command with a lot of pretty colours and some other stuff to enrich `--git` : Display git status. Directory git status is a reduction of included file statuses (recursively). +`--gitignore` +: Uses git .gitignore files for filtering files and directories + `--help` : Prints help information diff --git a/src/app.rs b/src/app.rs index 1be493972..4290007fb 100644 --- a/src/app.rs +++ b/src/app.rs @@ -154,11 +154,15 @@ pub struct Cli { #[arg(short, long)] pub inode: bool, - /// Show git status on file and directory" + /// Show git status on file and directory /// Only when used with --long option #[arg(short, long)] pub git: bool, + /// Uses git .gitignore files for filtering files and directories + #[arg(long)] + pub gitignore: bool, + /// When showing file information for a symbolic link, /// show information for the file the link references rather than for the link itself #[arg(short = 'L', long)] diff --git a/src/config_file.rs b/src/config_file.rs index e91b5884c..a5fd7ca8b 100644 --- a/src/config_file.rs +++ b/src/config_file.rs @@ -29,6 +29,7 @@ pub struct Config { pub date: Option, pub dereference: Option, pub display: Option, + pub gitignore: Option, pub icons: Option, pub ignore_globs: Option>, pub indicators: Option, @@ -114,6 +115,7 @@ impl Config { date: None, dereference: None, display: None, + gitignore: None, icons: None, ignore_globs: None, indicators: None, @@ -283,6 +285,12 @@ icons: # ignore-globs: # - .git +# == Use .gitignore == +# Whether to ignore files in git's .gitignore +# Possible values: false, true +# Do not specify this for the default behavior (false) +# gitignore: false + # == Indicators == # Whether to add indicator characters to certain listed files. # Possible values: false, true @@ -399,6 +407,7 @@ mod tests { date: None, dereference: Some(false), display: None, + gitignore: None, icons: Some(config_file::Icons { when: Some(IconOption::Auto), theme: Some(IconTheme::Fancy), diff --git a/src/core.rs b/src/core.rs index 97472ec40..b2df32459 100644 --- a/src/core.rs +++ b/src/core.rs @@ -126,7 +126,9 @@ impl Core { } }; - let cache = if self.flags.blocks.0.contains(&Block::GitStatus) { + let cache = if self.flags.blocks.0.contains(&Block::GitStatus) + || self.flags.gitignore.use_gitignore() + { Some(GitCache::new(&path)) } else { None diff --git a/src/flags.rs b/src/flags.rs index 52e7a49a0..69f702efa 100644 --- a/src/flags.rs +++ b/src/flags.rs @@ -3,6 +3,7 @@ pub mod color; pub mod date; pub mod dereference; pub mod display; +pub mod gitignore; pub mod header; pub mod hyperlink; pub mod icons; @@ -54,6 +55,8 @@ use clap::Error; #[cfg(doc)] use yaml_rust::Yaml; +use self::gitignore::Gitignore; + /// A struct to hold all set configuration flags for the application. #[derive(Clone, Debug, Default)] pub struct Flags { @@ -63,6 +66,7 @@ pub struct Flags { pub dereference: Dereference, pub display: Display, pub display_indicators: Indicators, + pub gitignore: Gitignore, pub icons: Icons, pub ignore_globs: IgnoreGlobs, pub layout: Layout, @@ -97,6 +101,7 @@ impl Flags { size: SizeFlag::configure_from(cli, config), permission: PermissionFlag::configure_from(cli, config), display_indicators: Indicators::configure_from(cli, config), + gitignore: Gitignore::configure_from(cli, config), icons: Icons::configure_from(cli, config), ignore_globs: IgnoreGlobs::configure_from(cli, config)?, no_symlink: NoSymlink::configure_from(cli, config), diff --git a/src/flags/gitignore.rs b/src/flags/gitignore.rs new file mode 100644 index 000000000..35b208b71 --- /dev/null +++ b/src/flags/gitignore.rs @@ -0,0 +1,116 @@ +//! This module defines the [Gitignore]. To set it up from [Cli], a [Config] and its +//! [Default] value, use the [configure_from](Gitignore::configure_from) method. + +use crate::app::Cli; +use crate::config_file::Config; + +use super::Configurable; +/// The struct holding whether or not to use the gitignore and methods to build it. +/// A value of `true` means to use the gitignore, and filter out gitignored files +#[derive(Clone, Debug, Copy, PartialEq, Eq)] +pub struct Gitignore(pub bool); + +impl Gitignore { + pub fn use_gitignore(&self) -> bool { + self.0 + } +} + +impl Configurable for Gitignore { + /// Returns a value from either [Cli], a [Config] or a [Default] value. The first value + /// that is not [None] is used. The order of precedence for the value used is: + /// - [from_cli](Gitignore::from_cli) + /// - [from_config](Gitignore::from_config) + /// - [Default::default] + fn configure_from(cli: &Cli, config: &Config) -> Self { + if let Some(value) = Self::from_cli(cli) { + return value; + } + + if let Some(value) = Self::from_config(config) { + return value; + } + + Default::default() + } + + /// Get a potential [Gitignore] from [Cli]. + /// + /// If the "gitignore" argument has been passed, this returns a [Gitignore] set to `true` in a [Some] + /// If the argument has not been passed, this returns [None]. + fn from_cli(cli: &Cli) -> Option { + cli.gitignore.then_some(Self(true)) + } + + /// Get a potential [Gitignore] from a [Config]. + /// + /// If the `Config::gitignore` contains an boolean value, + /// this returns a [Gitignore] set to the value of `Config::gitignore` in a [Some]. + /// Otherwise, returns [None]. + fn from_config(config: &Config) -> Option { + config.gitignore.map(Self) + } +} + +/// The default value of `Gitignore` is false. +impl Default for Gitignore { + fn default() -> Self { + Self(false) + } +} + +#[cfg(test)] +mod test { + use clap::Parser; + + use super::Gitignore; + + use super::super::Configurable; + + use crate::app::Cli; + use crate::config_file::Config; + + #[test] + fn test_configuration_from_none() { + let argv = ["lsd"]; + let cli = Cli::try_parse_from(argv).unwrap(); + assert!(matches!( + Gitignore::configure_from(&cli, &Config::with_none()), + Gitignore(false) + )); + } + + #[test] + fn test_configuration_from_args() { + let argv = ["lsd", "--gitignore"]; + let cli = Cli::try_parse_from(argv).unwrap(); + assert!(matches!( + Gitignore::configure_from(&cli, &Config::with_none()), + Gitignore(true) + )); + } + + #[test] + fn test_configuration_from_config() { + let argv = ["lsd"]; + let cli = Cli::try_parse_from(argv).unwrap(); + let mut c = Config::with_none(); + c.gitignore = Some(true); + assert!(matches!( + Gitignore::configure_from(&cli, &c), + Gitignore(true) + )); + } + + #[test] + fn test_from_cli_none() { + let argv = ["lsd"]; + let cli = Cli::try_parse_from(argv).unwrap(); + assert!(Gitignore::from_cli(&cli).is_none()); + } + + #[test] + fn test_from_config_none() { + assert!(Gitignore::from_config(&Config::with_none()).is_none()); + } +} diff --git a/src/meta/git_file_status.rs b/src/meta/git_file_status.rs index 160517d9b..24c7af4b0 100644 --- a/src/meta/git_file_status.rs +++ b/src/meta/git_file_status.rs @@ -64,4 +64,8 @@ impl GitFileStatus { }); ColoredString::new(Colors::default_style(), res) } + + pub fn is_ignored(&self) -> bool { + self.workdir == GitStatus::Ignored + } } diff --git a/src/meta/mod.rs b/src/meta/mod.rs index 57ebbe45b..851338b01 100644 --- a/src/meta/mod.rs +++ b/src/meta/mod.rs @@ -74,6 +74,13 @@ impl Meta { return Ok((None, ExitCode::OK)); } + let current_meta_git_status = cache.and_then(|cache| cache.get(&self.path, true)); + if flags.gitignore.use_gitignore() + && current_meta_git_status.is_some_and(|git_status| git_status.is_ignored()) + { + return Ok((None, ExitCode::OK)); + } + match self.file_type { FileType::Directory { .. } => (), FileType::SymLink { is_dir: true } => { @@ -107,7 +114,7 @@ impl Meta { )?; "..".clone_into(&mut parent_meta.name.name); - current_meta.git_status = cache.and_then(|cache| cache.get(¤t_meta.path, true)); + current_meta.git_status = current_meta_git_status; parent_meta.git_status = cache.and_then(|cache| cache.get(&parent_meta.path, true)); content.push(current_meta); @@ -165,6 +172,19 @@ impl Meta { continue; } + let is_directory = entry.file_type()?.is_dir(); + entry_meta.git_status = + cache.and_then(|cache| cache.get(&entry_meta.path, is_directory)); + + // skip .gitignored files if flags.gitignore is set + if flags.gitignore.use_gitignore() + && entry_meta + .git_status + .is_some_and(|git_status| git_status.is_ignored()) + { + continue; + } + // check dereferencing if flags.dereference.0 || !matches!(entry_meta.file_type, FileType::SymLink { .. }) { match entry_meta.recurse_into(depth - 1, flags, cache) { @@ -180,9 +200,6 @@ impl Meta { }; } - let is_directory = entry.file_type()?.is_dir(); - entry_meta.git_status = - cache.and_then(|cache| cache.get(&entry_meta.path, is_directory)); content.push(entry_meta); }