use std::collections::BTreeMap;
use filetime::FileTime;
use gix_hash::ObjectId;
use gix_index::entry::stat;
use crate::tree::{directory_of, Blob};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Verdict {
Clone,
AskGit,
Reject,
}
fn stat_options() -> stat::Options {
stat::Options::default()
}
fn entry_is_untrustworthy(flags: gix_index::entry::Flags) -> bool {
use gix_index::entry::Flags;
flags.intersects(Flags::ASSUME_VALID | Flags::SKIP_WORKTREE | Flags::INTENT_TO_ADD)
}
pub fn mode_is_clonable(mode: u32) -> bool {
matches!(mode, 0o100644 | 0o100755 | 0o120000)
}
pub fn entry_can_stand_in(target: &Blob, entry: &gix_index::Entry) -> bool {
mode_is_clonable(target.mode)
&& entry.stage() == gix_index::entry::Stage::Unconflicted
&& !entry_is_untrustworthy(entry.flags)
&& entry.mode.bits() == target.mode
&& entry.id == target.oid
}
pub fn stat_verdict(
entry: &gix_index::Entry,
metadata: &gix_index::fs::Metadata,
index_timestamp: FileTime,
) -> Verdict {
if metadata.is_dir() {
return Verdict::Reject;
}
let Ok(on_disk) = gix_index::entry::Stat::from_fs(metadata) else {
return Verdict::Reject;
};
if !on_disk.matches(&entry.stat, stat_options()) {
return Verdict::Reject;
}
if entry.stat.is_racy(index_timestamp, stat_options()) {
return Verdict::AskGit;
}
Verdict::Clone
}
pub fn poisoned_prefixes(
target_attributes: &BTreeMap<Vec<u8>, ObjectId>,
source_attributes: &BTreeMap<Vec<u8>, ObjectId>,
dirty_attributes: &[Vec<u8>],
) -> Vec<Vec<u8>> {
let mut poisoned: Vec<Vec<u8>> = Vec::new();
let mut note = |path: &[u8]| poisoned.push(directory_of(path).to_vec());
for (path, oid) in target_attributes {
if source_attributes.get(path) != Some(oid) {
note(path);
}
}
for path in source_attributes.keys() {
if !target_attributes.contains_key(path) {
note(path);
}
}
for path in dirty_attributes {
note(path);
}
poisoned.sort();
poisoned.dedup();
poisoned
}
pub fn is_poisoned(path: &[u8], poisoned: &[Vec<u8>]) -> bool {
poisoned
.iter()
.any(|prefix| prefix.is_empty() || path.starts_with(prefix))
}
#[cfg(test)]
mod tests {
use super::*;
fn oid(byte: u8) -> ObjectId {
ObjectId::from_hex(format!("{:02x}", byte).repeat(20).as_bytes()).unwrap()
}
fn attributes(entries: &[(&str, u8)]) -> BTreeMap<Vec<u8>, ObjectId> {
entries
.iter()
.map(|(path, byte)| (path.as_bytes().to_vec(), oid(*byte)))
.collect()
}
#[test]
fn only_regular_files_and_symlinks_are_clonable() {
assert!(mode_is_clonable(0o100644));
assert!(mode_is_clonable(0o100755));
assert!(mode_is_clonable(0o120000));
assert!(!mode_is_clonable(0o160000));
assert!(!mode_is_clonable(0o040000));
}
#[test]
fn identical_attributes_disqualify_nothing() {
let both = attributes(&[(".gitattributes", 1), ("src/.gitattributes", 2)]);
assert!(poisoned_prefixes(&both, &both, &[]).is_empty());
}
#[test]
fn a_changed_attributes_file_disqualifies_its_directory() {
let target = attributes(&[("src/.gitattributes", 1)]);
let source = attributes(&[("src/.gitattributes", 2)]);
let poisoned = poisoned_prefixes(&target, &source, &[]);
assert_eq!(poisoned, vec![b"src/".to_vec()]);
assert!(is_poisoned(b"src/a.txt", &poisoned));
assert!(!is_poisoned(b"doc/a.txt", &poisoned));
}
#[test]
fn a_root_attributes_file_disqualifies_everything() {
let target = attributes(&[(".gitattributes", 1)]);
let poisoned = poisoned_prefixes(&target, &BTreeMap::new(), &[]);
assert!(is_poisoned(b"anything", &poisoned));
}
#[test]
fn an_attributes_file_only_the_source_has_disqualifies_its_directory() {
let source = attributes(&[("src/.gitattributes", 1)]);
let poisoned = poisoned_prefixes(&BTreeMap::new(), &source, &[]);
assert!(is_poisoned(b"src/a.txt", &poisoned));
}
#[test]
fn a_modified_attributes_file_disqualifies_its_directory() {
let both = attributes(&[("src/.gitattributes", 1)]);
let poisoned = poisoned_prefixes(&both, &both, &[b"src/.gitattributes".to_vec()]);
assert!(is_poisoned(b"src/a.txt", &poisoned));
}
}