use crate::fs_beneath::{entry_metadata_at, open_directory_beneath, open_root, plain_components};
use crate::repo_read_index::IndexEntry;
use crate::repo_read_layout::ObjectFormat;
use crate::subvolume_index::StatFields;
use crate::subvolume_index_files::fields_of;
use crate::worktree_clean_content::{ContentJudge, ConversionEnvironment, Place};
use crate::worktree_clean_stat_verdicts::{CleanRefusal, CleanRefusalKind, StatEntry, StatVerdict, changed_file, metadata_verdict, stat_finding};
use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt;
use std::path::Path;
fn cache_misses(cached: &Option<(Vec<std::ffi::OsString>, std::fs::File)>, parents: &[std::ffi::OsString]) -> bool {
return match cached {
Some((known, _directory)) => known.as_slice() != parents,
None => true,
};
}
pub fn tree_differs_from_index(root: &Path, entries: &[StatEntry], index_seconds: u32) -> Option<String> {
return match open_root(root) {
Ok(held) => tree_differs_from_index_beneath(&held, entries, index_seconds),
Err(error) => Some(format!("{} cannot be opened without following a link ({error})", root.display())),
};
}
pub fn tree_refusal(root: &Path, entries: &[StatEntry], index_seconds: u32) -> Option<CleanRefusal> {
return match open_root(root) {
Ok(held) => tree_refusal_beneath(&held, entries, index_seconds),
Err(error) => Some(CleanRefusal::unreadable(format!("{} cannot be opened without following a link ({error})", root.display()))),
};
}
pub struct ContentTarget<'a> {
pub root: &'a Path,
pub git_dir: &'a Path,
pub common_dir: &'a Path,
pub environment: &'a ConversionEnvironment,
pub format: ObjectFormat,
}
pub fn tree_differs_from_index_by_content(target: &ContentTarget<'_>, entries: &[StatEntry], indexed: &[IndexEntry], index_seconds: u32) -> Option<String> {
if entries.len() != indexed.len() {
return Some(String::from("the index entries and their stat data are not the same files"));
}
let held: std::fs::File = match open_root(target.root) {
Ok(opened) => opened,
Err(error) => return Some(format!("{} cannot be opened without following a link ({error})", target.root.display())),
};
let mut judge: ContentJudge<'_> = ContentJudge::new(&held, target.git_dir, target.common_dir, target.environment, target.format);
let mut cached: Option<(Vec<std::ffi::OsString>, std::fs::File)> = None;
for (entry, committed) in entries.iter().zip(indexed) {
if entry.path != committed.path {
return Some(String::from("the index entries and their stat data are not the same files"));
}
let shown: String = String::from_utf8_lossy(entry.path.as_slice()).into_owned();
let Ok(names) = plain_components(Path::new(OsStr::from_bytes(entry.path.as_slice()))) else { return Some(format!("{shown} is not a plain path")) };
let Some((name, parents)) = names.split_last() else { return Some(format!("{shown} is not a plain path")) };
if cache_misses(&cached, parents) {
let Ok(directory) = open_directory_beneath(&held, parents) else { return Some(format!("{shown} is missing from the worktree or reached through a link")) };
cached = Some((parents.to_vec(), directory));
}
let Some((_known, directory)) = cached.as_ref() else { return Some(format!("{shown} is missing from the worktree")) };
let Ok(metadata) = entry_metadata_at(directory, name.as_os_str()) else { return Some(format!("{shown} is missing from the worktree")) };
match metadata_verdict(entry, &metadata, index_seconds) {
StatVerdict::Agrees => {}
StatVerdict::Differs(reason) => return Some(reason),
StatVerdict::Unproven(_reason) => {
let place: Place<'_> = Place { directory, parents, name: name.as_os_str() };
if let Err(reason) = judge.agrees(&place, entry, committed.id.as_slice(), index_seconds) {
return Some(reason);
}
}
}
}
return None;
}
pub fn tree_differs_from_index_beneath(root: &std::fs::File, entries: &[StatEntry], index_seconds: u32) -> Option<String> {
return match tree_refusal_beneath(root, entries, index_seconds) {
Some(refusal) => Some(refusal.reason),
None => None,
};
}
fn tree_refusal_beneath(root: &std::fs::File, entries: &[StatEntry], index_seconds: u32) -> Option<CleanRefusal> {
let mut cached: Option<(Vec<std::ffi::OsString>, std::fs::File)> = None;
for entry in entries {
let shown: String = String::from_utf8_lossy(entry.path.as_slice()).into_owned();
let Ok(names) = plain_components(Path::new(OsStr::from_bytes(entry.path.as_slice()))) else { return Some(CleanRefusal::unreadable(format!("{shown} is not a plain path"))) };
let Some((name, parents)) = names.split_last() else { return Some(CleanRefusal::unreadable(format!("{shown} is not a plain path"))) };
if cache_misses(&cached, parents) {
let Ok(directory) = open_directory_beneath(root, parents) else { return changed_file(format!("{shown} is missing from the worktree or reached through a link")) };
cached = Some((parents.to_vec(), directory));
}
let Some((_held, directory)) = cached.as_ref() else { return changed_file(format!("{shown} is missing from the worktree")) };
let Ok(metadata) = entry_metadata_at(directory, name.as_os_str()) else { return changed_file(format!("{shown} is missing from the worktree")) };
if let Some(refusal) = stat_finding(entry, &metadata, index_seconds) {
return Some(refusal);
}
}
return None;
}
pub struct StatCursor<'a> {
root: &'a std::fs::File,
held: Option<(Vec<std::ffi::OsString>, std::fs::File)>,
}
impl<'a> StatCursor<'a> {
pub fn new(root: &'a std::fs::File) -> Self {
return Self { root, held: None };
}
fn metadata(&mut self, path: &[u8]) -> Result<std::fs::Metadata, String> {
let shown: String = String::from_utf8_lossy(path).into_owned();
let Ok(names) = plain_components(Path::new(OsStr::from_bytes(path))) else { return Err(format!("{shown} is not a plain path")) };
let Some((name, parents)) = names.split_last() else { return Err(format!("{shown} is not a plain path")) };
if cache_misses(&self.held, parents) {
let Ok(directory) = open_directory_beneath(self.root, parents) else { return Err(format!("{shown} is missing from the worktree or reached through a link")) };
self.held = Some((parents.to_vec(), directory));
}
let Some((_known, directory)) = self.held.as_ref() else { return Err(format!("{shown} is missing from the worktree")) };
return match entry_metadata_at(directory, name.as_os_str()) {
Ok(found) => Ok(found),
Err(_missing) => Err(format!("{shown} is missing from the worktree")),
};
}
pub fn fields(&mut self, path: &[u8]) -> Option<StatFields> {
return match self.metadata(path) {
Ok(found) => Some(fields_of(&found)),
Err(_refused) => None,
};
}
}
pub fn tree_fields_beneath(root: &std::fs::File, entries: &[StatEntry], index_seconds: u32) -> Result<Vec<StatFields>, CleanRefusal> {
let mut cursor: StatCursor<'_> = StatCursor::new(root);
let mut fields: Vec<StatFields> = Vec::with_capacity(entries.len());
for entry in entries {
let metadata: std::fs::Metadata = match cursor.metadata(entry.path.as_slice()) {
Ok(found) => found,
Err(reason) if reason.ends_with("is not a plain path") => return Err(CleanRefusal::unreadable(reason)),
Err(reason) => return Err(CleanRefusal::new(CleanRefusalKind::FileChanged, reason)),
};
if let Some(refusal) = stat_finding(entry, &metadata, index_seconds) {
return Err(refusal);
}
fields.push(fields_of(&metadata));
}
return Ok(fields);
}
pub fn stat_fields_beneath(root: &std::fs::File, path: &[u8]) -> Option<StatFields> {
return StatCursor::new(root).fields(path);
}