diff --git a/gitoxide-core/src/pack/receive.rs b/gitoxide-core/src/pack/receive.rs index 4caa4faa92d..32b273b90e6 100644 --- a/gitoxide-core/src/pack/receive.rs +++ b/gitoxide-core/src/pack/receive.rs @@ -221,7 +221,7 @@ mod async_io { &mut self.ctx, futures_lite::io::BlockOn::new(input), progress, - &refs, + refs, ) } } @@ -241,6 +241,7 @@ mod async_io { { let transport = net::connect( url, + #[allow(clippy::needless_update)] gix::protocol::transport::client::connect::Options { version: protocol.unwrap_or_default().into(), ..Default::default() diff --git a/gitoxide-core/src/repository/attributes.rs b/gitoxide-core/src/repository/attributes.rs deleted file mode 100644 index d37e2ea254a..00000000000 --- a/gitoxide-core/src/repository/attributes.rs +++ /dev/null @@ -1,66 +0,0 @@ -use std::io; - -use anyhow::bail; -use gix::prelude::FindExt; - -use crate::OutputFormat; - -pub mod query { - use crate::OutputFormat; - - pub struct Options { - pub format: OutputFormat, - pub statistics: bool, - } -} - -pub fn query( - repo: gix::Repository, - pathspecs: impl Iterator, - mut out: impl io::Write, - mut err: impl io::Write, - query::Options { format, statistics }: query::Options, -) -> anyhow::Result<()> { - if format != OutputFormat::Human { - bail!("JSON output isn't implemented yet"); - } - - let index = repo.index()?; - let mut cache = repo.attributes( - &index, - gix::worktree::cache::state::attributes::Source::WorktreeThenIdMapping, - gix::worktree::cache::state::ignore::Source::IdMapping, - None, - )?; - - let prefix = repo.prefix().expect("worktree - we have an index by now")?; - let mut matches = cache.attribute_matches(); - - for mut spec in pathspecs { - for path in spec.apply_prefix(&prefix).items() { - let is_dir = gix::path::from_bstr(path).metadata().ok().map(|m| m.is_dir()); - let entry = cache.at_entry(path, is_dir, |oid, buf| repo.objects.find_blob(oid, buf))?; - - if !entry.matching_attributes(&mut matches) { - continue; - } - for m in matches.iter() { - writeln!( - out, - "{}:{}:{}\t{}\t{}", - m.location.source.map(|p| p.to_string_lossy()).unwrap_or_default(), - m.location.sequence_number, - m.pattern, - path, - m.assignment - )?; - } - } - } - - if let Some(stats) = statistics.then(|| cache.take_statistics()) { - out.flush()?; - writeln!(err, "{:#?}", stats).ok(); - } - Ok(()) -} diff --git a/gitoxide-core/src/repository/attributes/mod.rs b/gitoxide-core/src/repository/attributes/mod.rs new file mode 100644 index 00000000000..ad49bf3c3ca --- /dev/null +++ b/gitoxide-core/src/repository/attributes/mod.rs @@ -0,0 +1,5 @@ +pub mod query; +pub use query::function::query; + +pub mod validate_baseline; +pub use validate_baseline::function::validate_baseline; diff --git a/gitoxide-core/src/repository/attributes/query.rs b/gitoxide-core/src/repository/attributes/query.rs new file mode 100644 index 00000000000..3e170117065 --- /dev/null +++ b/gitoxide-core/src/repository/attributes/query.rs @@ -0,0 +1,69 @@ +use crate::OutputFormat; + +pub struct Options { + pub format: OutputFormat, + pub statistics: bool, +} + +pub(crate) mod function { + use crate::repository::attributes::query::{attributes_cache, Options}; + use crate::OutputFormat; + use std::io; + + use anyhow::bail; + use gix::prelude::FindExt; + + pub fn query( + repo: gix::Repository, + pathspecs: impl Iterator, + mut out: impl io::Write, + mut err: impl io::Write, + Options { format, statistics }: Options, + ) -> anyhow::Result<()> { + if format != OutputFormat::Human { + bail!("JSON output isn't implemented yet"); + } + + let mut cache = attributes_cache(&repo)?; + let prefix = repo.prefix().expect("worktree - we have an index by now")?; + let mut matches = cache.attribute_matches(); + + for mut spec in pathspecs { + for path in spec.apply_prefix(&prefix).items() { + let is_dir = gix::path::from_bstr(path).metadata().ok().map(|m| m.is_dir()); + let entry = cache.at_entry(path, is_dir, |oid, buf| repo.objects.find_blob(oid, buf))?; + + if !entry.matching_attributes(&mut matches) { + continue; + } + for m in matches.iter() { + writeln!( + out, + "{}:{}:{}\t{}\t{}", + m.location.source.map(|p| p.to_string_lossy()).unwrap_or_default(), + m.location.sequence_number, + m.pattern, + path, + m.assignment + )?; + } + } + } + + if let Some(stats) = statistics.then(|| cache.take_statistics()) { + out.flush()?; + writeln!(err, "{:#?}", stats).ok(); + } + Ok(()) + } +} + +pub(crate) fn attributes_cache(repo: &gix::Repository) -> anyhow::Result { + let index = repo.index()?; + Ok(repo.attributes( + &index, + gix::worktree::cache::state::attributes::Source::WorktreeThenIdMapping, + gix::worktree::cache::state::ignore::Source::IdMapping, + None, + )?) +} diff --git a/gitoxide-core/src/repository/attributes/validate_baseline.rs b/gitoxide-core/src/repository/attributes/validate_baseline.rs new file mode 100644 index 00000000000..ba1098790e4 --- /dev/null +++ b/gitoxide-core/src/repository/attributes/validate_baseline.rs @@ -0,0 +1,366 @@ +use crate::OutputFormat; + +pub struct Options { + pub format: OutputFormat, + pub statistics: bool, + pub ignore: bool, +} + +pub(crate) mod function { + use std::collections::BTreeSet; + use std::io; + use std::io::{BufRead, Write}; + use std::iter::Peekable; + use std::ops::Sub; + use std::path::PathBuf; + use std::sync::atomic::Ordering; + + use anyhow::{anyhow, bail}; + use gix::odb::FindExt; + use gix::Progress; + + use crate::repository::attributes::query::attributes_cache; + use crate::repository::attributes::validate_baseline::Options; + use crate::OutputFormat; + + pub fn validate_baseline( + repo: gix::Repository, + pathspecs: Option + Send + 'static>, + mut progress: impl Progress + 'static, + mut out: impl io::Write, + mut err: impl io::Write, + Options { + format, + statistics, + ignore, + }: Options, + ) -> anyhow::Result<()> { + if format != OutputFormat::Human { + bail!("JSON output isn't implemented yet"); + } + + let mut num_entries = None; + let pathspecs = pathspecs + .map(|i| anyhow::Result::Ok(Box::new(i) as Box + Send + 'static>)) + .unwrap_or_else({ + let repo = repo.clone(); + let num_entries = &mut num_entries; + move || -> anyhow::Result<_> { + let index = repo.open_index()?; + let (entries, path_backing) = index.into_parts().0.into_entries(); + *num_entries = Some(entries.len()); + Ok(Box::new(entries.into_iter().map(move |e| { + gix::path::Spec::from_bytes(e.path_in(&path_backing)).expect("each entry path is a valid spec") + }))) + } + })?; + + let work_dir = repo + .work_dir() + .map(ToOwned::to_owned) + .ok_or_else(|| anyhow!("repository at {:?} must have a worktree checkout", repo.path()))?; + let (tx_base, rx_base) = std::sync::mpsc::channel::<(String, Baseline)>(); + let feed_attrs = { + let (tx, rx) = std::sync::mpsc::sync_channel::(1); + std::thread::spawn({ + let path = work_dir.clone(); + let tx_base = tx_base.clone(); + let mut progress = progress.add_child("attributes"); + move || -> anyhow::Result<()> { + let mut child = std::process::Command::new(GIT_NAME) + .args(["check-attr", "--stdin", "-a"]) + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::null()) + .current_dir(path) + .spawn()?; + + std::thread::spawn({ + let mut stdin = child.stdin.take().expect("we configured it"); + move || -> anyhow::Result<()> { + progress.init(num_entries, gix::progress::count("paths")); + let start = std::time::Instant::now(); + for spec in rx { + progress.inc(); + for path in spec.items() { + stdin.write_all(path.as_ref())?; + stdin.write_all(b"\n")?; + } + } + progress.show_throughput(start); + Ok(()) + } + }); + + let stdout = std::io::BufReader::new(child.stdout.take().expect("we configured it")); + let mut lines = stdout.lines().filter_map(Result::ok).peekable(); + while let Some(baseline) = parse_attributes(&mut lines) { + if tx_base.send(baseline).is_err() { + child.kill().ok(); + break; + } + } + + Ok(()) + } + }); + tx + }; + let feed_excludes = ignore.then(|| { + let (tx, rx) = std::sync::mpsc::sync_channel::(1); + std::thread::spawn({ + let path = work_dir.clone(); + let tx_base = tx_base.clone(); + let mut progress = progress.add_child("excludes"); + move || -> anyhow::Result<()> { + let mut child = std::process::Command::new(GIT_NAME) + .args(["check-ignore", "--stdin", "-nv", "--no-index"]) + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::null()) + .current_dir(path) + .spawn()?; + + std::thread::spawn({ + let mut stdin = child.stdin.take().expect("we configured it"); + move || -> anyhow::Result<()> { + progress.init(num_entries, gix::progress::count("paths")); + let start = std::time::Instant::now(); + for spec in rx { + progress.inc(); + for path in spec.items() { + stdin.write_all(path.as_ref())?; + stdin.write_all(b"\n")?; + } + } + progress.show_throughput(start); + Ok(()) + } + }); + + let stdout = std::io::BufReader::new(child.stdout.take().expect("we configured it")); + for line in stdout.lines() { + let line = line?; + if let Some(baseline) = parse_exclude(&line) { + if tx_base.send(baseline).is_err() { + child.kill().ok(); + break; + } + } else { + eprintln!("Failed to parse line {line:?} - ignored"); + } + } + + Ok(()) + } + }); + tx + }); + drop(tx_base); + + std::thread::spawn(move || { + for spec in pathspecs { + if feed_attrs.send(spec.clone()).is_err() { + break; + } + if let Some(ch) = feed_excludes.as_ref() { + if ch.send(spec).is_err() { + break; + } + } + } + }); + + let mut cache = attributes_cache(&repo)?; + let mut matches = cache.attribute_matches(); + let mut progress = progress.add_child("validate"); + let mut mismatches = Vec::new(); + let start = std::time::Instant::now(); + progress.init( + num_entries.map(|n| n + if ignore { n } else { 0 }), + gix::progress::count("paths"), + ); + + for (rela_path, baseline) in rx_base { + let entry = cache.at_entry(rela_path.as_str(), Some(false), |oid, buf| { + repo.objects.find_blob(oid, buf) + })?; + match baseline { + Baseline::Attribute { assignments: expected } => { + entry.matching_attributes(&mut matches); + let fast_path_mismatch = matches + .iter() + .map(|m| m.assignment) + .zip(expected.iter().map(|a| a.as_ref())) + .any(|(a, b)| a != b); + if fast_path_mismatch { + let actual_set = BTreeSet::from_iter(matches.iter().map(|m| m.assignment)); + let expected_set = BTreeSet::from_iter(expected.iter().map(|a| a.as_ref())); + let too_few_or_too_many = + !(expected_set.sub(&actual_set).is_empty() && actual_set.sub(&expected_set).is_empty()); + if too_few_or_too_many { + mismatches.push(( + rela_path, + Mismatch::Attributes { + actual: matches.iter().map(|m| m.assignment.to_owned()).collect(), + expected, + }, + )) + } + } + } + Baseline::Exclude { location } => { + let match_ = entry.matching_exclude_pattern(); + if match_.is_some() != location.is_some() { + mismatches.push(( + rela_path, + Mismatch::Exclude { + actual: match_.map(Into::into), + expected: location, + }, + )) + } + } + } + progress.inc(); + } + + if let Some(stats) = statistics.then(|| cache.take_statistics()) { + out.flush()?; + writeln!(err, "{:#?}", stats).ok(); + } + progress.show_throughput(start); + + if mismatches.is_empty() { + Ok(()) + } else { + for (rela_path, mm) in &mismatches { + writeln!(err, "{rela_path}: {mm:#?}").ok(); + } + bail!( + "{}: Validation failed with {} mismatches out of {}", + gix::path::realpath(&work_dir).unwrap_or(work_dir).display(), + mismatches.len(), + progress + .counter() + .map(|a| a.load(Ordering::Relaxed)) + .unwrap_or_default() + ); + } + } + + static GIT_NAME: &str = if cfg!(windows) { "git.exe" } else { "git" }; + + enum Baseline { + Attribute { assignments: Vec }, + Exclude { location: Option }, + } + + #[derive(Debug)] + pub struct ExcludeLocation { + pub line: usize, + pub rela_source_file: String, + pub pattern: String, + } + + #[derive(Debug)] + pub enum Mismatch { + Attributes { + actual: Vec, + expected: Vec, + }, + Exclude { + actual: Option, + expected: Option, + }, + } + + #[derive(Debug)] + pub struct ExcludeMatch { + pub pattern: gix::glob::Pattern, + pub source: Option, + pub sequence_number: usize, + } + + impl From> for ExcludeMatch { + fn from(value: gix::ignore::search::Match<'_>) -> Self { + ExcludeMatch { + pattern: value.pattern.clone(), + source: value.source.map(ToOwned::to_owned), + sequence_number: value.sequence_number, + } + } + } + + fn parse_exclude(line: &str) -> Option<(String, Baseline)> { + let (left, value) = line.split_at(line.find(|c| c == '\t')?); + let value = &value[1..]; + + let location = if left == "::" { + None + } else { + let mut tokens = left.split(|b| b == ':'); + let source = tokens.next()?; + let line_number: usize = tokens.next()?.parse().ok()?; + let pattern = tokens.next()?; + Some(ExcludeLocation { + line: line_number, + rela_source_file: source.into(), + pattern: pattern.into(), + }) + }; + Some((value.to_string(), Baseline::Exclude { location })) + } + + fn parse_attributes(lines: &mut Peekable>) -> Option<(String, Baseline)> { + let first = lines.next()?; + let mut out = Vec::new(); + let (path, assignment) = parse_attribute_line(&first)?; + + let current = path.to_owned(); + out.push(assignment.to_owned()); + loop { + let next_line = match lines.peek() { + None => break, + Some(l) => l, + }; + let (next_path, next_assignment) = parse_attribute_line(next_line)?; + if next_path != current { + return Some((current, Baseline::Attribute { assignments: out })); + } else { + out.push(next_assignment.to_owned()); + lines.next(); + } + } + Some((current, Baseline::Attribute { assignments: out })) + } + + fn parse_attribute_line(line: &str) -> Option<(&str, gix::attrs::AssignmentRef<'_>)> { + use gix::attrs::StateRef; + use gix::bstr::ByteSlice; + + let mut prev = None; + let mut tokens = line.splitn(3, |b| { + let is_match = b == ' ' && prev.take() == Some(':'); + prev = Some(b); + is_match + }); + if let Some(((mut path, attr), info)) = tokens.next().zip(tokens.next()).zip(tokens.next()) { + let state = match info { + "set" => StateRef::Set, + "unset" => StateRef::Unset, + "unspecified" => StateRef::Unspecified, + _ => StateRef::from_bytes(info.as_bytes()), + }; + path = path.trim_end_matches(|b| b == ':'); + let attr = attr.trim_end_matches(|b| b == ':'); + let assignment = gix::attrs::AssignmentRef { + name: gix::attrs::NameRef::try_from(attr.as_bytes().as_bstr()).ok()?, + state, + }; + Some((path, assignment)) + } else { + None + } + } +} diff --git a/gix-attributes/src/search/attributes.rs b/gix-attributes/src/search/attributes.rs index a34ae8b3ea9..078c187bbfc 100644 --- a/gix-attributes/src/search/attributes.rs +++ b/gix-attributes/src/search/attributes.rs @@ -25,10 +25,16 @@ impl Search { collection: &mut MetadataCollection, ) -> std::io::Result { let mut group = Self::default(); - group.add_patterns_buffer(b"[attr]binary -diff -merge -text", "[builtin]", None, collection); + group.add_patterns_buffer( + b"[attr]binary -diff -merge -text", + "[builtin]", + None, + collection, + true, /* allow macros */ + ); for path in files.into_iter() { - group.add_patterns_file(path, true, None, buf, collection)?; + group.add_patterns_file(path, true, None, buf, collection, true /* allow macros */)?; } Ok(group) } @@ -39,6 +45,7 @@ impl Search { /// Add the given file at `source` to our patterns if it exists, otherwise do nothing. /// Update `collection` with newly added attribute names. /// If a `root` is provided, it's not considered a global file anymore. + /// If `allow_macros` is `true`, macros will be processed like normal, otherwise they will be skipped entirely. /// Returns `true` if the file was added, or `false` if it didn't exist. pub fn add_patterns_file( &mut self, @@ -47,24 +54,39 @@ impl Search { root: Option<&Path>, buf: &mut Vec, collection: &mut MetadataCollection, + allow_macros: bool, ) -> std::io::Result { + // TODO: should `Pattern` trait use an instance as first argument to carry this information + // (so no `retain` later, it's slower than skipping) let was_added = gix_glob::search::add_patterns_file(&mut self.patterns, source, follow_symlinks, root, buf)?; if was_added { - collection.update_from_list(self.patterns.last_mut().expect("just added")); + let last = self.patterns.last_mut().expect("just added"); + if !allow_macros { + last.patterns + .retain(|p| !matches!(p.value, Value::MacroAssignments { .. })) + } + collection.update_from_list(last); } Ok(was_added) } /// Add patterns as parsed from `bytes`, providing their `source` path and possibly their `root` path, the path they /// are relative to. This also means that `source` is contained within `root` if `root` is provided. + /// If `allow_macros` is `true`, macros will be processed like normal, otherwise they will be skipped entirely. pub fn add_patterns_buffer( &mut self, bytes: &[u8], source: impl Into, root: Option<&Path>, collection: &mut MetadataCollection, + allow_macros: bool, ) { self.patterns.push(pattern::List::from_bytes(bytes, source, root)); - collection.update_from_list(self.patterns.last_mut().expect("just added")); + let last = self.patterns.last_mut().expect("just added"); + if !allow_macros { + last.patterns + .retain(|p| !matches!(p.value, Value::MacroAssignments { .. })) + } + collection.update_from_list(last); } /// Pop the last attribute patterns list from our queue. diff --git a/gix-attributes/tests/search/mod.rs b/gix-attributes/tests/search/mod.rs index 7f3fee09545..38283329c2a 100644 --- a/gix-attributes/tests/search/mod.rs +++ b/gix-attributes/tests/search/mod.rs @@ -48,6 +48,7 @@ mod specials { .unwrap_or_else(|| Path::new("").into()), rela_containing_dir.map(|_| Path::new("")), &mut collection, + true, ); let mut out = Outcome::default(); out.initialize(&collection); @@ -81,12 +82,14 @@ fn baseline() -> crate::Result { ("a/.gitattributes", true), ("a/b/.gitattributes", true), ] { + let is_global = !use_base; group.add_patterns_file( base.join(file), false, use_base.then_some(base.as_path()), &mut buf, &mut collection, + is_global, /* allow macros */ )?; } assert_eq!( @@ -131,7 +134,14 @@ fn all_attributes_are_listed_in_declaration_order() -> crate::Result { let (mut group, mut collection, base, input) = baseline::user_attributes("lookup-order")?; let mut buf = Vec::new(); - group.add_patterns_file(base.join(".gitattributes"), false, None, &mut buf, &mut collection)?; + group.add_patterns_file( + base.join(".gitattributes"), + false, + None, + &mut buf, + &mut collection, + true, /* use macros */ + )?; let mut out = gix_attributes::search::Outcome::default(); out.initialize(&collection); @@ -217,7 +227,14 @@ fn given_attributes_are_made_available_in_given_order() -> crate::Result { baseline::user_attributes_named_baseline("lookup-order", "baseline.selected")?; let mut buf = Vec::new(); - group.add_patterns_file(base.join(".gitattributes"), false, None, &mut buf, &mut collection)?; + group.add_patterns_file( + base.join(".gitattributes"), + false, + None, + &mut buf, + &mut collection, + true, /* use macros */ + )?; let mut out = gix_attributes::search::Outcome::default(); out.initialize_with_selection(&collection, ["my-binary", "recursive", "unspecified"]); diff --git a/gix-ignore/src/search.rs b/gix-ignore/src/search.rs index 5c957f136fa..e5310b35ce3 100644 --- a/gix-ignore/src/search.rs +++ b/gix-ignore/src/search.rs @@ -10,11 +10,9 @@ use crate::Search; /// Describes a matching pattern within a search for ignored paths. #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] -pub struct Match<'a, T> { +pub struct Match<'a> { /// The glob pattern itself, like `/target/*`. pub pattern: &'a gix_glob::Pattern, - /// The value associated with the pattern. - pub value: &'a T, /// The path to the source from which the pattern was loaded, or `None` if it was specified by other means. pub source: Option<&'a Path>, /// The line at which the pattern was found in its `source` file, or the occurrence in which it was provided. @@ -114,7 +112,7 @@ pub fn pattern_matching_relative_path<'a>( basename_pos: Option, is_dir: Option, case: gix_glob::pattern::Case, -) -> Option> { +) -> Option> { let (relative_path, basename_start_pos) = list.strip_base_handle_recompute_basename_pos(relative_path, basename_pos, case)?; list.patterns @@ -124,14 +122,13 @@ pub fn pattern_matching_relative_path<'a>( .find_map( |pattern::Mapping { pattern, - value, + value: (), sequence_number, }| { pattern .matches_repo_relative_path(relative_path, basename_start_pos, is_dir, case) .then_some(Match { pattern, - value, source: list.source.as_deref(), sequence_number: *sequence_number, }) @@ -172,7 +169,7 @@ impl Search { relative_path: impl Into<&'a BStr>, is_dir: Option, case: gix_glob::pattern::Case, - ) -> Option> { + ) -> Option> { let relative_path = relative_path.into(); let basename_pos = relative_path.rfind(b"/").map(|p| p + 1); self.patterns diff --git a/gix-ignore/tests/search/mod.rs b/gix-ignore/tests/search/mod.rs index 3cb8763dbc8..8fc70bbd056 100644 --- a/gix-ignore/tests/search/mod.rs +++ b/gix-ignore/tests/search/mod.rs @@ -81,7 +81,6 @@ fn baseline_from_git_dir() -> crate::Result { sequence_number, pattern: _, source, - value: _, }), Some((expected_source, line, _expected_pattern)), ) => { @@ -119,10 +118,9 @@ fn from_overrides() { ); } -fn pattern_to_match(pattern: &gix_glob::Pattern, sequence_number: usize) -> Match<'_, ()> { +fn pattern_to_match(pattern: &gix_glob::Pattern, sequence_number: usize) -> Match<'_> { Match { pattern, - value: &(), source: None, sequence_number, } diff --git a/gix-index/src/access/mod.rs b/gix-index/src/access/mod.rs index fa0215d9fda..e38752e5690 100644 --- a/gix-index/src/access/mod.rs +++ b/gix-index/src/access/mod.rs @@ -147,6 +147,13 @@ impl State { }) } + /// Return all parts that relate to entries, which includes path storage. + /// + /// This can be useful for obtaining a standalone, boxable iterator + pub fn into_entries(self) -> (Vec, PathStorage) { + (self.entries, self.path_backing) + } + /// Sometimes it's needed to remove the path backing to allow certain mutation to happen in the state while supporting reading the entry's /// path. pub fn take_path_backing(&mut self) -> PathStorage { diff --git a/gix-worktree/src/cache/platform.rs b/gix-worktree/src/cache/platform.rs index f52b532051a..27d0bfbc822 100644 --- a/gix-worktree/src/cache/platform.rs +++ b/gix-worktree/src/cache/platform.rs @@ -28,7 +28,7 @@ impl<'a> Platform<'a> { /// # Panics /// /// If the cache was configured without exclude patterns. - pub fn matching_exclude_pattern(&self) -> Option> { + pub fn matching_exclude_pattern(&self) -> Option> { let ignore = self.parent.state.ignore_or_panic(); let relative_path = gix_path::to_unix_separators_on_windows(gix_path::into_bstr(self.parent.stack.current_relative())); diff --git a/gix-worktree/src/cache/state/attributes.rs b/gix-worktree/src/cache/state/attributes.rs index 64316b35b43..1b1dba4a7d3 100644 --- a/gix-worktree/src/cache/state/attributes.rs +++ b/gix-worktree/src/cache/state/attributes.rs @@ -92,6 +92,7 @@ impl Attributes { let attr_file_in_index = id_mappings.binary_search_by(|t| t.0.as_bstr().cmp(attr_path_relative.as_ref())); // Git does not follow symbolic links as per documentation. let no_follow_symlinks = false; + let read_macros_as_dir_is_root = root == dir; let mut added = false; match self.source { @@ -100,8 +101,13 @@ impl Attributes { let blob = find(&id_mappings[idx].1, buf) .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?; let attr_path = gix_path::from_bstring(attr_path_relative.into_owned()); - self.stack - .add_patterns_buffer(blob.data, attr_path, Some(Path::new("")), &mut self.collection); + self.stack.add_patterns_buffer( + blob.data, + attr_path, + Some(Path::new("")), + &mut self.collection, + read_macros_as_dir_is_root, + ); added = true; stats.patterns_buffers += 1; } @@ -112,6 +118,7 @@ impl Attributes { Some(root), buf, &mut self.collection, + read_macros_as_dir_is_root, )?; stats.pattern_files += usize::from(added); stats.tried_pattern_files += 1; @@ -124,6 +131,7 @@ impl Attributes { Some(root), buf, &mut self.collection, + read_macros_as_dir_is_root, )?; stats.pattern_files += usize::from(added); stats.tried_pattern_files += 1; @@ -131,8 +139,13 @@ impl Attributes { let blob = find(&id_mappings[idx].1, buf) .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?; let attr_path = gix_path::from_bstring(attr_path_relative.into_owned()); - self.stack - .add_patterns_buffer(blob.data, attr_path, Some(Path::new("")), &mut self.collection); + self.stack.add_patterns_buffer( + blob.data, + attr_path, + Some(Path::new("")), + &mut self.collection, + read_macros_as_dir_is_root, + ); added = true; stats.patterns_buffers += 1; } @@ -142,15 +155,20 @@ impl Attributes { // Need one stack level per component so push and pop matches, but only if this isn't the root level which is never popped. if !added && self.info_attributes.is_none() { self.stack - .add_patterns_buffer(&[], Path::new(""), None, &mut self.collection) + .add_patterns_buffer(&[], Path::new(""), None, &mut self.collection, true) } // When reading the root, always the first call, we can try to also read the `.git/info/attributes` file which is // by nature never popped, and follows the root, as global. if let Some(info_attr) = self.info_attributes.take() { - let added = self - .stack - .add_patterns_file(info_attr, true, None, buf, &mut self.collection)?; + let added = self.stack.add_patterns_file( + info_attr, + true, + None, + buf, + &mut self.collection, + true, /* read macros */ + )?; stats.pattern_files += usize::from(added); stats.tried_pattern_files += 1; } diff --git a/gix-worktree/src/cache/state/ignore.rs b/gix-worktree/src/cache/state/ignore.rs index 5ff4ccd42b5..97b055677a7 100644 --- a/gix-worktree/src/cache/state/ignore.rs +++ b/gix-worktree/src/cache/state/ignore.rs @@ -74,7 +74,7 @@ impl Ignore { relative_path: &BStr, is_dir: Option, case: Case, - ) -> Option> { + ) -> Option> { let groups = self.match_groups(); let mut dir_match = None; if let Some((source, mapping)) = self @@ -90,7 +90,6 @@ impl Ignore { { let match_ = gix_ignore::search::Match { pattern: &mapping.pattern, - value: &mapping.value, sequence_number: mapping.sequence_number, source, }; diff --git a/src/plumbing/main.rs b/src/plumbing/main.rs index 6f8fc29c990..c6af57c7d61 100644 --- a/src/plumbing/main.rs +++ b/src/plumbing/main.rs @@ -859,6 +859,33 @@ pub fn main() -> Result<()> { ) }, ), + attributes::Subcommands::ValidateBaseline { statistics, no_ignore } => prepare_and_run( + "attributes-validate-baseline", + auto_verbose, + progress, + progress_keep_open, + None, + move |progress, out, err| { + use gix::bstr::ByteSlice; + core::repository::attributes::validate_baseline( + repository(Mode::StrictWithGitInstallConfig)?, + stdin_or_bail().ok().map(|stdin| { + stdin + .byte_lines() + .filter_map(Result::ok) + .filter_map(|line| gix::path::Spec::from_bytes(line.as_bstr())) + }), + progress, + out, + err, + core::repository::attributes::validate_baseline::Options { + format, + statistics, + ignore: !no_ignore, + }, + ) + }, + ), }, Subcommands::Exclude(cmd) => match cmd { exclude::Subcommands::Query { diff --git a/src/plumbing/options/mod.rs b/src/plumbing/options/mod.rs index 69f818db54b..0e3b728d8fa 100644 --- a/src/plumbing/options/mod.rs +++ b/src/plumbing/options/mod.rs @@ -448,6 +448,16 @@ pub mod attributes { #[derive(Debug, clap::Subcommand)] pub enum Subcommands { + /// Run `git check-attr` and `git check-ignore` on all files of the index or all files passed via stdin and validate that + /// we get the same outcome when computing attributes. + ValidateBaseline { + /// Print various statistics to stderr + #[clap(long, short = 's')] + statistics: bool, + /// Don't validated excludes as obtaining them with `check-ignore` can be very slow. + #[clap(long)] + no_ignore: bool, + }, /// List all attributes of the given path-specs and display the result similar to `git check-attr`. Query { /// Print various statistics to stderr diff --git a/tests/journey/gix.sh b/tests/journey/gix.sh index 196ac442b2a..44335d796f5 100644 --- a/tests/journey/gix.sh +++ b/tests/journey/gix.sh @@ -30,7 +30,7 @@ title "gix-tempfile crate" ) ) -title "gix crate" +title '`gix` crate' (when "testing 'gix'" snapshot="$snapshot/gix" cd gix @@ -158,6 +158,15 @@ title "gix (with repository)" ) ) +title "gix attributes" +(with "gix attributes" + (with "the 'validate-baseline' sub-command" + it "passes when operating on all of our files" && { + expect_run_sh $SUCCESSFULLY "find . -type f | sed 's|^./||' | $exe_plumbing --no-verbose attributes validate-baseline" + } + ) +) + (with "gix free" snapshot="$snapshot/no-repo" title "gix free pack"