use std::fs::Metadata; use bstr::{BStr, ByteSlice}; use gix_fs::stack::ToNormalPathComponents; use gix_index::entry::Mode; use gix_worktree::{Stack, stack::state::ignore::Source}; use crate::{hex_to_id, stack::probe_case}; struct IgnoreExpectations<'a> { lines: bstr::Lines<'a>, } impl<'a> Iterator for IgnoreExpectations<'a> { type Item = (&'a BStr, Option<(&'a BStr, usize, &'a BStr)>); fn next(&mut self) -> Option { let line = self.lines.next()?; let (left, value) = line.split_at(line.find_byte(b'\t').unwrap()); let value = value[1..].as_bstr(); let source_and_line = if left == b"::" { None } else { let mut tokens = left.split(|b| *b == b':'); let source = tokens.next().unwrap().as_bstr(); let line_number: usize = tokens.next().unwrap().to_str_lossy().parse().ok().unwrap(); let pattern = tokens.next().unwrap().as_bstr(); Some((source, line_number, pattern)) }; Some((value, source_and_line)) } } #[test] fn exclude_by_dir_is_handled_just_like_git() { let dir = crate::scripted_fixture_read_only("make_special_exclude_case.sh").unwrap(); let git_dir = dir.join(".git"); let mut buf = Vec::new(); let case = gix_glob::pattern::Case::Sensitive; let state = gix_worktree::stack::State::for_add( Default::default(), gix_worktree::stack::state::Ignore::new( Default::default(), gix_ignore::Search::from_git_dir(&git_dir, None, &mut buf, Default::default()).unwrap(), None, Source::WorktreeThenIdMappingIfNotSkipped, Default::default(), ), ); let mut cache = Stack::new(&dir, state, case, buf, Default::default()); let baseline = std::fs::read(git_dir.parent().unwrap().join("git-check-ignore.baseline")).unwrap(); let expectations = IgnoreExpectations { lines: baseline.lines(), }; struct FindError; impl gix_object::Find for FindError { fn try_find<'a>( &self, _id: &gix_hash::oid, _buffer: &'a mut Vec, ) -> Result>, gix_object::find::Error> { Err(std::io::Error::other("unreachable").into()) } } for (relative_entry, source_and_line) in expectations { let (source, line, expected_pattern) = source_and_line.expect("every value is matched"); let relative_path = gix_path::from_byte_slice(relative_entry); let is_dir = dir.join(relative_path).metadata().ok().map(metadata_to_mode); let platform = cache.at_entry(relative_entry, is_dir, &FindError).unwrap(); let match_ = platform.matching_exclude_pattern().expect("match all values"); let _is_excluded = platform.is_excluded(); assert_eq!( match_.pattern.to_string(), expected_pattern, "we perfectly agree with git" ); assert_eq!( expected_pattern, "tld/", "each entry matches on the main directory exclude, ignoring negations entirely" ); // TODO: adjust baseline to also include precious files. assert_eq!( match_.kind, gix_ignore::Kind::Expendable, "for now all patterns are expendable until precious files are supported by git" ); assert_eq!(line, 2); assert_eq!(source, ".gitignore"); } } fn metadata_to_mode(meta: Metadata) -> Mode { if meta.is_dir() { gix_index::entry::Mode::DIR } else { gix_index::entry::Mode::FILE } } #[test] fn check_against_baseline() -> crate::Result { let dir = crate::scripted_fixture_read_only("make_ignore_and_attributes_setup.sh")?; let worktree_dir = dir.join("repo"); let git_dir = worktree_dir.join(".git"); let mut buf = Vec::new(); let user_exclude_path = dir.join("user.exclude"); assert!(user_exclude_path.is_file()); // Due to the way our setup differs from gits dynamic stack (which involves trying to read files from disk // by path) we can only test one case baseline, so we require multiple platforms (or filesystems) to run this. let case = probe_case()?; let mut index = gix_index::File::at(git_dir.join("index"), gix_hash::Kind::Sha1, false, Default::default())?; let odb = gix_odb::at(git_dir.join("objects"))?; let parse_ignore = gix_ignore::search::Ignore::default(); let state = gix_worktree::stack::State::for_add( Default::default(), gix_worktree::stack::state::Ignore::new( gix_ignore::Search::from_overrides(["!force-include"], parse_ignore), gix_ignore::Search::from_git_dir(&git_dir, Some(user_exclude_path), &mut buf, parse_ignore)?, None, Source::WorktreeThenIdMappingIfNotSkipped, parse_ignore, ), ); let paths_storage = index.take_path_backing(); let attribute_files_in_index = state.id_mappings_from_index(&index, &paths_storage, case); assert_eq!( attribute_files_in_index, vec![( "other-dir-with-ignore/.gitignore".into(), hex_to_id("5c7e0ed672d3d31d83a3df61f13cc8f7b22d5bfd") )] ); let mut cache = Stack::new(&worktree_dir, state, case, buf, attribute_files_in_index); let baseline = std::fs::read(git_dir.parent().unwrap().join("git-check-ignore.baseline"))?; let expectations = IgnoreExpectations { lines: baseline.lines(), }; for (relative_entry, source_and_line) in expectations { let relative_path = gix_path::from_byte_slice(relative_entry); let is_dir = worktree_dir.join(relative_path).metadata().ok().map(metadata_to_mode); let platform = cache.at_entry(relative_entry, is_dir, &odb)?; let match_ = platform.matching_exclude_pattern(); let is_excluded = platform.is_excluded(); match (match_, source_and_line) { (None, None) => { assert!(!is_excluded); } (Some(m), Some((source_file, line, pattern))) => { assert_eq!(m.pattern.to_string(), pattern); assert_eq!(m.sequence_number, line); // TODO: adjust baseline to also include precious files. if !m.pattern.is_negative() { assert_eq!( m.kind, platform.excluded_kind().expect("it matches"), "both values agree, no matter which method is used" ); } // Paths read from the index are relative to the repo, and they don't exist locally due tot skip-worktree if m.source.is_some_and(std::path::Path::exists) { assert_eq!( m.source.map(|p| p.canonicalize().unwrap()), Some(worktree_dir.join(source_file.to_str_lossy().as_ref()).canonicalize()?) ); } } (Some(actual), None) if actual.pattern.is_negative() => { // OK: we provide negative patterns that matched on paths if there was no other match, while git doesn't. } (actual, expected) => { panic!( "actual {actual:?} didn't match {expected:?} at '{relative_entry}': {components:?}", components = relative_entry.to_normal_path_components().collect::>() ); } } } Ok(()) }