use crate::side_by_side_threads::run_beside_caller;
use crate::worktree_copy_error::{WorktreeCopyError, failure_with_cause, raw_failure, raw_failure_of};
use crate::worktree_copy_manifest::{
EntryKind, FileStamp, WorktreeCopyEntry, collect_entry_manifest, collect_pruned_manifest, retained_mode, stamp_of,
};
use crate::worktree_copy_prune::Prune;
use crate::worktree_copy_selection::filesystem_path;
use crate::json_record::quote;
use std::io::Read;
use std::path::{Path, PathBuf};
const COMPARE_BUFFER_BYTES: usize = 64 * 1_024;
pub fn lstat_or_absent(path: &Path) -> Result<Option<std::fs::Metadata>, WorktreeCopyError> {
match std::fs::symlink_metadata(path) {
Ok(metadata) => return Ok(Some(metadata)),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(error) => return Err(raw_failure(&error)),
}
}
fn regular_file_bytes_equal(
left_path: &Path,
right_path: &Path,
) -> Result<bool, WorktreeCopyError> {
let mut left: std::fs::File =
std::fs::File::open(left_path).map_err(raw_failure_of)?;
let mut right: std::fs::File =
std::fs::File::open(right_path).map_err(raw_failure_of)?;
let left_size: u64 = left
.metadata()
.map_err(raw_failure_of)?
.len();
let right_size: u64 = right
.metadata()
.map_err(raw_failure_of)?
.len();
if left_size != right_size {
return Ok(false);
}
let mut left_buffer: Vec<u8> = vec![0; COMPARE_BUFFER_BYTES];
let mut right_buffer: Vec<u8> = vec![0; COMPARE_BUFFER_BYTES];
let mut remaining: u64 = left_size;
while remaining > 0 {
let length: usize = match usize::try_from(remaining) {
Ok(size) => size.min(COMPARE_BUFFER_BYTES),
Err(_too_large) => COMPARE_BUFFER_BYTES,
};
let left_chunk: &mut [u8] = &mut left_buffer[..length];
let right_chunk: &mut [u8] = &mut right_buffer[..length];
match (left.read_exact(left_chunk), right.read_exact(right_chunk)) {
(Ok(()), Ok(())) => {}
(Err(error), _) | (_, Err(error))
if error.kind() == std::io::ErrorKind::UnexpectedEof =>
{
return Ok(false);
}
(Err(error), _) | (_, Err(error)) => {
return Err(raw_failure(&error));
}
}
if left_chunk != right_chunk {
return Ok(false);
}
remaining -= u64::try_from(length).unwrap_or(0);
}
return Ok(true);
}
fn link_target(path: &Path) -> Result<PathBuf, WorktreeCopyError> {
return std::fs::read_link(path).map_err(raw_failure_of);
}
pub fn entry_matches(
expected_root: &Path,
actual_root: &Path,
entry: &WorktreeCopyEntry,
) -> Result<bool, WorktreeCopyError> {
return entry_matches_stamped(expected_root, actual_root, entry, None);
}
pub fn entry_matches_stamped(
expected_root: &Path,
actual_root: &Path,
entry: &WorktreeCopyEntry,
stamp: Option<&FileStamp>,
) -> Result<bool, WorktreeCopyError> {
let expected_path: PathBuf = filesystem_path(expected_root, entry.relative_path.as_str())?;
let actual_path: PathBuf = filesystem_path(actual_root, entry.relative_path.as_str())?;
let Some(actual) = lstat_or_absent(actual_path.as_path())? else {
return Ok(false);
};
if retained_mode(&actual) != entry.mode {
return Ok(false);
}
match entry.kind {
EntryKind::Directory => return Ok(actual.is_dir()),
EntryKind::File => {
if !actual.is_file() {
return Ok(false);
}
if let Some(recorded) = stamp {
return Ok(stamp_of(&actual).as_ref() == Some(recorded));
}
return regular_file_bytes_equal(expected_path.as_path(), actual_path.as_path());
}
EntryKind::Symlink => {
if !actual.file_type().is_symlink() {
return Ok(false);
}
return Ok(link_target(expected_path.as_path())? == link_target(actual_path.as_path())?);
}
}
}
fn walk_source_pruned(
(source_root, selected_roots, excluded_source_roots, prune): (&Path, &[String], &[PathBuf], Prune),
) -> Result<Vec<WorktreeCopyEntry>, WorktreeCopyError> {
let mut unreadable: Vec<String> = Vec::new();
return collect_pruned_manifest(source_root, selected_roots, excluded_source_roots, prune, &mut unreadable);
}
fn walk_stage((stage_root, selected_roots): (&Path, &[String])) -> Result<Vec<WorktreeCopyEntry>, WorktreeCopyError> {
return collect_entry_manifest(stage_root, selected_roots, &[]);
}
pub fn assert_final_source_equivalence(
source_root: &Path,
stage_root: &Path,
selected_roots: &[String],
excluded_source_roots: &[PathBuf],
staged_entries: &[WorktreeCopyEntry],
) -> Result<(), WorktreeCopyError> {
return assert_final_source_equivalence_pruned(
source_root,
stage_root,
selected_roots,
excluded_source_roots,
staged_entries,
&FinalCheck { prune: Prune::Never, stamps: &[] },
);
}
pub struct FinalCheck<'a> {
pub prune: Prune,
pub stamps: &'a [Option<FileStamp>],
}
pub fn assert_final_source_equivalence_pruned(
source_root: &Path,
stage_root: &Path,
selected_roots: &[String],
excluded_source_roots: &[PathBuf],
staged_entries: &[WorktreeCopyEntry],
check: &FinalCheck<'_>,
) -> Result<(), WorktreeCopyError> {
let (source_joined, stage_walked): (std::thread::Result<Result<Vec<WorktreeCopyEntry>, WorktreeCopyError>>, Result<Vec<WorktreeCopyEntry>, WorktreeCopyError>) =
run_beside_caller(
((source_root, selected_roots, excluded_source_roots, check.prune), walk_source_pruned),
((stage_root, selected_roots), walk_stage),
);
let source_walked: Result<Vec<WorktreeCopyEntry>, WorktreeCopyError> = match source_joined {
Ok(walked) => walked,
Err(_panic) => Err(raw_failure(&std::io::Error::other("a manifest walk panicked"))),
};
let final_source: Vec<WorktreeCopyEntry> = source_walked?;
let final_stage: Vec<WorktreeCopyEntry> = stage_walked?;
if final_source.len() != staged_entries.len() || final_stage.len() != staged_entries.len() {
return Err(failure_with_cause(
"monochromatic-git-policy-cli: ignored source paths changed while snapshot was staged.",
None,
));
}
for (index, staged) in staged_entries.iter().enumerate() {
let source_entry: &WorktreeCopyEntry = &final_source[index];
let stage_entry: &WorktreeCopyEntry = &final_stage[index];
let same_shape: bool = source_entry.kind == staged.kind
&& source_entry.mode == staged.mode
&& source_entry.relative_path == staged.relative_path
&& stage_entry.kind == staged.kind
&& stage_entry.mode == staged.mode
&& stage_entry.relative_path == staged.relative_path;
let stamp: Option<&FileStamp> = check.stamps.get(index).and_then(Option::as_ref);
if !same_shape || !entry_matches_stamped(stage_root, source_root, staged, stamp)? {
let message: String = format!(
"monochromatic-git-policy-cli: ignored source entry changed while copying: {}.",
quote(staged.relative_path.as_str())
);
return Err(failure_with_cause(message.as_str(), None));
}
}
return Ok(());
}
#[cfg(test)]
#[path = "worktree_copy_compare_tests.rs"]
mod tests;