git_policy_cli/worktree_copying/
worktree_copy_compare.rs1use crate::worktree_copy_error::{WorktreeCopyError, failure_with_cause, raw_failure};
15use crate::worktree_copy_manifest::{
16 EntryKind, FileStamp, WorktreeCopyEntry, collect_entry_manifest, collect_pruned_manifest, retained_mode, stamp_of,
17};
18use crate::worktree_copy_prune::Prune;
19use crate::worktree_copy_selection::filesystem_path;
20use crate::json_record::quote;
21use std::io::Read;
22use std::path::{Path, PathBuf};
23
24const COMPARE_BUFFER_BYTES: usize = 64 * 1_024;
37
38pub fn lstat_or_absent(path: &Path) -> Result<Option<std::fs::Metadata>, WorktreeCopyError> {
48 match std::fs::symlink_metadata(path) {
50 Ok(metadata) => return Ok(Some(metadata)),
51 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
52 Err(error) => return Err(raw_failure(&error)),
53 }
54}
55
56fn regular_file_bytes_equal(
66 left_path: &Path,
67 right_path: &Path,
68) -> Result<bool, WorktreeCopyError> {
69 let mut left: std::fs::File =
70 std::fs::File::open(left_path).map_err(|error| return raw_failure(&error))?;
71 let mut right: std::fs::File =
72 std::fs::File::open(right_path).map_err(|error| return raw_failure(&error))?;
73 let left_size: u64 = left
74 .metadata()
75 .map_err(|error| return raw_failure(&error))?
76 .len();
77 let right_size: u64 = right
78 .metadata()
79 .map_err(|error| return raw_failure(&error))?
80 .len();
81 if left_size != right_size {
82 return Ok(false);
83 }
84 let mut left_buffer: Vec<u8> = vec![0; COMPARE_BUFFER_BYTES];
85 let mut right_buffer: Vec<u8> = vec![0; COMPARE_BUFFER_BYTES];
86 let mut remaining: u64 = left_size;
87 while remaining > 0 {
88 let length: usize = usize::try_from(remaining).map_or(COMPARE_BUFFER_BYTES, |size| {
89 return size.min(COMPARE_BUFFER_BYTES);
90 });
91 let left_chunk: &mut [u8] = &mut left_buffer[..length];
92 let right_chunk: &mut [u8] = &mut right_buffer[..length];
93 match (left.read_exact(left_chunk), right.read_exact(right_chunk)) {
95 (Ok(()), Ok(())) => {}
96 (Err(error), _) | (_, Err(error))
97 if error.kind() == std::io::ErrorKind::UnexpectedEof =>
98 {
99 return Ok(false);
100 }
101 (Err(error), _) | (_, Err(error)) => {
102 return Err(raw_failure(&error));
103 }
104 }
105 if left_chunk != right_chunk {
106 return Ok(false);
107 }
108 remaining -= u64::try_from(length).unwrap_or(0);
109 }
110 return Ok(true);
111}
112
113fn link_target(path: &Path) -> Result<PathBuf, WorktreeCopyError> {
123 return std::fs::read_link(path).map_err(|error| return raw_failure(&error));
124}
125
126pub fn entry_matches(
136 expected_root: &Path,
137 actual_root: &Path,
138 entry: &WorktreeCopyEntry,
139) -> Result<bool, WorktreeCopyError> {
140 return entry_matches_stamped(expected_root, actual_root, entry, None);
141}
142
143pub fn entry_matches_stamped(
154 expected_root: &Path,
155 actual_root: &Path,
156 entry: &WorktreeCopyEntry,
157 stamp: Option<&FileStamp>,
158) -> Result<bool, WorktreeCopyError> {
159 let expected_path: PathBuf = filesystem_path(expected_root, entry.relative_path.as_str())?;
160 let actual_path: PathBuf = filesystem_path(actual_root, entry.relative_path.as_str())?;
161 let Some(actual) = lstat_or_absent(actual_path.as_path())? else {
162 return Ok(false);
163 };
164 if retained_mode(&actual) != entry.mode {
165 return Ok(false);
166 }
167 match entry.kind {
169 EntryKind::Directory => return Ok(actual.is_dir()),
170 EntryKind::File => {
171 if !actual.is_file() {
172 return Ok(false);
173 }
174 if let Some(recorded) = stamp {
175 return Ok(stamp_of(&actual).as_ref() == Some(recorded));
176 }
177 return regular_file_bytes_equal(expected_path.as_path(), actual_path.as_path());
178 }
179 EntryKind::Symlink => {
180 if !actual.file_type().is_symlink() {
181 return Ok(false);
182 }
183 return Ok(link_target(expected_path.as_path())? == link_target(actual_path.as_path())?);
184 }
185 }
186}
187
188pub fn assert_final_source_equivalence(
199 source_root: &Path,
200 stage_root: &Path,
201 selected_roots: &[String],
202 excluded_source_roots: &[PathBuf],
203 staged_entries: &[WorktreeCopyEntry],
204) -> Result<(), WorktreeCopyError> {
205 return assert_final_source_equivalence_pruned(
206 source_root,
207 stage_root,
208 selected_roots,
209 excluded_source_roots,
210 staged_entries,
211 &FinalCheck { prune: Prune::Never, stamps: &[] },
212 );
213}
214
215pub struct FinalCheck<'a> {
225 pub prune: Prune,
227 pub stamps: &'a [Option<FileStamp>],
229}
230
231pub fn assert_final_source_equivalence_pruned(
242 source_root: &Path,
243 stage_root: &Path,
244 selected_roots: &[String],
245 excluded_source_roots: &[PathBuf],
246 staged_entries: &[WorktreeCopyEntry],
247 check: &FinalCheck<'_>,
248) -> Result<(), WorktreeCopyError> {
249 let (source_walked, stage_walked) = std::thread::scope(|scope| {
251 let source_walk = scope.spawn(|| {
252 let mut unreadable: Vec<String> = Vec::new();
253 return collect_pruned_manifest(
254 source_root,
255 selected_roots,
256 excluded_source_roots,
257 check.prune,
258 &mut unreadable,
259 );
260 });
261 let stage_walk = collect_entry_manifest(stage_root, selected_roots, &[]);
262 let source_result = source_walk
263 .join()
264 .unwrap_or_else(|_panic| return Err(raw_failure(&std::io::Error::other("a manifest walk panicked"))));
265 return (source_result, stage_walk);
266 });
267 let final_source: Vec<WorktreeCopyEntry> = source_walked?;
268 let final_stage: Vec<WorktreeCopyEntry> = stage_walked?;
269 if final_source.len() != staged_entries.len() || final_stage.len() != staged_entries.len() {
270 return Err(failure_with_cause(
271 "cli-git: ignored source paths changed while snapshot was staged.",
272 None,
273 ));
274 }
275 for (index, staged) in staged_entries.iter().enumerate() {
276 let source_entry: &WorktreeCopyEntry = &final_source[index];
277 let stage_entry: &WorktreeCopyEntry = &final_stage[index];
278 let same_shape: bool = source_entry.kind == staged.kind
279 && source_entry.mode == staged.mode
280 && source_entry.relative_path == staged.relative_path
281 && stage_entry.kind == staged.kind
282 && stage_entry.mode == staged.mode
283 && stage_entry.relative_path == staged.relative_path;
284 let stamp: Option<&FileStamp> = check.stamps.get(index).and_then(Option::as_ref);
285 if !same_shape || !entry_matches_stamped(stage_root, source_root, staged, stamp)? {
287 let message: String = format!(
288 "cli-git: ignored source entry changed while copying: {}.",
289 quote(staged.relative_path.as_str())
290 );
291 return Err(failure_with_cause(message.as_str(), None));
292 }
293 }
294 return Ok(());
295}
296
297#[cfg(test)]
299#[path = "worktree_copy_compare_tests.rs"]
300mod tests;