From 718a00f6f2f88238546f7d33c1ea52217002495e Mon Sep 17 00:00:00 2001 From: David Torosyan Date: Sun, 20 Jan 2019 17:32:34 -0800 Subject: [PATCH] ripgrep: add --ignore-file-case-insensitive The --ignore-file-case-insensitive flag causes all .gitignore/.rgignore/.ignore files to have their globs matched without regard for case. Because this introduces a potentially significant performance regression, this is always disabled by default. Users that need case insensitive matching can enable it on a case by case basis. Closes #1164, Closes #1170 --- CHANGELOG.md | 10 +++++++++ GUIDE.md | 5 +++++ complete/_rg | 4 ++++ ignore/src/dir.rs | 49 ++++++++++++++++++++++++++++++++++++----- ignore/src/gitignore.rs | 49 +++++++++++++++++++++++++++++++---------- ignore/src/overrides.rs | 7 ++++-- ignore/src/walk.rs | 8 +++++++ src/app.rs | 22 ++++++++++++++++++ src/args.rs | 10 +++++++-- tests/regression.rs | 13 +++++++++++ 10 files changed, 156 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53c21cc2..e7792a3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +0.11.0 (TBD) +============ +TODO. + +Feature enhancements: + +* [FEATURE #1170](https://github.com/BurntSushi/ripgrep/pull/1170): + Add `--ignore-file-case-insensitive` for case insensitive .ignore globs. + + 0.10.0 (2018-09-07) =================== This is a new minor version release of ripgrep that contains some major new diff --git a/GUIDE.md b/GUIDE.md index ffcbff9f..8523b6a5 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -235,6 +235,11 @@ Like `.gitignore`, a `.ignore` file can be placed in any directory. Its rules will be processed with respect to the directory it resides in, just like `.gitignore`. +To process `.gitignore` and `.ignore` files case insensitively, use the flag +`--ignore-file-case-insensitive`. This is especially useful on case insensitive +file systems like those on Windows and macOS. Note though that this can come +with a significant performance penalty, and is therefore disabled by default. + For a more in depth description of how glob patterns in a `.gitignore` file are interpreted, please see `man gitignore`. diff --git a/complete/_rg b/complete/_rg index 6f7b0ef8..f48c0998 100644 --- a/complete/_rg +++ b/complete/_rg @@ -115,6 +115,10 @@ _rg() { "(--no-ignore-global --no-ignore-parent --no-ignore-vcs)--no-ignore[don't respect ignore files]" $no'(--ignore-global --ignore-parent --ignore-vcs)--ignore[respect ignore files]' + + '(ignore-file-case-insensitive)' # Ignore-file case sensitivity options + '--ignore-file-case-insensitive[process ignore files case insensitively]' + $no'--no-ignore-file-case-insensitive[process ignore files case sensitively]' + + '(ignore-global)' # Global ignore-file options "--no-ignore-global[don't respect global ignore files]" $no'--ignore-global[respect global ignore files]' diff --git a/ignore/src/dir.rs b/ignore/src/dir.rs index 66b18635..30f4cb87 100644 --- a/ignore/src/dir.rs +++ b/ignore/src/dir.rs @@ -73,6 +73,8 @@ struct IgnoreOptions { git_ignore: bool, /// Whether to read .git/info/exclude files. git_exclude: bool, + /// Whether to ignore files case insensitively + ignore_case_insensitive: bool, } /// Ignore is a matcher useful for recursively walking one or more directories. @@ -225,7 +227,11 @@ impl Ignore { Gitignore::empty() } else { let (m, err) = - create_gitignore(&dir, &self.0.custom_ignore_filenames); + create_gitignore( + &dir, + &self.0.custom_ignore_filenames, + self.0.opts.ignore_case_insensitive, + ); errs.maybe_push(err); m }; @@ -233,7 +239,12 @@ impl Ignore { if !self.0.opts.ignore { Gitignore::empty() } else { - let (m, err) = create_gitignore(&dir, &[".ignore"]); + let (m, err) = + create_gitignore( + &dir, + &[".ignore"], + self.0.opts.ignore_case_insensitive, + ); errs.maybe_push(err); m }; @@ -241,7 +252,12 @@ impl Ignore { if !self.0.opts.git_ignore { Gitignore::empty() } else { - let (m, err) = create_gitignore(&dir, &[".gitignore"]); + let (m, err) = + create_gitignore( + &dir, + &[".gitignore"], + self.0.opts.ignore_case_insensitive, + ); errs.maybe_push(err); m }; @@ -249,7 +265,12 @@ impl Ignore { if !self.0.opts.git_exclude { Gitignore::empty() } else { - let (m, err) = create_gitignore(&dir, &[".git/info/exclude"]); + let (m, err) = + create_gitignore( + &dir, + &[".git/info/exclude"], + self.0.opts.ignore_case_insensitive, + ); errs.maybe_push(err); m }; @@ -483,6 +504,7 @@ impl IgnoreBuilder { git_global: true, git_ignore: true, git_exclude: true, + ignore_case_insensitive: false, }, } } @@ -496,7 +518,11 @@ impl IgnoreBuilder { if !self.opts.git_global { Gitignore::empty() } else { - let (gi, err) = Gitignore::global(); + let mut builder = GitignoreBuilder::new(""); + builder + .case_insensitive(self.opts.ignore_case_insensitive) + .unwrap(); + let (gi, err) = builder.build_global(); if let Some(err) = err { debug!("{}", err); } @@ -627,6 +653,17 @@ impl IgnoreBuilder { self.opts.git_exclude = yes; self } + + /// Process ignore files case insensitively + /// + /// This is disabled by default. + pub fn ignore_case_insensitive( + &mut self, + yes: bool, + ) -> &mut IgnoreBuilder { + self.opts.ignore_case_insensitive = yes; + self + } } /// Creates a new gitignore matcher for the directory given. @@ -638,9 +675,11 @@ impl IgnoreBuilder { pub fn create_gitignore>( dir: &Path, names: &[T], + case_insensitive: bool, ) -> (Gitignore, Option) { let mut builder = GitignoreBuilder::new(dir); let mut errs = PartialErrorBuilder::default(); + builder.case_insensitive(case_insensitive).unwrap(); for name in names { let gipath = dir.join(name.as_ref()); errs.maybe_push_ignore_io(builder.add(gipath)); diff --git a/ignore/src/gitignore.rs b/ignore/src/gitignore.rs index a151e2de..66f98dfe 100644 --- a/ignore/src/gitignore.rs +++ b/ignore/src/gitignore.rs @@ -127,16 +127,7 @@ impl Gitignore { /// `$XDG_CONFIG_HOME/git/ignore` is read. If `$XDG_CONFIG_HOME` is not /// set or is empty, then `$HOME/.config/git/ignore` is used instead. pub fn global() -> (Gitignore, Option) { - match gitconfig_excludes_path() { - None => (Gitignore::empty(), None), - Some(path) => { - if !path.is_file() { - (Gitignore::empty(), None) - } else { - Gitignore::new(path) - } - } - } + GitignoreBuilder::new("").build_global() } /// Creates a new empty gitignore matcher that never matches anything. @@ -359,6 +350,36 @@ impl GitignoreBuilder { }) } + /// Build a global gitignore matcher using the configuration in this + /// builder. + /// + /// This consumes ownership of the builder unlike `build` because it + /// must mutate the builder to add the global gitignore globs. + /// + /// Note that this ignores the path given to this builder's constructor + /// and instead derives the path automatically from git's global + /// configuration. + pub fn build_global(mut self) -> (Gitignore, Option) { + match gitconfig_excludes_path() { + None => (Gitignore::empty(), None), + Some(path) => { + if !path.is_file() { + (Gitignore::empty(), None) + } else { + let mut errs = PartialErrorBuilder::default(); + errs.maybe_push_ignore_io(self.add(path)); + match self.build() { + Ok(gi) => (gi, errs.into_error_option()), + Err(err) => { + errs.push(err); + (Gitignore::empty(), errs.into_error_option()) + } + } + } + } + } + } + /// Add each glob from the file path given. /// /// The file given should be formatted as a `gitignore` file. @@ -505,12 +526,16 @@ impl GitignoreBuilder { /// Toggle whether the globs should be matched case insensitively or not. /// - /// When this option is changed, only globs added after the change will be affected. + /// When this option is changed, only globs added after the change will be + /// affected. /// /// This is disabled by default. pub fn case_insensitive( - &mut self, yes: bool + &mut self, + yes: bool, ) -> Result<&mut GitignoreBuilder, Error> { + // TODO: This should not return a `Result`. Fix this in the next semver + // release. self.case_insensitive = yes; Ok(self) } diff --git a/ignore/src/overrides.rs b/ignore/src/overrides.rs index c63532af..08dbdac2 100644 --- a/ignore/src/overrides.rs +++ b/ignore/src/overrides.rs @@ -139,13 +139,16 @@ impl OverrideBuilder { } /// Toggle whether the globs should be matched case insensitively or not. - /// + /// /// When this option is changed, only globs added after the change will be affected. /// /// This is disabled by default. pub fn case_insensitive( - &mut self, yes: bool + &mut self, + yes: bool, ) -> Result<&mut OverrideBuilder, Error> { + // TODO: This should not return a `Result`. Fix this in the next semver + // release. self.builder.case_insensitive(yes)?; Ok(self) } diff --git a/ignore/src/walk.rs b/ignore/src/walk.rs index aee7a881..ae1f58ba 100644 --- a/ignore/src/walk.rs +++ b/ignore/src/walk.rs @@ -764,6 +764,14 @@ impl WalkBuilder { self } + /// Process ignore files case insensitively + /// + /// This is disabled by default. + pub fn ignore_case_insensitive(&mut self, yes: bool) -> &mut WalkBuilder { + self.ig_builder.ignore_case_insensitive(yes); + self + } + /// Set a function for sorting directory entries by their path. /// /// If a compare function is set, the resulting iterator will return all diff --git a/src/app.rs b/src/app.rs index 5b25b72f..037feec3 100644 --- a/src/app.rs +++ b/src/app.rs @@ -571,6 +571,7 @@ pub fn all_args_and_flags() -> Vec { flag_iglob(&mut args); flag_ignore_case(&mut args); flag_ignore_file(&mut args); + flag_ignore_file_case_insensitive(&mut args); flag_invert_match(&mut args); flag_json(&mut args); flag_line_buffered(&mut args); @@ -1209,6 +1210,27 @@ directly on the command line, then used -g instead. args.push(arg); } +fn flag_ignore_file_case_insensitive(args: &mut Vec) { + const SHORT: &str = + "Process ignore files (.gitignore, .ignore, etc.) case insensitively."; + const LONG: &str = long!("\ +Process ignore files (.gitignore, .ignore, etc.) case insensitively. Note that +this comes with a performance penalty and is most useful on case insensitive +file systems (such as Windows). + +This flag can be disabled with the --no-ignore-file-case-insensitive flag. +"); + let arg = RGArg::switch("ignore-file-case-insensitive") + .help(SHORT).long_help(LONG) + .overrides("no-ignore-file-case-insensitive"); + args.push(arg); + + let arg = RGArg::switch("no-ignore-file-case-insensitive") + .hidden() + .overrides("ignore-file-case-insensitive"); + args.push(arg); +} + fn flag_invert_match(args: &mut Vec) { const SHORT: &str = "Invert matching."; const LONG: &str = long!("\ diff --git a/src/args.rs b/src/args.rs index 70af9df1..df55df25 100644 --- a/src/args.rs +++ b/src/args.rs @@ -797,7 +797,8 @@ impl ArgMatches { && !self.no_ignore_vcs() && !self.no_ignore_global()) .git_ignore(!self.no_ignore() && !self.no_ignore_vcs()) - .git_exclude(!self.no_ignore() && !self.no_ignore_vcs()); + .git_exclude(!self.no_ignore() && !self.no_ignore_vcs()) + .ignore_case_insensitive(self.ignore_file_case_insensitive()); if !self.no_ignore() { builder.add_custom_ignore_filename(".rgignore"); } @@ -1003,6 +1004,11 @@ impl ArgMatches { self.is_present("hidden") || self.unrestricted_count() >= 2 } + /// Returns true if ignore files should be processed case insensitively. + fn ignore_file_case_insensitive(&self) -> bool { + self.is_present("ignore-file-case-insensitive") + } + /// Return all of the ignore file paths given on the command line. fn ignore_paths(&self) -> Vec { let paths = match self.values_of_os("ignore-file") { @@ -1143,7 +1149,7 @@ impl ArgMatches { builder.add(&glob)?; } // This only enables case insensitivity for subsequent globs. - builder.case_insensitive(true)?; + builder.case_insensitive(true).unwrap(); for glob in self.values_of_lossy_vec("iglob") { builder.add(&glob)?; } diff --git a/tests/regression.rs b/tests/regression.rs index 4ee3ab53..90760ec9 100644 --- a/tests/regression.rs +++ b/tests/regression.rs @@ -568,3 +568,16 @@ rgtest!(r1064, |dir: Dir, mut cmd: TestCommand| { dir.create("input", "abc"); eqnice!("input:abc\n", cmd.arg("a(.*c)").stdout()); }); + +// See: https://github.com/BurntSushi/ripgrep/issues/1164 +rgtest!(r1164, |dir: Dir, mut cmd: TestCommand| { + dir.create_dir(".git"); + dir.create(".gitignore", "myfile"); + dir.create("MYFILE", "test"); + + cmd.arg("--ignore-file-case-insensitive").arg("test").assert_err(); + eqnice!( + "MYFILE:test\n", + cmd.arg("--no-ignore-file-case-insensitive").stdout() + ); +});