Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package sndiff for openSUSE:Factory checked in at 2025-04-07 17:34:57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/sndiff (Old) and /work/SRC/openSUSE:Factory/.sndiff.new.1907 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "sndiff" Mon Apr 7 17:34:57 2025 rev:3 rq:1267052 version:0.2.2~0 Changes: -------- --- /work/SRC/openSUSE:Factory/sndiff/sndiff.changes 2025-04-02 17:08:55.702192292 +0200 +++ /work/SRC/openSUSE:Factory/.sndiff.new.1907/sndiff.changes 2025-04-07 17:35:03.096894698 +0200 @@ -1,0 +2,8 @@ +Fri Apr 04 07:56:46 UTC 2025 - apla...@suse.com + +- Update to version 0.2.2~0: + * Update to v0.2.2 + * Automatic detection of snapshots + * Add list command + +------------------------------------------------------------------- Old: ---- sndiff-0.2.1~0.tar.zst New: ---- sndiff-0.2.2~0.tar.zst ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ sndiff.spec ++++++ --- /var/tmp/diff_new_pack.54NLQq/_old 2025-04-07 17:35:04.216941545 +0200 +++ /var/tmp/diff_new_pack.54NLQq/_new 2025-04-07 17:35:04.216941545 +0200 @@ -18,7 +18,7 @@ %global rustflags '-Clink-arg=-Wl,-z,relro,-z,now' Name: sndiff -Version: 0.2.1~0 +Version: 0.2.2~0 Release: 0 Summary: Tool for diffing packages and files from snapshots License: MIT ++++++ _service ++++++ --- /var/tmp/diff_new_pack.54NLQq/_old 2025-04-07 17:35:04.252943051 +0200 +++ /var/tmp/diff_new_pack.54NLQq/_new 2025-04-07 17:35:04.256943219 +0200 @@ -3,7 +3,7 @@ <param name="url">https://github.com/aplanas/sndiff.git</param> <param name="versionformat">@PARENT_TAG@~@TAG_OFFSET@</param> <param name="scm">git</param> - <param name="revision">v0.2.1</param> + <param name="revision">v0.2.2</param> <param name="revision">main</param> <param name="match-tag">*</param> <param name="versionrewrite-pattern">v(\d+\.\d+\.\d+)</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.54NLQq/_old 2025-04-07 17:35:04.280944222 +0200 +++ /var/tmp/diff_new_pack.54NLQq/_new 2025-04-07 17:35:04.284944390 +0200 @@ -1,6 +1,6 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/aplanas/sndiff.git</param> - <param name="changesrevision">83bbc3285320ab15973f2ce5d977fff2a17072b1</param></service></servicedata> + <param name="changesrevision">888faba0dcbb23bee89d4f7d5e256093a35eb5c1</param></service></servicedata> (No newline at EOF) ++++++ sndiff-0.2.1~0.tar.zst -> sndiff-0.2.2~0.tar.zst ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sndiff-0.2.1~0/Cargo.lock new/sndiff-0.2.2~0/Cargo.lock --- old/sndiff-0.2.1~0/Cargo.lock 2025-03-31 14:41:27.000000000 +0200 +++ new/sndiff-0.2.2~0/Cargo.lock 2025-04-04 09:53:16.000000000 +0200 @@ -3,6 +3,21 @@ version = 4 [[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] name = "anstream" version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -65,12 +80,41 @@ checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" [[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "cc" +version = "1.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a" +dependencies = [ + "shlex", +] + +[[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] +name = "chrono" +version = "0.4.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] name = "clap" version = "4.5.34" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -126,6 +170,12 @@ ] [[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] name = "crossterm" version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -188,6 +238,30 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -200,6 +274,16 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] name = "libc" version = "0.2.171" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -284,6 +368,15 @@ checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] name = "num_threads" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -407,6 +500,12 @@ ] [[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + +[[package]] name = "ryu" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -457,6 +556,12 @@ ] [[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] name = "signal-hook" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -519,6 +624,7 @@ name = "sndiff" version = "0.2.1" dependencies = [ + "chrono", "clap", "colored", "semver", @@ -682,6 +788,64 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -713,6 +877,65 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] +name = "windows-core" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-result" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +dependencies = [ + "windows-link", +] + +[[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sndiff-0.2.1~0/Cargo.toml new/sndiff-0.2.2~0/Cargo.toml --- old/sndiff-0.2.1~0/Cargo.toml 2025-03-31 14:41:27.000000000 +0200 +++ new/sndiff-0.2.2~0/Cargo.toml 2025-04-04 09:53:16.000000000 +0200 @@ -1,9 +1,10 @@ [package] name = "sndiff" -version = "0.2.1" +version = "0.2.2" edition = "2024" [dependencies] +chrono = "0.4.40" clap = { version = "4.5.34", features = ["derive"] } colored = "3.0.0" semver = "1.0.26" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sndiff-0.2.1~0/README.md new/sndiff-0.2.2~0/README.md --- old/sndiff-0.2.1~0/README.md 2025-03-31 14:41:27.000000000 +0200 +++ new/sndiff-0.2.2~0/README.md 2025-04-04 09:53:16.000000000 +0200 @@ -16,6 +16,10 @@ `sndiff 3 4`: Compare snapshots 3 (old) and 4 (new), showing the same information than before. +`sndiff`: Compare the previous snapshot (old) with the current active +one (new). The previous snapshot selection depends on the kind of +distribution (MicroOS or Tumbleweed). + `sndiff --short 3 4`: Present the information in a compact way. `sndiff --packages 3 4`: Only compares packages from snapshots 3 and @@ -29,4 +33,9 @@ `sndiff --diff --json 3 4`: Generate JSON output, this time includes the full differences. -`sndiff --no-colors 3 4` -- No colorized output +`sndiff --no-colors 3 4`: No colorized output. + +`sndiff list`: Print a table of current snapshots. Mark the snapshot +selected as `old` and `new`. + +`sndiff --json list`: Shows the same table in JSON format. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sndiff-0.2.1~0/src/main.rs new/sndiff-0.2.2~0/src/main.rs --- old/sndiff-0.2.1~0/src/main.rs 2025-03-31 14:41:27.000000000 +0200 +++ new/sndiff-0.2.2~0/src/main.rs 2025-04-04 09:53:16.000000000 +0200 @@ -9,10 +9,11 @@ use std::sync::LazyLock; use std::time::Duration; +use chrono::{DateTime, Utc}; use clap::{Parser, Subcommand}; use colored::Colorize; use semver::Version; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use similar::TextDiff; use termbg::Theme; use textwrap::{Options, WordSplitter, fill}; @@ -20,10 +21,11 @@ #[derive(Parser)] #[command(version, about, long_about = None)] struct Cli { - /// Snapshot to compare with + /// Snapshot to compare with. If missing will be autodetected old_snapshot: Option<u32>, - /// Optional reference snapshot. If missing use the current one + /// Optional reference snapshot. Usually the current one. If + /// missing will be autodetected new_snapshot: Option<u32>, /// Report only changes in packages @@ -50,9 +52,9 @@ #[arg(long, short)] no_colors: bool, - /// Turn debugging information on - #[arg(short, long, action = clap::ArgAction::Count)] - debug: u8, + /// Verbose output + #[arg(long, short)] + verbose: bool, #[command(subcommand)] command: Option<Commands>, @@ -119,15 +121,17 @@ removed: Vec<Package>, } -fn get_packages_from(snapshot: Option<u32>, changelog: bool) -> Result<Vec<Package>, String> { - let dbpath = if let Some(id) = snapshot { - format!("/.snapshots/{id}/snapshot/usr/lib/sysimage/rpm") - } else { - "/usr/lib/sysimage/rpm".to_string() - }; +fn get_packages_from( + snapshot: u32, + changelog: bool, + verbose: bool, +) -> Result<Vec<Package>, String> { + let dbpath = format!("/.snapshots/{snapshot}/snapshot/usr/lib/sysimage/rpm"); check_directory_exists_and_readable(&dbpath)?; - println!("Reading packages from {dbpath} ..."); + if verbose { + println!("Reading packages from {dbpath} ..."); + } let query_format = if changelog { "%{NAME}\n%{VERSION}-%{RELEASE}\n---BEGIN CHANGELOG\n[* %{CHANGELOGTIME:date} %{CHANGELOGNAME}\n%{CHANGELOGTEXT}\n\n]---END CHANGELOG\n" @@ -530,7 +534,7 @@ #[derive(Debug, Clone, Eq, Ord, PartialEq, PartialOrd, Serialize)] struct FileInfo { path: String, - root_path: Option<String>, + root_path: String, size: u64, file_type: FileType, } @@ -546,8 +550,8 @@ #[derive(Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)] struct FileChange { path: String, - root_path_from: Option<String>, - root_path_to: Option<String>, + root_path_from: String, + root_path_to: String, size_from: u64, size_to: u64, file_type_from: FileType, @@ -562,21 +566,24 @@ removed: Vec<FileInfo>, } -fn get_files_from(snapshot: Option<u32>, dir_path: &str) -> Result<Vec<FileInfo>, std::io::Error> { - let snapshot_path = snapshot.map(|id| format!("/.snapshots/{id}/snapshot")); - let path = format!( - "{}{dir_path}", - snapshot_path.clone().unwrap_or("".to_string()) - ); +fn get_files_from( + snapshot: u32, + dir_path: &str, + verbose: bool, +) -> Result<Vec<FileInfo>, std::io::Error> { + let snapshot_path = format!("/.snapshots/{snapshot}/snapshot"); + let path = format!("{snapshot_path}{dir_path}"); - println!("Reading files from {path} ..."); + if verbose { + println!("Reading files from {path} ..."); + } get_files_in_directory_recursive(&path, &snapshot_path) } fn get_files_in_directory_recursive( dir_path: &str, - root_path: &Option<String>, + root_path: &str, ) -> Result<Vec<FileInfo>, std::io::Error> { let path = Path::new(dir_path); @@ -594,9 +601,7 @@ let entry_path = entry.path(); let metadata = fs::symlink_metadata(&entry_path)?; - let relative_path = entry_path - .strip_prefix(root_path.as_ref().map_or("", |v| v)) - .unwrap_or(&entry_path); + let relative_path = entry_path.strip_prefix(root_path).unwrap_or(&entry_path); let full_path = Path::new("/").join(relative_path); let size = metadata.len(); @@ -613,7 +618,7 @@ let file_info = FileInfo { path: full_path.to_string_lossy().to_string(), - root_path: root_path.clone(), + root_path: root_path.to_string(), size, file_type, }; @@ -646,12 +651,12 @@ if let Some(old_file) = old_map.get(name) { if new_file.size != old_file.size || new_file.file_type != old_file.file_type { let old = fs::read_to_string( - Path::new(&old_file.root_path.clone().unwrap_or("/".to_string())) + Path::new(&old_file.root_path) .join(old_file.path.strip_prefix("/").unwrap_or(&old_file.path)), ) .unwrap_or("".to_string()); let new = fs::read_to_string( - Path::new(&new_file.root_path.clone().unwrap_or("/".to_string())) + Path::new(&new_file.root_path) .join(new_file.path.strip_prefix("/").unwrap_or(&new_file.path)), ) .unwrap_or("".to_string()); @@ -710,33 +715,33 @@ println!("{}", fill(&paths, &options)); } else { for f in &file_changes.modified { - let path = if colored { + let path = if colored { &f.path.green().to_string() - } else { + } else { &f.path - }; - let size_from = if colored { + }; + let size_from = if colored { match &*THEME { - Theme::Dark => f.size_from.to_string().white().to_string(), - Theme::Light => f.size_from.to_string().bright_black().to_string(), + Theme::Dark => f.size_from.to_string().white().to_string(), + Theme::Light => f.size_from.to_string().bright_black().to_string(), } - } else { + } else { f.size_from.to_string() - }; - let size_to = if colored { + }; + let size_to = if colored { match &*THEME { - Theme::Dark => f.size_to.to_string().bright_white().to_string(), - Theme::Light => f.size_to.to_string().black().to_string(), + Theme::Dark => f.size_to.to_string().bright_white().to_string(), + Theme::Light => f.size_to.to_string().black().to_string(), } - } else { + } else { f.size_to.to_string() - }; - println!(" {path} ({size_from} -> {size_to})"); - if let Some(ref file_diff) = f.file_diff { + }; + println!(" {path} ({size_from} -> {size_to})"); + if let Some(ref file_diff) = f.file_diff { print_diff(file_diff, 10, colored); - } + } } - } + } println!(); } @@ -765,14 +770,14 @@ println!("{}", fill(&paths, &options)); } else { for f in &file_changes.added { - let path = if colored { + let path = if colored { &f.path.blue().to_string() - } else { + } else { &f.path - }; - println!(" {path}"); + }; + println!(" {path}"); } - } + } println!(); } @@ -781,7 +786,7 @@ "The following {} files were REMOVED:", file_changes.removed.len() ); - if short { + if short { let paths = file_changes .removed .iter() @@ -801,14 +806,14 @@ println!("{}", fill(&paths, &options)); } else { for f in &file_changes.removed { - let path = if colored { + let path = if colored { &f.path.red().to_string() - } else { + } else { &f.path - }; - println!(" {path}"); + }; + println!(" {path}"); } - } + } println!(); } } @@ -855,9 +860,164 @@ } } -fn cmd_list(_cli: &Cli) -> Result<(), AppError> { - eprintln!("Command not implemented! Use `snapper ls` for now."); - // sudo snapper --jsonout --no-dbus ls --disable-used-space +#[derive(Debug, Deserialize, Serialize, PartialEq)] +#[serde(rename_all = "lowercase")] +enum SnapshotType { + Single, + Pre, + Post, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "lowercase")] +enum SnapshotCmp { + Old, + New, +} + +#[derive(Debug, Deserialize, Serialize)] +struct Snapshot { + number: u32, + default: bool, + active: bool, + #[serde(rename = "type")] + type_: SnapshotType, + #[serde(rename = "pre-number")] + pre_number: Option<u32>, + #[serde(with = "datetime_str")] + date: Option<DateTime<Utc>>, + description: String, + cmp: Option<SnapshotCmp>, +} + +#[derive(Debug, Deserialize, Serialize)] +struct Snapshots { + root: Vec<Snapshot>, +} + +mod datetime_str { + use chrono::{DateTime, NaiveDateTime, Utc}; + use serde::{self, Deserialize, Deserializer, Serializer}; + + const FORMAT: &str = "%Y-%m-%d %H:%M:%S"; + + pub fn serialize<S>(date: &Option<DateTime<Utc>>, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + let s = match date { + Some(d) => format!("{}", d.format(FORMAT)), + None => "".to_string(), + }; + serializer.serialize_str(&s) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<DateTime<Utc>>, D::Error> + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + + if s.is_empty() { + return Ok(None); + } + + let dt = NaiveDateTime::parse_from_str(&s, FORMAT).map_err(serde::de::Error::custom)?; + Ok(Some(DateTime::<Utc>::from_naive_utc_and_offset(dt, Utc))) + } +} + +fn get_snapshots() -> Result<Snapshots, AppError> { + let output = Command::new("snapper") + .arg("--jsonout") + .arg("--no-dbus") + .arg("ls") + .arg("--disable-used-space") + .output() + .map_err(|e| format!("Failed to execute snapper: {}", e))?; + + if !output.status.success() { + eprintln!("stdout: {}", str::from_utf8(&output.stdout).unwrap()); + eprintln!("stderr: {}", str::from_utf8(&output.stderr).unwrap()); + return Err(format!("snapper failed with status: {}", output.status).into()); + } + + let stdout = + str::from_utf8(&output.stdout).map_err(|e| format!("Invalid UTF-8 output: {}", e))?; + + let snapshots: Snapshots = serde_json::from_str(stdout).expect("Failed to parse JSON"); + Ok(snapshots) +} + +fn print_snapshots(snapshots: &Snapshots) { + let date_len = "xxxx-xx-xx xx:xx:xx xxx".len(); + println!( + "{:>5} | Cmp | {:6} | {:>5} | {:date_len$} | Description", + "#", "Type", "Pre #", "Date" + ); + println!( + "------+-----+--------+-------+-{}-+-{}", + "-".repeat(date_len), + "-".repeat(date_len) + ); + for snapshot in &snapshots.root { + if snapshot.number == 0 { + continue; + } + + println!( + "{:5}{}| {:3} | {:6} | {:>5} | {} | {}", + snapshot.number, + if snapshot.default && snapshot.active { + '*' + } else if snapshot.default { + '-' + } else if snapshot.active { + '+' + } else { + ' ' + }, + match snapshot.cmp { + Some(SnapshotCmp::Old) => "old", + Some(SnapshotCmp::New) => "new", + None => "", + }, + match snapshot.type_ { + SnapshotType::Single => "single", + SnapshotType::Pre => "pre", + SnapshotType::Post => "post", + }, + match snapshot.pre_number { + Some(n) => n.to_string(), + None => "".to_string(), + }, + match snapshot.date { + Some(d) => d.to_string(), + None => " ".repeat("xxxx-xx-xx xx:xx:xx xxx".len()), + }, + snapshot.description + ); + } +} + +fn cmd_list(cli: &Cli) -> Result<(), AppError> { + let mut snapshots = get_snapshots()?; + + let (old_snapshot, new_snapshot) = get_old_new_snapshots(cli)?; + + for s in &mut snapshots.root { + if s.number == old_snapshot { + s.cmp = Some(SnapshotCmp::Old); + } else if s.number == new_snapshot { + s.cmp = Some(SnapshotCmp::New); + } + } + + if cli.json { + println!("{}", serde_json::to_string(&snapshots).unwrap()); + } else { + print_snapshots(&snapshots); + } Ok(()) } @@ -867,26 +1027,94 @@ files: FileChanges, } -fn cmd_diff(cli: &Cli) -> Result<(), AppError> { - if cli.old_snapshot.is_none() { - return Err("Missing old snapshot parameter".into()); +fn is_transactional() -> bool { + if check_directory_exists_and_readable("/.snapshots").is_err() { + return false; + } + + let output = Command::new("rpm") + .arg("--query") + .arg("--quiet") + .arg("read-only-root-fs") + .output(); + + let output = match output { + Ok(o) => o, + Err(_) => return false, }; + output.status.success() +} + +fn get_old_new_snapshots(cli: &Cli) -> Result<(u32, u32), AppError> { + let microos = is_transactional(); + let snapshots = get_snapshots()?; + + let default = snapshots + .root + .iter() + .find(|s| s.default) + .map(|s| s.number) + .expect("No default snapshot found"); + let active = snapshots + .root + .iter() + .find(|s| s.active) + .map(|s| s.number) + .expect("No active snapshot found"); + + let old_snapshot_default = if active != default { + default + } else if microos { + // Select the snapshot ID just before the default / active one + snapshots + .root + .iter() + .filter(|s| s.number < active) + .map(|s| s.number) + .next_back() + .unwrap_or(active) + } else { + // Select the snapshot ID of the last "pre" with ID bigger + // than the active one + snapshots + .root + .iter() + .filter(|s| s.type_ == SnapshotType::Pre) + .filter(|s| s.number > active) + .map(|s| s.number) + .next_back() + .unwrap_or(active) + }; + + let new_snapshot_default = active; + + match (cli.old_snapshot, cli.new_snapshot) { + (Some(o), Some(n)) => Ok((o, n)), + (Some(o), None) => Ok((o, new_snapshot_default)), + (None, Some(n)) => Ok((old_snapshot_default, n)), + (None, None) => Ok((old_snapshot_default, new_snapshot_default)), + } +} + +fn cmd_diff(cli: &Cli) -> Result<(), AppError> { + let (old_snapshot, new_snapshot) = get_old_new_snapshots(cli)?; + check_directory_exists_and_readable("/.snapshots")?; let mut pkg_changes: Option<PackageChanges> = None; let mut etc_changes: Option<FileChanges> = None; if cli.packages || !cli.etc { - let old_packages = get_packages_from(cli.old_snapshot, cli.diff)?; - let new_packages = get_packages_from(cli.new_snapshot, cli.diff)?; + let old_packages = get_packages_from(old_snapshot, cli.diff, cli.verbose)?; + let new_packages = get_packages_from(new_snapshot, cli.diff, cli.verbose)?; pkg_changes = Some(package_changes(&old_packages, &new_packages)); } if cli.etc || !cli.packages { - let old_files = get_files_from(cli.old_snapshot, "/etc")?; - let new_files = get_files_from(cli.new_snapshot, "/etc")?; + let old_files = get_files_from(old_snapshot, "/etc", cli.verbose)?; + let new_files = get_files_from(new_snapshot, "/etc", cli.verbose)?; etc_changes = Some(file_changes(&old_files, &new_files)); } @@ -1009,7 +1237,7 @@ File::create("test_dir/subdir1/file2.txt")?; File::create("test_dir/subdir1/subdir2/file3.txt")?; - let files = get_files_in_directory_recursive("test_dir", &None)?; + let files = get_files_in_directory_recursive("test_dir", "")?; assert_eq!(files.len(), 3); // assert!(files.contains(&"file1.txt".to_string())); // assert!(files.contains(&"file2.txt".to_string())); @@ -1022,7 +1250,7 @@ #[test] fn test_get_files_in_directory_recursive_not_a_directory() { - let result = get_files_in_directory_recursive("not_a_directory", &None); // Nonexistent path + let result = get_files_in_directory_recursive("not_a_directory", ""); // Nonexistent path assert!(result.is_err()); if let Err(e) = result { assert_eq!(e.kind(), std::io::ErrorKind::NotADirectory); ++++++ sndiff.8.md ++++++ --- /var/tmp/diff_new_pack.54NLQq/_old 2025-04-07 17:35:04.404949409 +0200 +++ /var/tmp/diff_new_pack.54NLQq/_new 2025-04-07 17:35:04.408949577 +0200 @@ -27,10 +27,10 @@ # ARGUMENTS `OLD_SNAPSHOT` -: Snapshot to compare with +: Snapshot to compare with. If missing will be autodetected. `NEW_SNAPSHOT` -: Optional reference snapshot. If missing use the current one. +: Optional reference snapshot. Usually the current one. If missing will be autodetected. # OPTIONS @@ -52,8 +52,8 @@ `--no-colors`, `-n` : Disable colored output -`--debug`, `-d` -: Turn debugging information on +`--verbose`, `-v` +: Verbose output `--help`, `-h` : Print help @@ -72,6 +72,12 @@ : Compare snapshots 3 (old) and 4 (new), showing the same information than before. +`sndiff` + +: Compare the previous snapshot (old) with the current active one + (new). The previous snapshot selection depends on the kind of + distribution (MicroOS or Tumbleweed). + `sndiff --short 3 4` : Present the information in a compact way. @@ -91,6 +97,13 @@ `sndiff --no-colors 3 4` : No colorized output +`sndiff list` +: Print a table of current snapshots. Mark the snapshot selected as + `old` and `new`. + +`sndiff --json list` +: Shows the same table in JSON format. + # SEE ALSO **snapper**(8), **transactional-update**(8), **zypper**(8) ++++++ sndiff.obsinfo ++++++ --- /var/tmp/diff_new_pack.54NLQq/_old 2025-04-07 17:35:04.432950580 +0200 +++ /var/tmp/diff_new_pack.54NLQq/_new 2025-04-07 17:35:04.436950748 +0200 @@ -1,5 +1,5 @@ name: sndiff -version: 0.2.1~0 -mtime: 1743424887 -commit: 83bbc3285320ab15973f2ce5d977fff2a17072b1 +version: 0.2.2~0 +mtime: 1743753196 +commit: 888faba0dcbb23bee89d4f7d5e256093a35eb5c1 ++++++ vendor.tar.zst ++++++ /work/SRC/openSUSE:Factory/sndiff/vendor.tar.zst /work/SRC/openSUSE:Factory/.sndiff.new.1907/vendor.tar.zst differ: char 7, line 1