git_policy_cli/
worktree_copy_compare.rs1use super::worktree_copy_error::{WorktreeCopyError, failure_with_cause, raw_failure};
15use super::worktree_copy_manifest::{
16 EntryKind, WorktreeCopyEntry, collect_entry_manifest, retained_mode,
17};
18use super::worktree_copy_selection::filesystem_path;
19use crate::json_record::quote;
20use std::io::Read;
21use std::path::{Path, PathBuf};
22
23const COMPARE_BUFFER_BYTES: usize = 64 * 1_024;
33
34pub fn lstat_or_absent(path: &Path) -> Result<Option<std::fs::Metadata>, WorktreeCopyError> {
44 match std::fs::symlink_metadata(path) {
46 Ok(metadata) => return Ok(Some(metadata)),
47 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
48 Err(error) => return Err(raw_failure(&error)),
49 }
50}
51
52fn regular_file_bytes_equal(
62 left_path: &Path,
63 right_path: &Path,
64) -> Result<bool, WorktreeCopyError> {
65 let mut left: std::fs::File =
66 std::fs::File::open(left_path).map_err(|error| return raw_failure(&error))?;
67 let mut right: std::fs::File =
68 std::fs::File::open(right_path).map_err(|error| return raw_failure(&error))?;
69 let left_size: u64 = left
70 .metadata()
71 .map_err(|error| return raw_failure(&error))?
72 .len();
73 let right_size: u64 = right
74 .metadata()
75 .map_err(|error| return raw_failure(&error))?
76 .len();
77 if left_size != right_size {
78 return Ok(false);
79 }
80 let mut left_buffer: Vec<u8> = vec![0; COMPARE_BUFFER_BYTES];
81 let mut right_buffer: Vec<u8> = vec![0; COMPARE_BUFFER_BYTES];
82 let mut remaining: u64 = left_size;
83 while remaining > 0 {
84 let length: usize = usize::try_from(remaining).map_or(COMPARE_BUFFER_BYTES, |size| {
85 return size.min(COMPARE_BUFFER_BYTES);
86 });
87 let left_chunk: &mut [u8] = &mut left_buffer[..length];
88 let right_chunk: &mut [u8] = &mut right_buffer[..length];
89 match (left.read_exact(left_chunk), right.read_exact(right_chunk)) {
91 (Ok(()), Ok(())) => {}
92 (Err(error), _) | (_, Err(error))
93 if error.kind() == std::io::ErrorKind::UnexpectedEof =>
94 {
95 return Ok(false);
96 }
97 (Err(error), _) | (_, Err(error)) => {
98 return Err(raw_failure(&error));
99 }
100 }
101 if left_chunk != right_chunk {
102 return Ok(false);
103 }
104 remaining -= u64::try_from(length).unwrap_or(0);
105 }
106 return Ok(true);
107}
108
109fn link_target(path: &Path) -> Result<PathBuf, WorktreeCopyError> {
119 return std::fs::read_link(path).map_err(|error| return raw_failure(&error));
120}
121
122pub fn entry_matches(
132 expected_root: &Path,
133 actual_root: &Path,
134 entry: &WorktreeCopyEntry,
135) -> Result<bool, WorktreeCopyError> {
136 let expected_path: PathBuf = filesystem_path(expected_root, entry.relative_path.as_str())?;
137 let actual_path: PathBuf = filesystem_path(actual_root, entry.relative_path.as_str())?;
138 let Some(actual) = lstat_or_absent(actual_path.as_path())? else {
139 return Ok(false);
140 };
141 if retained_mode(&actual) != entry.mode {
142 return Ok(false);
143 }
144 match entry.kind {
146 EntryKind::Directory => return Ok(actual.is_dir()),
147 EntryKind::File => {
148 if !actual.is_file() {
149 return Ok(false);
150 }
151 return regular_file_bytes_equal(expected_path.as_path(), actual_path.as_path());
152 }
153 EntryKind::Symlink => {
154 if !actual.file_type().is_symlink() {
155 return Ok(false);
156 }
157 return Ok(link_target(expected_path.as_path())? == link_target(actual_path.as_path())?);
158 }
159 }
160}
161
162pub fn assert_final_source_equivalence(
173 source_root: &Path,
174 stage_root: &Path,
175 selected_roots: &[String],
176 excluded_source_roots: &[PathBuf],
177 staged_entries: &[WorktreeCopyEntry],
178) -> Result<(), WorktreeCopyError> {
179 let final_source = collect_entry_manifest(source_root, selected_roots, excluded_source_roots)?;
180 let final_stage = collect_entry_manifest(stage_root, selected_roots, &[])?;
181 if final_source.len() != staged_entries.len() || final_stage.len() != staged_entries.len() {
182 return Err(failure_with_cause(
183 "cli-git: ignored source paths changed while snapshot was staged.",
184 None,
185 ));
186 }
187 for (index, staged) in staged_entries.iter().enumerate() {
188 let source_entry: &WorktreeCopyEntry = &final_source[index];
189 let stage_entry: &WorktreeCopyEntry = &final_stage[index];
190 let same_shape: bool = source_entry.kind == staged.kind
191 && source_entry.mode == staged.mode
192 && source_entry.relative_path == staged.relative_path
193 && stage_entry.kind == staged.kind
194 && stage_entry.mode == staged.mode
195 && stage_entry.relative_path == staged.relative_path;
196 if !same_shape || !entry_matches(stage_root, source_root, staged)? {
198 let message: String = format!(
199 "cli-git: ignored source entry changed while copying: {}.",
200 quote(staged.relative_path.as_str())
201 );
202 return Err(failure_with_cause(message.as_str(), None));
203 }
204 }
205 return Ok(());
206}
207
208#[cfg(test)]
210#[path = "worktree_copy_compare_tests.rs"]
211mod tests;