1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254
//! `diff` subcommand
use crate::{commands::open_repository, status_err, Application, RUSTIC_APP};
use abscissa_core::{Command, Runnable, Shutdown};
use std::path::{Path, PathBuf};
use anyhow::{bail, Context, Result};
use rustic_core::{
repofile::{BlobType, Node, NodeType},
IndexedFull, LocalDestination, LocalSource, LocalSourceFilterOptions, LocalSourceSaveOptions,
LsOptions, ReadSourceEntry, Repository, RusticResult,
};
/// `diff` subcommand
#[derive(clap::Parser, Command, Debug)]
pub(crate) struct DiffCmd {
/// Reference snapshot/path
#[clap(value_name = "SNAPSHOT1[:PATH1]")]
snap1: String,
/// New snapshot/path or local path [default for PATH2: PATH1]
#[clap(value_name = "SNAPSHOT2[:PATH2]|PATH2")]
snap2: String,
/// show differences in metadata
#[clap(long)]
metadata: bool,
/// don't check for different file contents
#[clap(long)]
no_content: bool,
/// Ignore options
#[clap(flatten)]
ignore_opts: LocalSourceFilterOptions,
}
impl Runnable for DiffCmd {
fn run(&self) {
if let Err(err) = self.inner_run() {
status_err!("{}", err);
RUSTIC_APP.shutdown(Shutdown::Crash);
};
}
}
impl DiffCmd {
fn inner_run(&self) -> Result<()> {
let config = RUSTIC_APP.config();
let repo = open_repository(&config)?.to_indexed()?;
let (id1, path1) = arg_to_snap_path(&self.snap1, "");
let (id2, path2) = arg_to_snap_path(&self.snap2, path1);
_ = match (id1, id2) {
(Some(id1), Some(id2)) => {
// diff between two snapshots
let snaps = repo.get_snapshots(&[id1, id2])?;
let snap1 = &snaps[0];
let snap2 = &snaps[1];
let node1 = repo.node_from_snapshot_and_path(snap1, path1)?;
let node2 = repo.node_from_snapshot_and_path(snap2, path2)?;
diff(
repo.ls(&node1, &LsOptions::default())?,
repo.ls(&node2, &LsOptions::default())?,
self.no_content,
|_path, node1, node2| Ok(node1.content == node2.content),
self.metadata,
)
}
(Some(id1), None) => {
// diff between snapshot and local path
let snap1 =
repo.get_snapshot_from_str(id1, |sn| config.snapshot_filter.matches(sn))?;
let node1 = repo.node_from_snapshot_and_path(&snap1, path1)?;
let local = LocalDestination::new(path2, false, !node1.is_dir())?;
let path2 = PathBuf::from(path2);
let is_dir = path2
.metadata()
.with_context(|| format!("Error accessing {path2:?}"))?
.is_dir();
let src = LocalSource::new(
LocalSourceSaveOptions::default(),
&self.ignore_opts,
&[&path2],
)?
.map(|item| -> RusticResult<_> {
let ReadSourceEntry { path, node, .. } = item?;
let path = if is_dir {
// remove given path prefix for dirs as local path
path.strip_prefix(&path2).unwrap().to_path_buf()
} else {
// ensure that we really get the filename if local path is a file
path2.file_name().unwrap().into()
};
Ok((path, node))
});
diff(
repo.ls(&node1, &LsOptions::default())?,
src,
self.no_content,
|path, node1, _node2| identical_content_local(&local, &repo, path, node1),
self.metadata,
)
}
(None, _) => {
bail!("cannot use local path as first argument");
}
};
Ok(())
}
}
/// Split argument into snapshot id and path
///
/// # Arguments
///
/// * `arg` - argument to split
/// * `default_path` - default path if no path is given
///
/// # Returns
///
/// A tuple of the snapshot id and the path
fn arg_to_snap_path<'a>(arg: &'a str, default_path: &'a str) -> (Option<&'a str>, &'a str) {
match arg.split_once(':') {
Some((id, path)) => (Some(id), path),
None => {
if arg.contains('/') {
(None, arg)
} else {
(Some(arg), default_path)
}
}
}
}
/// Check if the content of a file in a snapshot is identical to the content of a local file
///
/// # Arguments
///
/// * `local` - local destination
/// * `repo` - repository
/// * `path` - path of the file in the snapshot
/// * `node` - node of the file in the snapshot
///
/// # Errors
///
/// * [`RepositoryErrorKind::IdNotFound`] - If the id of a blob is not found in the repository
///
/// # Returns
///
/// `true` if the content of the file in the snapshot is identical to the content of the local file,
/// `false` otherwise
///
/// [`RepositoryErrorKind::IdNotFound`]: rustic_core::error::RepositoryErrorKind::IdNotFound
fn identical_content_local<P, S: IndexedFull>(
local: &LocalDestination,
repo: &Repository<P, S>,
path: &Path,
node: &Node,
) -> Result<bool> {
let Some(mut open_file) = local.get_matching_file(path, node.meta.size) else {
return Ok(false);
};
for id in node.content.iter().flatten() {
let ie = repo.get_index_entry(BlobType::Data, id)?;
let length = ie.data_length();
if !id.blob_matches_reader(length as usize, &mut open_file) {
return Ok(false);
}
}
Ok(true)
}
/// Compare two streams of nodes and print the differences
///
/// # Arguments
///
/// * `tree_streamer1` - first stream of nodes
/// * `tree_streamer2` - second stream of nodes
/// * `no_content` - don't check for different file contents
/// * `file_identical` - function to check if the content of two files is identical
/// * `metadata` - show differences in metadata
///
/// # Errors
///
// TODO!: add errors!
fn diff(
mut tree_streamer1: impl Iterator<Item = RusticResult<(PathBuf, Node)>>,
mut tree_streamer2: impl Iterator<Item = RusticResult<(PathBuf, Node)>>,
no_content: bool,
file_identical: impl Fn(&Path, &Node, &Node) -> Result<bool>,
metadata: bool,
) -> Result<()> {
let mut item1 = tree_streamer1.next().transpose()?;
let mut item2 = tree_streamer2.next().transpose()?;
loop {
match (&item1, &item2) {
(None, None) => break,
(Some(i1), None) => {
println!("- {:?}", i1.0);
item1 = tree_streamer1.next().transpose()?;
}
(None, Some(i2)) => {
println!("+ {:?}", i2.0);
item2 = tree_streamer2.next().transpose()?;
}
(Some(i1), Some(i2)) if i1.0 < i2.0 => {
println!("- {:?}", i1.0);
item1 = tree_streamer1.next().transpose()?;
}
(Some(i1), Some(i2)) if i1.0 > i2.0 => {
println!("+ {:?}", i2.0);
item2 = tree_streamer2.next().transpose()?;
}
(Some(i1), Some(i2)) => {
let path = &i1.0;
let node1 = &i1.1;
let node2 = &i2.1;
match &node1.node_type {
tpe if tpe != &node2.node_type => println!("T {path:?}"), // type was changed
NodeType::File if !no_content && !file_identical(path, node1, node2)? => {
println!("M {path:?}");
}
NodeType::File if metadata && node1.meta != node2.meta => {
println!("U {path:?}");
}
NodeType::Symlink { .. } => {
if node1.node_type.to_link() != node1.node_type.to_link() {
println!("U {path:?}");
}
}
_ => {} // no difference to show
}
item1 = tree_streamer1.next().transpose()?;
item2 = tree_streamer2.next().transpose()?;
}
}
}
Ok(())
}