backpak/ui/
diff.rs

1use anyhow::*;
2use camino::Utf8Path;
3use clap::Parser;
4use tracing::*;
5
6use crate::backend;
7use crate::config::Configuration;
8use crate::diff;
9use crate::fs_tree;
10use crate::hashing::ObjectId;
11use crate::index;
12use crate::ls;
13use crate::snapshot;
14use crate::tree::{self, Forest, Node, NodeType, meta_diff_char};
15
16/// Compare two snapshots, or compare a snapshot to its paths on the filesystem
17///
18/// + added/file/or/dir
19/// - removed
20/// C contents changed
21/// O ownership changed
22/// P permissions changed
23/// T modify time changed
24/// A access time changed
25/// M other metadata changed
26///
27/// Type changes (e.g. dir -> file, or file -> symlink)
28/// are modeled as removing one and adding the other.
29/// Same goes for symlinks so we can show
30///   - some/symlink -> previous/target
31///   + some/symlink -> new/target
32#[derive(Debug, Parser)]
33#[command(verbatim_doc_comment)]
34#[allow(clippy::doc_lazy_continuation)] // It's a verbatim doc comment, shut up Clippy.
35pub struct Args {
36    /// Print metadata changes (times, permissoins)
37    #[clap(short, long)]
38    metadata: bool,
39
40    #[clap(name = "SNAPSHOT_1")]
41    first_snapshot: String,
42
43    #[clap(name = "SNAPSHOT_2")]
44    second_snapshot: Option<String>,
45    // Should we provide options for remapping to an arbitrary directory, like `restore`?
46}
47
48pub fn run(config: &Configuration, repository: &Utf8Path, args: Args) -> Result<()> {
49    let (_cfg, cached_backend) = backend::open(
50        repository,
51        config.cache_size,
52        backend::CacheBehavior::Normal,
53    )?;
54    let index = index::build_master_index(&cached_backend)?;
55    let blob_map = index::blob_to_pack_map(&index)?;
56    let mut tree_cache = tree::Cache::new(&index, &blob_map, &cached_backend);
57
58    let snapshots = snapshot::load_chronologically(&cached_backend)?;
59    let (snapshot1, id1) = snapshot::find(&snapshots, &args.first_snapshot)?;
60    let snapshot1_forest = tree::forest_from_root(&snapshot1.tree, &mut tree_cache)?;
61
62    let (id2, forest2) = load_snapshot2_or_paths(
63        id1,
64        snapshot1,
65        &snapshot1_forest,
66        &args.second_snapshot,
67        &snapshots,
68        &mut tree_cache,
69    )?;
70
71    diff::compare_trees(
72        (&snapshot1.tree, &snapshot1_forest),
73        (&id2, &forest2),
74        Utf8Path::new(""),
75        &mut PrintDiffs {
76            metadata: args.metadata,
77        },
78    )
79}
80
81fn load_snapshot2_or_paths(
82    id1: &ObjectId,
83    snapshot1: &snapshot::Snapshot,
84    snapshot1_forest: &tree::Forest,
85    second_snapshot: &Option<String>,
86    snapshots: &[(snapshot::Snapshot, ObjectId)],
87    tree_cache: &mut tree::Cache,
88) -> Result<(ObjectId, tree::Forest)> {
89    if let Some(second_snapshot) = second_snapshot {
90        let (snapshot2, id2) = snapshot::find(snapshots, second_snapshot)?;
91        let snapshot2_forest = tree::forest_from_root(&snapshot2.tree, tree_cache)?;
92
93        info!("Comparing snapshot {} to {}", id1, id2);
94
95        Ok((snapshot2.tree, snapshot2_forest))
96    } else {
97        info!(
98            "Comparing snapshot {} to its paths, {:?}",
99            id1, snapshot1.paths
100        );
101        fs_tree::forest_from_fs(
102            // NB: We want the behavior of `diff` to match `restore`,
103            // and we do not dereference symlinks in a filesystem directory we're restoring to.
104            // See the related comments in ui/restore.rs.
105            // Maybe we should expose this rationale in help text or some other user docs...
106            tree::Symlink::Read,
107            &snapshot1.paths,
108            Some(&snapshot1.tree),
109            snapshot1_forest,
110        )
111    }
112}
113
114#[derive(Debug, Default)]
115pub struct PrintDiffs {
116    pub metadata: bool,
117}
118
119impl diff::Callbacks for PrintDiffs {
120    fn node_added(&mut self, node_path: &Utf8Path, new_node: &Node, forest: &Forest) -> Result<()> {
121        ls::print_node("+ ", node_path, new_node, ls::Recurse::Yes(forest));
122        Ok(())
123    }
124
125    fn node_removed(
126        &mut self,
127        node_path: &Utf8Path,
128        old_node: &Node,
129        forest: &Forest,
130    ) -> Result<()> {
131        ls::print_node("- ", node_path, old_node, ls::Recurse::Yes(forest));
132        Ok(())
133    }
134
135    fn contents_changed(
136        &mut self,
137        node_path: &Utf8Path,
138        old_node: &Node,
139        new_node: &Node,
140    ) -> Result<()> {
141        assert!(old_node.kind() == NodeType::File || old_node.kind() == NodeType::Symlink);
142        assert_eq!(old_node.kind(), new_node.kind());
143
144        if old_node.kind() == NodeType::Symlink {
145            ls::print_node("- ", node_path, old_node, ls::Recurse::No);
146            ls::print_node("+ ", node_path, new_node, ls::Recurse::No);
147        } else {
148            ls::print_node("C ", node_path, old_node, ls::Recurse::No);
149        }
150        Ok(())
151    }
152
153    fn metadata_changed(
154        &mut self,
155        node_path: &Utf8Path,
156        old_node: &Node,
157        new_node: &Node,
158    ) -> Result<()> {
159        if self.metadata {
160            let leading_char = format!(
161                "{} ",
162                meta_diff_char(&old_node.metadata, &new_node.metadata).unwrap()
163            );
164            ls::print_node(&leading_char, node_path, new_node, ls::Recurse::No);
165        }
166        Ok(())
167    }
168}