diff --git a/Cargo.lock b/Cargo.lock index 334ad4e..54697ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -800,6 +800,28 @@ dependencies = [ "triomphe", ] +[[package]] +name = "conflate" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06f57b4959992e02db0da86fcb4b0c80dc112d31b823de27f61576b1c809bc45" +dependencies = [ + "conflate_derive", + "num-traits", +] + +[[package]] +name = "conflate_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9395ace5316656ca6a778aa2c28ab0c4ea2c94a8f7dc942898c20be9b7a9a4b9" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.77", +] + [[package]] name = "console" version = "0.15.8" @@ -3153,6 +3175,28 @@ dependencies = [ "version_check", ] +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.77", +] + [[package]] name = "proc-macro2" version = "1.0.86" @@ -3674,6 +3718,7 @@ dependencies = [ "clap", "clap_complete", "comfy-table", + "conflate", "convert_case", "crossterm 0.28.1", "dav-server", @@ -3690,7 +3735,6 @@ dependencies = [ "jemallocator-global", "libc", "log", - "merge", "mimalloc", "once_cell", "predicates", diff --git a/Cargo.toml b/Cargo.toml index 9f7407b..ad2263c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -85,6 +85,7 @@ bytesize = "1" cached = "0.53.1" clap = { version = "4", features = ["derive", "env", "wrap_help"] } clap_complete = "4" +conflate = "0.2" convert_case = "0.6.0" dialoguer = "0.11.0" directories = "5" @@ -94,7 +95,6 @@ human-panic = "2.0.1" humantime = "2" indicatif = "0.17" itertools = "0.13" -merge = "0.1" once_cell = "1.19" self_update = { version = "0.41", default-features = false, optional = true, features = ["rustls", "archive-tar", "compression-flate2"] } toml = "0.8" diff --git a/src/commands/backup.rs b/src/commands/backup.rs index ae07e1c..f5ec3b2 100644 --- a/src/commands/backup.rs +++ b/src/commands/backup.rs @@ -12,8 +12,8 @@ use abscissa_core::{Command, Runnable, Shutdown}; use anyhow::{bail, Context, Result}; use clap::ValueHint; use comfy_table::Cell; +use conflate::Merge; use log::{debug, info, warn}; -use merge::Merge; use serde::{Deserialize, Serialize}; use serde_with::serde_as; @@ -47,10 +47,12 @@ pub struct BackupCmd { /// Start the given command and use its output as stdin #[clap(long, value_name = "COMMAND")] + #[merge(strategy = conflate::option::overwrite_none)] stdin_command: Option, /// Manually set backup path in snapshot #[clap(long, value_name = "PATH", value_hint = ValueHint::DirPath)] + #[merge(strategy = conflate::option::overwrite_none)] as_path: Option, /// Ignore save options @@ -60,27 +62,27 @@ pub struct BackupCmd { /// Don't scan the backup source for its size - this disables ETA estimation for backup. #[clap(long)] - #[merge(strategy = merge::bool::overwrite_false)] + #[merge(strategy = conflate::bool::overwrite_false)] pub no_scan: bool, /// Output generated snapshot in json format #[clap(long)] - #[merge(strategy = merge::bool::overwrite_false)] + #[merge(strategy = conflate::bool::overwrite_false)] json: bool, /// Show detailed information about generated snapshot #[clap(long, conflicts_with = "json")] - #[merge(strategy = merge::bool::overwrite_false)] + #[merge(strategy = conflate::bool::overwrite_false)] long: bool, /// Don't show any output #[clap(long, conflicts_with_all = ["json", "long"])] - #[merge(strategy = merge::bool::overwrite_false)] + #[merge(strategy = conflate::bool::overwrite_false)] quiet: bool, /// Initialize repository, if it doesn't exist yet #[clap(long)] - #[merge(strategy = merge::bool::overwrite_false)] + #[merge(strategy = conflate::bool::overwrite_false)] init: bool, /// Parent processing options diff --git a/src/commands/copy.rs b/src/commands/copy.rs index c905ade..7323d03 100644 --- a/src/commands/copy.rs +++ b/src/commands/copy.rs @@ -7,8 +7,8 @@ use crate::{ }; use abscissa_core::{config::Override, Command, FrameworkError, Runnable, Shutdown}; use anyhow::{bail, Result}; +use conflate::Merge; use log::{error, info, log, Level}; -use merge::Merge; use serde::{Deserialize, Serialize}; use rustic_core::{CopySnapshot, Id, KeyOptions}; @@ -30,7 +30,7 @@ pub struct CopyCmd { /// Target repository (can be specified multiple times) #[clap(long = "target", value_name = "TARGET")] - #[merge(strategy = merge::vec::overwrite_empty)] + #[merge(strategy = conflate::vec::overwrite_empty)] targets: Vec, /// Key options (when using --init) diff --git a/src/commands/forget.rs b/src/commands/forget.rs index 30b0c23..d488fd4 100644 --- a/src/commands/forget.rs +++ b/src/commands/forget.rs @@ -10,7 +10,7 @@ use abscissa_core::{Command, FrameworkError, Runnable}; use anyhow::Result; use chrono::Local; -use merge::Merge; +use conflate::Merge; use serde::{Deserialize, Serialize}; use serde_with::{serde_as, DisplayFromStr}; @@ -70,11 +70,12 @@ pub struct ForgetOptions { /// Group snapshots by any combination of host,label,paths,tags (default: "host,label,paths") #[clap(long, short = 'g', value_name = "CRITERION")] #[serde_as(as = "Option")] + #[merge(strategy = conflate::option::overwrite_none)] group_by: Option, /// Also prune the repository #[clap(long)] - #[merge(strategy = merge::bool::overwrite_false)] + #[merge(strategy = conflate::bool::overwrite_false)] prune: bool, /// Snapshot filter options diff --git a/src/commands/webdav.rs b/src/commands/webdav.rs index 1923549..1d40e64 100644 --- a/src/commands/webdav.rs +++ b/src/commands/webdav.rs @@ -8,8 +8,8 @@ use std::{net::ToSocketAddrs, str::FromStr}; use crate::{commands::open_repository_indexed, status_err, Application, RusticConfig, RUSTIC_APP}; use abscissa_core::{config::Override, Command, FrameworkError, Runnable, Shutdown}; use anyhow::{anyhow, Result}; +use conflate::Merge; use dav_server::{warp::dav_handler, DavHandler}; -use merge::Merge; use serde::{Deserialize, Serialize}; use rustic_core::vfs::{FilePolicy, IdenticalSnapshot, Latest, Vfs}; @@ -19,27 +19,32 @@ use rustic_core::vfs::{FilePolicy, IdenticalSnapshot, Latest, Vfs}; pub struct WebDavCmd { /// Address to bind the webdav server to. [default: "localhost:8000"] #[clap(long, value_name = "ADDRESS")] + #[merge(strategy = conflate::option::overwrite_none)] address: Option, /// The path template to use for snapshots. {id}, {id_long}, {time}, {username}, {hostname}, {label}, {tags}, {backup_start}, {backup_end} are replaced. [default: "[{hostname}]/[{label}]/{time}"] #[clap(long)] + #[merge(strategy = conflate::option::overwrite_none)] path_template: Option, /// The time template to use to display times in the path template. See https://docs.rs/chrono/latest/chrono/format/strftime/index.html for format options. [default: "%Y-%m-%d_%H-%M-%S"] #[clap(long)] + #[merge(strategy = conflate::option::overwrite_none)] time_template: Option, /// Use symlinks. This may not be supported by all WebDAV clients #[clap(long)] - #[merge(strategy = merge::bool::overwrite_false)] + #[merge(strategy = conflate::bool::overwrite_false)] symlinks: bool, /// How to handle access to files. [default: "forbidden" for hot/cold repositories, else "read"] #[clap(long)] + #[merge(strategy = conflate::option::overwrite_none)] file_access: Option, /// Specify directly which snapshot/path to serve #[clap(value_name = "SNAPSHOT[:PATH]")] + #[merge(strategy = conflate::option::overwrite_none)] snapshot_path: Option, } diff --git a/src/config.rs b/src/config.rs index 4be98a5..f7fa806 100644 --- a/src/config.rs +++ b/src/config.rs @@ -12,10 +12,10 @@ use abscissa_core::config::Config; use abscissa_core::path::AbsPathBuf; use abscissa_core::FrameworkError; use clap::{Parser, ValueHint}; +use conflate::Merge; use directories::ProjectDirs; use itertools::Itertools; use log::Level; -use merge::Merge; use rustic_backend::BackendOptions; use rustic_core::RepositoryOptions; use serde::{Deserialize, Serialize}; @@ -136,21 +136,22 @@ pub struct GlobalOptions { value_name = "PROFILE", env = "RUSTIC_USE_PROFILE" )] - #[merge(strategy = merge::vec::append)] + #[merge(strategy = conflate::vec::append)] pub use_profiles: Vec, /// Only show what would be done without modifying anything. Does not affect read-only commands. #[clap(long, short = 'n', global = true, env = "RUSTIC_DRY_RUN")] - #[merge(strategy = merge::bool::overwrite_false)] + #[merge(strategy = conflate::bool::overwrite_false)] pub dry_run: bool, /// Check if index matches pack files and read pack headers if neccessary #[clap(long, global = true, env = "RUSTIC_CHECK_INDEX")] - #[merge(strategy = merge::bool::overwrite_false)] + #[merge(strategy = conflate::bool::overwrite_false)] pub check_index: bool, /// Use this log level [default: info] #[clap(long, global = true, env = "RUSTIC_LOG_LEVEL")] + #[merge(strategy = conflate::option::overwrite_none)] pub log_level: Option, /// Write log messages to the given file instead of printing them. @@ -159,6 +160,7 @@ pub struct GlobalOptions { /// /// Warnings and errors are still additionally printed unless they are ignored by `--log-level` #[clap(long, global = true, env = "RUSTIC_LOG_FILE", value_name = "LOGFILE", value_hint = ValueHint::FilePath)] + #[merge(strategy = conflate::option::overwrite_none)] pub log_file: Option, /// Settings to customize progress bars @@ -168,16 +170,10 @@ pub struct GlobalOptions { /// List of environment variables to set (only in config file) #[clap(skip)] - #[merge(strategy = extend)] + #[merge(strategy = conflate::hashmap::overwrite)] pub env: HashMap, } -/// Extend the contents of a [`HashMap`] with the contents of another -/// [`HashMap`] with the same key and value types. -fn extend(left: &mut HashMap, right: HashMap) { - left.extend(right); -} - /// Get the paths to the config file /// /// # Arguments diff --git a/src/config/progress_options.rs b/src/config/progress_options.rs index cfb41ce..19c8841 100644 --- a/src/config/progress_options.rs +++ b/src/config/progress_options.rs @@ -5,7 +5,7 @@ use std::{borrow::Cow, fmt::Write, time::Duration}; use indicatif::{HumanDuration, ProgressBar, ProgressState, ProgressStyle}; use clap::Parser; -use merge::Merge; +use conflate::Merge; use serde::{Deserialize, Serialize}; use serde_with::{serde_as, DisplayFromStr}; @@ -19,7 +19,7 @@ use rustic_core::{Progress, ProgressBars}; pub struct ProgressOptions { /// Don't show any progress bar #[clap(long, global = true, env = "RUSTIC_NO_PROGRESS")] - #[merge(strategy=merge::bool::overwrite_false)] + #[merge(strategy=conflate::bool::overwrite_false)] pub no_progress: bool, /// Interval to update progress bars @@ -31,6 +31,7 @@ pub struct ProgressOptions { conflicts_with = "no_progress" )] #[serde_as(as = "Option")] + #[merge(strategy = conflate::option::overwrite_none)] pub progress_interval: Option, } diff --git a/src/filtering.rs b/src/filtering.rs index e2ae651..e817a30 100644 --- a/src/filtering.rs +++ b/src/filtering.rs @@ -55,34 +55,35 @@ impl SnapshotFn { } #[serde_as] -#[derive(Clone, Default, Debug, Serialize, Deserialize, merge::Merge, clap::Parser)] +#[derive(Clone, Default, Debug, Serialize, Deserialize, conflate::Merge, clap::Parser)] #[serde(default, rename_all = "kebab-case", deny_unknown_fields)] pub struct SnapshotFilter { /// Hostname to filter (can be specified multiple times) #[clap(long = "filter-host", global = true, value_name = "HOSTNAME")] - #[merge(strategy=merge::vec::overwrite_empty)] + #[merge(strategy=conflate::vec::overwrite_empty)] filter_hosts: Vec, /// Label to filter (can be specified multiple times) #[clap(long = "filter-label", global = true, value_name = "LABEL")] - #[merge(strategy=merge::vec::overwrite_empty)] + #[merge(strategy=conflate::vec::overwrite_empty)] filter_labels: Vec, /// Path list to filter (can be specified multiple times) #[clap(long, global = true, value_name = "PATH[,PATH,..]")] #[serde_as(as = "Vec")] - #[merge(strategy=merge::vec::overwrite_empty)] + #[merge(strategy=conflate::vec::overwrite_empty)] filter_paths: Vec, /// Tag list to filter (can be specified multiple times) #[clap(long, global = true, value_name = "TAG[,TAG,..]")] #[serde_as(as = "Vec")] - #[merge(strategy=merge::vec::overwrite_empty)] + #[merge(strategy=conflate::vec::overwrite_empty)] filter_tags: Vec, /// Function to filter snapshots #[clap(long, global = true, value_name = "FUNC")] #[serde_as(as = "Option")] + #[merge(strategy = conflate::option::overwrite_none)] filter_fn: Option, }