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

Reply via email to