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<Self::Item> { 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<u8>, ) -> Result<Option<gix_object::Data<'a>>, 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::<Vec<_>>() ); } } } Ok(())}
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<Self::Item> {
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<u8>,
) -> Result<Option<gix_object::Data<'a>>, 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::<Vec<_>>()
);
}
}
}
Ok(())
}