Unified · Split
| @@ -61,6 +61,10 @@ pub enum Value { · 60 unmodified lines | |||
| 61 | #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Default)] | 61 | #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Default)] |
| 62 | pub struct Attributes; | 62 | pub struct Attributes; |
| 63 | 63 | ||
| 64 | fn macro_mode() -> gix_glob::pattern::Mode { | ||
| 65 | gix_glob::pattern::Mode::all() | ||
| 66 | } | ||
| 67 | |||
| 64 | impl Pattern for Attributes { | 68 | impl Pattern for Attributes { |
| 65 | type Value = Value; | 69 | type Value = Value; |
| 66 | 70 | ||
| @@ -72,7 +76,7 @@ impl Pattern for Attributes { · 5 unmodified lines | |||
| 72 | crate::parse::Kind::Macro(macro_name) => ( | 76 | crate::parse::Kind::Macro(macro_name) => ( |
| 73 | gix_glob::Pattern { | 77 | gix_glob::Pattern { |
| 74 | text: macro_name.as_str().into(), | 78 | text: macro_name.as_str().into(), |
| 75 | mode: gix_glob::pattern::Mode::all(), | 79 | mode: macro_mode(), |
| 76 | first_wildcard_pos: None, | 80 | first_wildcard_pos: None, |
| 77 | }, | 81 | }, |
| 78 | Value::MacroAttributes(into_owned_assignments(assignments).ok()?), | 82 | Value::MacroAttributes(into_owned_assignments(assignments).ok()?), |
| @@ -93,7 +97,7 @@ impl Pattern for Attributes { · 14 unmodified lines | |||
| 93 | } | 97 | } |
| 94 | 98 | ||
| 95 | fn may_use_glob_pattern(pattern: &gix_glob::Pattern) -> bool { | 99 | fn may_use_glob_pattern(pattern: &gix_glob::Pattern) -> bool { |
| 96 | pattern.mode != gix_glob::pattern::Mode::all() | 100 | pattern.mode != macro_mode() |
| 97 | } | 101 | } |
| 98 | } | 102 | } |
| 99 | 103 | ||
| @@ -0,0 +1,116 @@ | |||
| 1 | use std::io::Read; | ||
| 2 | |||
| 3 | use bstr::{BStr, ByteSlice}; | ||
| 4 | use gix_attributes::{Ignore, Match, MatchGroup}; | ||
| 5 | use gix_glob::pattern::Case; | ||
| 6 | |||
| 7 | struct Expectations<'a> { | ||
| 8 | lines: bstr::Lines<'a>, | ||
| 9 | } | ||
| 10 | |||
| 11 | impl<'a> Iterator for Expectations<'a> { | ||
| 12 | type Item = (&'a BStr, Option<(&'a BStr, usize, &'a BStr)>); | ||
| 13 | |||
| 14 | fn next(&mut self) -> Option<Self::Item> { | ||
| 15 | let line = self.lines.next()?; | ||
| 16 | let (left, value) = line.split_at(line.find_byte(b'\t').unwrap()); | ||
| 17 | let value = value[1..].as_bstr(); | ||
| 18 | |||
| 19 | let source_and_line = if left == b"::" { | ||
| 20 | None | ||
| 21 | } else { | ||
| 22 | let mut tokens = left.split(|b| *b == b':'); | ||
| 23 | let source = tokens.next().unwrap().as_bstr(); | ||
| 24 | let line_number: usize = tokens.next().unwrap().to_str_lossy().parse().ok().unwrap(); | ||
| 25 | let pattern = tokens.next().unwrap().as_bstr(); | ||
| 26 | Some((source, line_number, pattern)) | ||
| 27 | }; | ||
| 28 | Some((value, source_and_line)) | ||
| 29 | } | ||
| 30 | } | ||
| 31 | |||
| 32 | #[test] | ||
| 33 | fn from_git_dir() -> crate::Result { | ||
| 34 | let dir = gix_testtools::scripted_fixture_read_only("make_global_and_external_and_dir_ignores.sh")?; | ||
| 35 | let repo_dir = dir.join("repo"); | ||
| 36 | let git_dir = repo_dir.join(".git"); | ||
| 37 | let baseline = std::fs::read(git_dir.parent().unwrap().join("git-check-ignore.baseline"))?; | ||
| 38 | let mut buf = Vec::new(); | ||
| 39 | let mut group = MatchGroup::from_git_dir(git_dir, Some(dir.join("user.exclude")), &mut buf)?; | ||
| 40 | |||
| 41 | assert!( | ||
| 42 | !group.add_patterns_file("not-a-file", false, None, &mut buf)?, | ||
| 43 | "missing files are no problem and cause a negative response" | ||
| 44 | ); | ||
| 45 | assert!( | ||
| 46 | group.add_patterns_file(repo_dir.join(".gitignore"), true, repo_dir.as_path().into(), &mut buf)?, | ||
| 47 | "existing files return true" | ||
| 48 | ); | ||
| 49 | |||
| 50 | buf.clear(); | ||
| 51 | let ignore_file = repo_dir.join("dir-with-ignore").join(".gitignore"); | ||
| 52 | std::fs::File::open(&ignore_file)?.read_to_end(&mut buf)?; | ||
| 53 | group.add_patterns_buffer(&buf, ignore_file, repo_dir.as_path().into()); | ||
| 54 | |||
| 55 | for (path, source_and_line) in (Expectations { | ||
| 56 | lines: baseline.lines(), | ||
| 57 | }) { | ||
| 58 | let actual = group.pattern_matching_relative_path( | ||
| 59 | path, | ||
| 60 | repo_dir | ||
| 61 | .join(path.to_str_lossy().as_ref()) | ||
| 62 | .metadata() | ||
| 63 | .ok() | ||
| 64 | .map(|m| m.is_dir()), | ||
| 65 | Case::Sensitive, | ||
| 66 | ); | ||
| 67 | match (actual, source_and_line) { | ||
| 68 | ( | ||
| 69 | Some(Match { | ||
| 70 | sequence_number, | ||
| 71 | pattern: _, | ||
| 72 | source, | ||
| 73 | value: _, | ||
| 74 | }), | ||
| 75 | Some((expected_source, line, _expected_pattern)), | ||
| 76 | ) => { | ||
| 77 | assert_eq!(sequence_number, line, "our counting should match the one used in git"); | ||
| 78 | assert_eq!( | ||
| 79 | source.map(|p| p.canonicalize().unwrap()), | ||
| 80 | Some(repo_dir.join(expected_source.to_str_lossy().as_ref()).canonicalize()?) | ||
| 81 | ); | ||
| 82 | } | ||
| 83 | (None, None) => {} | ||
| 84 | (actual, expected) => panic!("actual {actual:?} should match {expected:?} with path '{path}'"), | ||
| 85 | } | ||
| 86 | } | ||
| 87 | Ok(()) | ||
| 88 | } | ||
| 89 | |||
| 90 | #[test] | ||
| 91 | fn from_overrides() { | ||
| 92 | let input = ["simple", "pattern/"]; | ||
| 93 | let group = gix_attributes::MatchGroup::<Ignore>::from_overrides(input); | ||
| 94 | assert_eq!( | ||
| 95 | group.pattern_matching_relative_path("Simple", None, gix_glob::pattern::Case::Fold), | ||
| 96 | Some(pattern_to_match(&gix_glob::parse("simple").unwrap(), 0)) | ||
| 97 | ); | ||
| 98 | assert_eq!( | ||
| 99 | group.pattern_matching_relative_path("pattern", Some(true), gix_glob::pattern::Case::Sensitive), | ||
| 100 | Some(pattern_to_match(&gix_glob::parse("pattern/").unwrap(), 1)) | ||
| 101 | ); | ||
| 102 | assert_eq!(group.patterns.len(), 1); | ||
| 103 | assert_eq!( | ||
| 104 | gix_attributes::PatternList::<Ignore>::from_overrides(input), | ||
| 105 | group.patterns.into_iter().next().unwrap() | ||
| 106 | ); | ||
| 107 | } | ||
| 108 | |||
| 109 | fn pattern_to_match(pattern: &gix_glob::Pattern, sequence_number: usize) -> Match<'_, ()> { | ||
| 110 | Match { | ||
| 111 | pattern, | ||
| 112 | value: &(), | ||
| 113 | source: None, | ||
| 114 | sequence_number, | ||
| 115 | } | ||
| 116 | } | ||
| @@ -1,118 +1 @@ | |||
| 1 | mod ignore { | 1 | mod ignore; |
| 2 | use std::io::Read; | ||
| 3 | |||
| 4 | use bstr::{BStr, ByteSlice}; | ||
| 5 | use gix_attributes::{Ignore, Match, MatchGroup}; | ||
| 6 | use gix_glob::pattern::Case; | ||
| 7 | |||
| 8 | struct Expectations<'a> { | ||
| 9 | lines: bstr::Lines<'a>, | ||
| 10 | } | ||
| 11 | |||
| 12 | impl<'a> Iterator for Expectations<'a> { | ||
| 13 | type Item = (&'a BStr, Option<(&'a BStr, usize, &'a BStr)>); | ||
| 14 | |||
| 15 | fn next(&mut self) -> Option<Self::Item> { | ||
| 16 | let line = self.lines.next()?; | ||
| 17 | let (left, value) = line.split_at(line.find_byte(b'\t').unwrap()); | ||
| 18 | let value = value[1..].as_bstr(); | ||
| 19 | |||
| 20 | let source_and_line = if left == b"::" { | ||
| 21 | None | ||
| 22 | } else { | ||
| 23 | let mut tokens = left.split(|b| *b == b':'); | ||
| 24 | let source = tokens.next().unwrap().as_bstr(); | ||
| 25 | let line_number: usize = tokens.next().unwrap().to_str_lossy().parse().ok().unwrap(); | ||
| 26 | let pattern = tokens.next().unwrap().as_bstr(); | ||
| 27 | Some((source, line_number, pattern)) | ||
| 28 | }; | ||
| 29 | Some((value, source_and_line)) | ||
| 30 | } | ||
| 31 | } | ||
| 32 | |||
| 33 | #[test] | ||
| 34 | fn from_git_dir() -> crate::Result { | ||
| 35 | let dir = gix_testtools::scripted_fixture_read_only("make_global_and_external_and_dir_ignores.sh")?; | ||
| 36 | let repo_dir = dir.join("repo"); | ||
| 37 | let git_dir = repo_dir.join(".git"); | ||
| 38 | let baseline = std::fs::read(git_dir.parent().unwrap().join("git-check-ignore.baseline"))?; | ||
| 39 | let mut buf = Vec::new(); | ||
| 40 | let mut group = MatchGroup::from_git_dir(git_dir, Some(dir.join("user.exclude")), &mut buf)?; | ||
| 41 | |||
| 42 | assert!( | ||
| 43 | !group.add_patterns_file("not-a-file", false, None, &mut buf)?, | ||
| 44 | "missing files are no problem and cause a negative response" | ||
| 45 | ); | ||
| 46 | assert!( | ||
| 47 | group.add_patterns_file(repo_dir.join(".gitignore"), true, repo_dir.as_path().into(), &mut buf)?, | ||
| 48 | "existing files return true" | ||
| 49 | ); | ||
| 50 | |||
| 51 | buf.clear(); | ||
| 52 | let ignore_file = repo_dir.join("dir-with-ignore").join(".gitignore"); | ||
| 53 | std::fs::File::open(&ignore_file)?.read_to_end(&mut buf)?; | ||
| 54 | group.add_patterns_buffer(&buf, ignore_file, repo_dir.as_path().into()); | ||
| 55 | |||
| 56 | for (path, source_and_line) in (Expectations { | ||
| 57 | lines: baseline.lines(), | ||
| 58 | }) { | ||
| 59 | let actual = group.pattern_matching_relative_path( | ||
| 60 | path, | ||
| 61 | repo_dir | ||
| 62 | .join(path.to_str_lossy().as_ref()) | ||
| 63 | .metadata() | ||
| 64 | .ok() | ||
| 65 | .map(|m| m.is_dir()), | ||
| 66 | Case::Sensitive, | ||
| 67 | ); | ||
| 68 | match (actual, source_and_line) { | ||
| 69 | ( | ||
| 70 | Some(Match { | ||
| 71 | sequence_number, | ||
| 72 | pattern: _, | ||
| 73 | source, | ||
| 74 | value: _, | ||
| 75 | }), | ||
| 76 | Some((expected_source, line, _expected_pattern)), | ||
| 77 | ) => { | ||
| 78 | assert_eq!(sequence_number, line, "our counting should match the one used in git"); | ||
| 79 | assert_eq!( | ||
| 80 | source.map(|p| p.canonicalize().unwrap()), | ||
| 81 | Some(repo_dir.join(expected_source.to_str_lossy().as_ref()).canonicalize()?) | ||
| 82 | ); | ||
| 83 | } | ||
| 84 | (None, None) => {} | ||
| 85 | (actual, expected) => panic!("actual {actual:?} should match {expected:?} with path '{path}'"), | ||
| 86 | } | ||
| 87 | } | ||
| 88 | Ok(()) | ||
| 89 | } | ||
| 90 | |||
| 91 | #[test] | ||
| 92 | fn from_overrides() { | ||
| 93 | let input = ["simple", "pattern/"]; | ||
| 94 | let group = gix_attributes::MatchGroup::<Ignore>::from_overrides(input); | ||
| 95 | assert_eq!( | ||
| 96 | group.pattern_matching_relative_path("Simple", None, gix_glob::pattern::Case::Fold), | ||
| 97 | Some(pattern_to_match(&gix_glob::parse("simple").unwrap(), 0)) | ||
| 98 | ); | ||
| 99 | assert_eq!( | ||
| 100 | group.pattern_matching_relative_path("pattern", Some(true), gix_glob::pattern::Case::Sensitive), | ||
| 101 | Some(pattern_to_match(&gix_glob::parse("pattern/").unwrap(), 1)) | ||
| 102 | ); | ||
| 103 | assert_eq!(group.patterns.len(), 1); | ||
| 104 | assert_eq!( | ||
| 105 | gix_attributes::PatternList::<Ignore>::from_overrides(input), | ||
| 106 | group.patterns.into_iter().next().unwrap() | ||
| 107 | ); | ||
| 108 | } | ||
| 109 | |||
| 110 | fn pattern_to_match(pattern: &gix_glob::Pattern, sequence_number: usize) -> Match<'_, ()> { | ||
| 111 | Match { | ||
| 112 | pattern, | ||
| 113 | value: &(), | ||
| 114 | source: None, | ||
| 115 | sequence_number, | ||
| 116 | } | ||
| 117 | } | ||
| 118 | } | ||
| @@ -120,18 +120,25 @@ pub mod excludes { · 119 unmodified lines | |||
| 120 | /// Configure a file-system cache checking if files below the repository are excluded. | 120 | /// Configure a file-system cache checking if files below the repository are excluded. |
| 121 | /// | 121 | /// |
| 122 | /// This takes into consideration all the usual repository configuration. | 122 | /// This takes into consideration all the usual repository configuration. |
| 123 | // TODO: test, provide higher-level interface that is much easier to use and doesn't panic. | 123 | /// |
| 124 | /// `index` may be used to obtain `.gitignore` files directly from the index under certain conditions. | ||
| 125 | // TODO: test, provide higher-level interface that is much easier to use and doesn't panic when accessing entries | ||
| 126 | // by non-relative path. | ||
| 127 | // TODO: `index` might be so special (given the conditions we are talking about) that it's better obtained internally | ||
| 128 | // so the caller won't have to care. | ||
| 129 | // TODO: global files like `~/.gitignore` seem to be missing here, but we need a way to control if these should be loaded. | ||
| 130 | // probably that needs another permission in the repo options or a custom config variable. The latter is easiest to manage. | ||
| 124 | pub fn excludes( | 131 | pub fn excludes( |
| 125 | &self, | 132 | &self, |
| 126 | index: &gix_index::State, | 133 | index: &gix_index::State, |
| 127 | overrides: Option<gix_attributes::MatchGroup<gix_attributes::Ignore>>, | 134 | overrides: Option<gix_attributes::MatchGroup<gix_attributes::Ignore>>, |
| 128 | ) -> Result<gix_worktree::fs::Cache, Error> { | 135 | ) -> Result<gix_worktree::fs::Cache, Error> { |
| 129 | let repo = self.parent; | 136 | let repo = self.parent; |
| 130 | let case = repo | 137 | let case = if repo.config.ignore_case { |
| 131 | .config | 138 | gix_glob::pattern::Case::Fold |
| 132 | .ignore_case | 139 | } else { |
| 133 | .then_some(gix_glob::pattern::Case::Fold) | 140 | gix_glob::pattern::Case::Sensitive |
| 134 | .unwrap_or_default(); | 141 | }; |
| 135 | let mut buf = Vec::with_capacity(512); | 142 | let mut buf = Vec::with_capacity(512); |
| 136 | let excludes_file = match repo.config.excludes_file().transpose()? { | 143 | let excludes_file = match repo.config.excludes_file().transpose()? { |
| 137 | Some(user_path) => Some(user_path), | 144 | Some(user_path) => Some(user_path), |