diff --git a/src/cli/ahab.rs b/src/cli/ahab.rs index 20d9c73..38d443a 100644 --- a/src/cli/ahab.rs +++ b/src/cli/ahab.rs @@ -1,6 +1,7 @@ use super::{Django, Link, Postgres}; use clap::builder::styling::{AnsiColor, Effects, Styles}; -use clap::{Parser, Subcommand}; +use clap::error::ErrorKind; +use clap::{CommandFactory, Parser, Subcommand}; use clap_complete::Shell; /// A program for interacting with various dockerized applications @@ -55,13 +56,63 @@ fn help_styles() -> Styles { .placeholder(AnsiColor::Cyan.on_default()) } +/// Exit with a usage error when `--dry-run` would be a lie +pub fn reject_unsupported_dry_run(command: &Commands) { + if let Some(name) = writes_locally(command) { + Ahab::command() + .error( + ErrorKind::ArgumentConflict, + format!("--dry-run is not supported by `{name}`, which writes to the working tree"), + ) + .exit() + } +} + +// TODO:(@janezicmatej) honour --dry-run in these commands instead of refusing it +fn writes_locally(command: &Commands) -> Option<&'static str> { + match command { + Commands::Link { command } => match command { + Link::Add { .. } => Some("link add"), + Link::Restore { .. } => Some("link restore"), + Link::Check { .. } => None, + }, + Commands::Django { command } => match command { + Django::MakeCommand { .. } => Some("django make-command"), + _ => None, + }, + Commands::Postgres { .. } | Commands::Completions { .. } => None, + } +} + #[cfg(test)] mod tests { use super::Ahab; - use clap::CommandFactory; + use clap::{CommandFactory, Parser}; #[test] fn the_command_is_well_formed() { Ahab::command().debug_assert(); } + + #[test] + fn dry_run_is_refused_only_where_it_cannot_be_honoured() { + let command = |args: &[&str]| Ahab::try_parse_from(args).unwrap().command; + + assert_eq!( + super::writes_locally(&command(&["ahab", "link", "add", ".env"])), + Some("link add") + ); + assert_eq!( + super::writes_locally(&command(&["ahab", "django", "make-command", "app", "name"])), + Some("django make-command") + ); + assert_eq!( + super::writes_locally(&command(&["ahab", "link", "check"])), + None + ); + assert_eq!( + super::writes_locally(&command(&["ahab", "postgres", "dump", "out.sql"])), + None + ); + } } diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 1957def..15dc00a 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -3,7 +3,7 @@ mod django; mod link; mod postgres; -pub use ahab::{Ahab, Commands}; +pub use ahab::{Ahab, Commands, reject_unsupported_dry_run}; pub use django::Django; pub use link::{Link, Store}; pub use postgres::{Format, Postgres}; diff --git a/src/main.rs b/src/main.rs index 7ec9aac..e4945d3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,6 +13,10 @@ fn main() -> ExitCode { dry_run: args.dry_run, }); + if args.dry_run { + cli::reject_unsupported_dry_run(&args.command); + } + match run(args.command) { Ok(code) => code, Err(e) => { diff --git a/src/scripts/postgres.rs b/src/scripts/postgres.rs index 3bb73f5..f5b4d76 100644 --- a/src/scripts/postgres.rs +++ b/src/scripts/postgres.rs @@ -387,9 +387,14 @@ pub fn dump(file: &PathBuf, format: Format, gzip: bool) -> Result<()> { // written beside the target and renamed once the dump succeeds, so a failure // cannot destroy the dump that is already there let partial = suffixed(file, ".partial"); - let stdout = Stdio::from( - File::create(&partial).with_context(|| format!("creating {}", partial.display()))?, - ); + // a dry run produces no dump, so it must not lay a hand on the target either + let stdout = if command_builder::is_dry_run() { + Stdio::null() + } else { + Stdio::from( + File::create(&partial).with_context(|| format!("creating {}", partial.display()))?, + ) + }; let dumped = if gzip { // the whole pipeline has to arrive as one shell argument @@ -417,6 +422,10 @@ pub fn dump(file: &PathBuf, format: Format, gzip: bool) -> Result<()> { return Err(e); } + if command_builder::is_dry_run() { + return Ok(()); + } + fs::rename(&partial, file) .with_context(|| format!("renaming {} to {}", partial.display(), file.display()))?;