use super::worktree_copy_error::{WorktreeCopyError, failure_with_cause, raw_failure};
use super::worktree_copy_manifest::{
EntryKind, WorktreeCopyEntry, collect_entry_manifest, retained_mode,
};
use super::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(|error| return raw_failure(&error))?;
let mut right: std::fs::File =
std::fs::File::open(right_path).map_err(|error| return raw_failure(&error))?;
let left_size: u64 = left
.metadata()
.map_err(|error| return raw_failure(&error))?
.len();
let right_size: u64 = right
.metadata()
.map_err(|error| return raw_failure(&error))?
.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 = usize::try_from(remaining).map_or(COMPARE_BUFFER_BYTES, |size| {
return size.min(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(|error| return raw_failure(&error));
}
pub fn entry_matches(
expected_root: &Path,
actual_root: &Path,
entry: &WorktreeCopyEntry,
) -> 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);
}
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())?);
}
}
}
pub fn assert_final_source_equivalence(
source_root: &Path,
stage_root: &Path,
selected_roots: &[String],
excluded_source_roots: &[PathBuf],
staged_entries: &[WorktreeCopyEntry],
) -> Result<(), WorktreeCopyError> {
let final_source = collect_entry_manifest(source_root, selected_roots, excluded_source_roots)?;
let final_stage = collect_entry_manifest(stage_root, selected_roots, &[])?;
if final_source.len() != staged_entries.len() || final_stage.len() != staged_entries.len() {
return Err(failure_with_cause(
"cli-git: 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;
if !same_shape || !entry_matches(stage_root, source_root, staged)? {
let message: String = format!(
"cli-git: 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;