use super::*;
use crate::index_cache_tree::CacheTree;
use crate::index_fsmonitor::Fsmonitor;
use crate::index_stat::StatData;
use crate::index_untracked::{Untracked, UntrackedDir};
const PLAIN: IndexSettings = IndexSettings { monitor: false, untracked: UntrackedMode::Keep, shared_repository: 0 };
const MONITORED: IndexSettings = IndexSettings { monitor: true, untracked: UntrackedMode::Keep, shared_repository: 0 };
fn held(path: &str, id: u8, valid: bool) -> FullEntry {
return FullEntry {
stat: StatData { mtime_sec: 5, size: 3, ..StatData::default() },
mode: 0o100_644,
id: vec![id; 20],
flags: 0,
extended: 0,
path: path.as_bytes().to_vec(),
monitor_valid: valid,
};
}
fn wanted(path: &str, id: u8) -> FlatEntry {
return FlatEntry { path: path.as_bytes().to_vec(), mode: 0o100_644, id: vec![id; 20] };
}
fn index_of(entries: Vec<FullEntry>, monitored: bool) -> FullIndex {
let directory = |name: &str| -> UntrackedDir {
return UntrackedDir { name: name.as_bytes().to_vec(), recurse: true, valid: true, check_only: false, stat: StatData::default(), exclude_id: vec![0; 20], untracked: vec![b"u".to_vec()], dirs: Vec::new() };
};
let mut top: UntrackedDir = directory("");
top.dirs.push(directory("d"));
return FullIndex {
version: 2,
entries,
cache_tree: Some(CacheTree { entry_count: 2, id: vec![9; 20], subtrees: vec![(b"d".to_vec(), CacheTree { entry_count: 1, id: vec![8; 20], subtrees: Vec::new() })] }),
resolve_undo: None,
untracked: Some(Untracked {
ident: Vec::new(),
info_exclude: (StatData::default(), vec![0; 20]),
excludes_file: (StatData::default(), vec![0; 20]),
dir_flags: 0,
exclude_per_dir: b".gitignore".to_vec(),
root: Some(top),
}),
fsmonitor: monitored.then(|| { return Fsmonitor { token: b"t".to_vec(), dirty: Vec::new() }; }),
};
}
#[test]
fn a_reset_entry_has_no_stat_data_and_the_others_keep_theirs() {
let mut index: FullIndex = index_of(vec![held("a", 1, true), held("d/x", 2, true)], false);
reset_paths(&mut index, &[b"a".to_vec()], &[wanted("a", 7)], PLAIN, u32::MAX).expect("reset");
assert_eq!(index.entries[0].stat, StatData::default());
assert_eq!(index.entries[0].id, vec![7; 20]);
assert_eq!(index.entries[1].stat.mtime_sec, 5);
assert!(index.fsmonitor.is_none(), "without a monitor the extension is dropped");
}
#[test]
fn an_equal_entry_changes_nothing() {
let mut index: FullIndex = index_of(vec![held("a", 1, true)], false);
let before: FullIndex = index.clone();
reset_paths(&mut index, &[b"a".to_vec()], &[wanted("a", 1)], PLAIN, u32::MAX).expect("reset");
assert_eq!(index, before);
}
#[test]
fn with_the_monitor_on_new_entries_are_dirty_and_replacing_clears_the_untracked_cache() {
let mut link: FullEntry = held("g", 4, true);
link.mode = 0o160_000;
let mut index: FullIndex = index_of(vec![held("a", 1, true), held("d/x", 2, true), held("d/y", 3, true), link], true);
index.fsmonitor = Some(Fsmonitor { token: b"t".to_vec(), dirty: vec![2] });
reset_paths(&mut index, &[b"d/x".to_vec(), b"d/y".to_vec(), b"e".to_vec()], &[wanted("d/x", 7), wanted("e", 6)], MONITORED, u32::MAX).expect("reset");
let listed: Vec<(&[u8], bool)> = index.entries.iter().map(|entry| { return (entry.path.as_slice(), entry.monitor_valid); }).collect();
assert_eq!(listed, vec![(&b"a"[..], true), (&b"d/x"[..], false), (&b"e"[..], false), (&b"g"[..], false)]);
let top = index.untracked.as_ref().and_then(|cache| { return cache.root.as_ref(); }).expect("kept");
assert!(!top.dirs[0].valid, "replacing d/x cleared the listing of d");
assert_eq!(index.cache_tree.as_ref().map(|tree| { return tree.entry_count; }), Some(-1));
}
#[test]
fn without_the_monitor_only_adding_and_removing_clear_the_untracked_cache() {
let mut index: FullIndex = index_of(vec![held("d/x", 2, true)], false);
reset_paths(&mut index, &[b"d/x".to_vec()], &[wanted("d/x", 7)], PLAIN, u32::MAX).expect("reset");
assert!(index.untracked.as_ref().and_then(|cache| { return cache.root.as_ref(); }).expect("kept").dirs[0].valid);
reset_paths(&mut index, &[b"d/n".to_vec()], &[wanted("d/n", 7)], PLAIN, u32::MAX).expect("reset");
assert!(!index.untracked.as_ref().and_then(|cache| { return cache.root.as_ref(); }).expect("kept").dirs[0].valid);
}
#[test]
fn forms_that_depend_on_more_than_the_tree_and_the_index_are_declined() {
let names: Vec<Vec<u8>> = vec![b"a".to_vec()];
let tree: Vec<FlatEntry> = vec![wanted("a", 7)];
assert!(reset_paths(&mut index_of(vec![held("a", 1, true)], false), &names, &tree, MONITORED, u32::MAX).is_none(), "monitor without extension");
let mut long: FullIndex = index_of(vec![held("a", 1, true)], true);
long.fsmonitor = Some(Fsmonitor { token: Vec::new(), dirty: vec![3] });
assert!(reset_paths(&mut long, &names, &tree, MONITORED, u32::MAX).is_none(), "bitmap past the entries");
let mut absent: FullIndex = index_of(vec![held("a", 1, true)], false);
absent.untracked = None;
let adding: IndexSettings = IndexSettings { monitor: false, untracked: UntrackedMode::Write, shared_repository: 0 };
assert!(reset_paths(&mut absent, &names, &tree, adding, u32::MAX).is_none(), "untracked cache to add");
let mut directory: FullIndex = index_of(vec![held("d/x", 1, true)], false);
assert!(reset_paths(&mut directory, &[b"d".to_vec()], &[wanted("d", 7)], PLAIN, u32::MAX).is_none(), "file over directory");
for bad in [".git/hooks/x", "a/../b", "a//b", ".GIT", "x/.git ", "git~1"] {
let mut refused: FullIndex = index_of(vec![held("z", 1, true)], false);
assert!(reset_paths(&mut refused, &[bad.as_bytes().to_vec()], &[wanted(bad, 7)], PLAIN, u32::MAX).is_none(), "{bad}");
}
let mut racy: FullIndex = index_of(vec![held("a", 1, true), held("b", 2, true)], false);
assert!(reset_paths(&mut racy, &names, &tree, PLAIN, 5).is_none(), "an entry recorded at the index file's second");
assert!(reset_paths(&mut racy, &names, &tree, PLAIN, 6).is_some(), "one second later it is not racy");
}
#[test]
fn modes_are_made_as_create_ce_mode_makes_them() {
assert_eq!(index_mode(0o100_755), 0o100_755);
assert_eq!(index_mode(0o100_664), 0o100_644);
assert_eq!(index_mode(0o100_775), 0o100_755);
assert_eq!(index_mode(0o120_000), 0o120_000);
assert_eq!(index_mode(0o160_000), 0o160_000);
assert_eq!(index_mode(0o040_000), 0o160_000);
}
#[test]
fn names_that_git_refuses_under_the_protection_settings_are_declined() {
let link: u32 = 0o120_000;
let refused: [(&str, u32); 14] = [
(".gitmodules::$DATA", link),
(".gitmodules . .", link),
(".gitmodules ", link),
("GITMOD~1", link),
("gitmod~1 ", link),
(".git::$INDEX_ALLOCATION", 0o100_644),
("dir/.git ", 0o100_644),
("dir\\.git\\config", 0o100_644),
("a\\b", 0o100_644),
("GIT~1", 0o100_644),
("dir/.g\u{200c}it", 0o100_644),
(".git\u{200d}", 0o100_644),
("x/.\u{206a}git/y", 0o100_644),
(".GitModules", link),
];
for (name, mode) in refused {
let mut index: FullIndex = index_of(vec![held("z", 1, true)], false);
let entry: FlatEntry = FlatEntry { path: name.as_bytes().to_vec(), mode, id: vec![7; 20] };
assert!(reset_paths(&mut index, &[name.as_bytes().to_vec()], &[entry], PLAIN, u32::MAX).is_none(), "{name:?}");
}
}
#[test]
fn ordinary_names_are_taken() {
for name in [".gitignore", ".gitattributes", ".github/workflows/ci.yml", "a.b", "name with space", "ünïcode/файл", ".gitmodules", "dir/.config", "gitmodules", "git-hooks/pre", "a:b"] {
let mut index: FullIndex = index_of(vec![held("z", 1, true)], false);
let entry: FlatEntry = FlatEntry { path: name.as_bytes().to_vec(), mode: 0o100_644, id: vec![7; 20] };
let done: Option<()> = reset_paths(&mut index, &[name.as_bytes().to_vec()], &[entry], PLAIN, u32::MAX);
assert_eq!(done.is_some(), name != "a:b", "{name:?}");
}
}
#[test]
fn the_dotgitmodules_predicate_follows_git() {
for name in [".gitmodules", ".GITMODULES", ".gitmodules . ", ".gitmodules:stream", "gitmod~1", "GITMOD~4 ", "GI7EBA~1", "gi7eba~9", "GI7EBA~1 ."] {
assert!(is_ntfs_dotgitmodules(name.as_bytes()), "{name:?}");
}
for name in ["", ".gitmodule", ".gitmodulesx", "gitmod~5", "gitmod~", "gitmodules", "GI7EBA~0", "GI7EBA~1x", "gi7eba", "GI7EBB~1", "xi7eba~1", ".gitignore", "GI250A~1", "GI7EBA~12:x"] {
assert!(!is_ntfs_dotgitmodules(name.as_bytes()), "{name:?}");
}
}