Skip to main content

git_policy_cli/worktree/
worktree_convert_compare.rs

1//! What:
2//!  A read-only comparison of the old tree of a conversion with the installed one: the first entry of the old tree that the
3//!  installed tree does not hold in the same form, or that this command does not delete.
4//! Why:
5//!  The old tree is deleted only after the installed copy is known to hold everything in it. The comparison changes neither
6//!  tree and copies nothing: a difference keeps the old tree and says which entry differs, and whoever knows which version is
7//!  wanted settles it. An entry on another device (a nested subvolume or a mount point) is reported the same way, because
8//!  deleting it is not this command's to do.
9//!
10//! In TS you'd write (pseudocode):
11//! ```ts
12//! const difference = await firstDifference(oldRoot, installedRoot); // undefined when the installed tree holds all of it
13//! ```
14
15use std::os::unix::fs::MetadataExt;
16use std::path::{Path, PathBuf};
17
18/// What:
19///  The first way the old tree at `old` is not held by the installed tree at `installed`, or nothing when it is held whole.
20/// Why:
21///   See the module description. A file is the same when its type, size and modification time are, a link when its target is;
22///  the installed tree may hold more than the old one, because it has been in use since the exchange.
23///
24/// In TS you'd write (pseudocode):
25/// ```ts
26/// firstDifference(old, installed);
27/// ```
28pub fn first_difference(old: &Path, installed: &Path) -> std::io::Result<Option<String>> {
29    // The root may be named through a descriptor's own path, a link that is followed to the directory it holds.
30    let device: u64 = std::fs::metadata(old)?.dev();
31    let mut pending: Vec<PathBuf> = vec![PathBuf::new()];
32    while let Some(relative) = pending.pop() {
33        for item in std::fs::read_dir(old.join(&relative))? {
34            let entry: std::fs::DirEntry = item?;
35            let inside: PathBuf = relative.join(entry.file_name());
36            let before: std::fs::Metadata = std::fs::symlink_metadata(old.join(&inside))?;
37            if before.dev() != device {
38                return Ok(Some(format!("{} is a subvolume or a mount point inside the old tree, which this command does not delete", inside.display())));
39            }
40            let after: std::fs::Metadata = match std::fs::symlink_metadata(installed.join(&inside)) {
41                Ok(found) => found,
42                Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Some(format!("{} is missing from the installed worktree", inside.display()))),
43                Err(error) => return Err(error),
44            };
45            if before.file_type() != after.file_type() {
46                return Ok(Some(format!("{} is not the same kind of entry in the installed worktree", inside.display())));
47            }
48            if before.file_type().is_dir() {
49                pending.push(inside);
50            } else if before.file_type().is_symlink() {
51                if std::fs::read_link(old.join(&inside))? != std::fs::read_link(installed.join(&inside))? {
52                    return Ok(Some(format!("the link {} points elsewhere in the installed worktree", inside.display())));
53                }
54            } else if before.file_type().is_file() && (before.size() != after.size() || before.mtime() != after.mtime() || before.mtime_nsec() != after.mtime_nsec()) {
55                return Ok(Some(format!("{} differs from the installed worktree (size or modification time)", inside.display())));
56            }
57        }
58    }
59    return Ok(None);
60}
61
62/// Comparison controls stay out of the release executable.
63#[cfg(test)]
64#[path = "worktree_convert_compare_tests.rs"]
65mod tests;