From 61f35f5ae469d49e452d25c33d10a9f040e6fcf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Jane=C5=BEi=C4=8D?= Date: Tue, 8 Sep 2026 14:16:08 +0000 Subject: [PATCH] feat: silence progress with --quiet --- src/cli/ahab.rs | 4 ++++ src/cmd/compose.rs | 46 +++++++++++++++++++++----------------- src/commands/django.rs | 8 +++---- src/commands/link.rs | 6 ++--- src/commands/link/store.rs | 5 ++++- src/commands/postgres.rs | 29 ++++++++++++++---------- src/ctx.rs | 1 + src/main.rs | 3 ++- src/output.rs | 10 ++++++--- 9 files changed, 67 insertions(+), 45 deletions(-) diff --git a/src/cli/ahab.rs b/src/cli/ahab.rs index e1f5feb..61a9a4a 100644 --- a/src/cli/ahab.rs +++ b/src/cli/ahab.rs @@ -16,6 +16,10 @@ pub struct Ahab { #[arg(short, long, global = true)] pub verbose: bool, + /// Print only what was asked for, not the progress along the way + #[arg(short, long, global = true, conflicts_with = "verbose")] + pub quiet: bool, + /// Print the docker commands that would run, without running them #[arg(long, global = true)] pub dry_run: bool, diff --git a/src/cmd/compose.rs b/src/cmd/compose.rs index 77f55c5..d06313f 100644 --- a/src/cmd/compose.rs +++ b/src/cmd/compose.rs @@ -1,7 +1,13 @@ use super::{Argv, Cmd}; -fn compose() -> Argv { - Argv::new("docker").arg("compose") +// compose prints its own progress, which --quiet has to ask it to stop +fn compose(quiet: bool) -> Argv { + let argv = Argv::new("docker").arg("compose"); + + match quiet { + true => argv.flag("--progress", "quiet"), + false => argv, + } } // docker compose run --rm, which runs the image's entrypoint and so fixes up the @@ -22,7 +28,7 @@ impl Run { impl Cmd for Run { fn argv(&self) -> Argv { - compose() + compose(false) .arg("run") .arg("--rm") .arg(&self.service) @@ -35,7 +41,7 @@ pub struct Config; impl Cmd for Config { fn argv(&self) -> Argv { - compose().arg("config").flag("--format", "json") + compose(false).arg("config").flag("--format", "json") } } @@ -54,39 +60,37 @@ impl Ps { impl Cmd for Ps { fn argv(&self) -> Argv { - compose().arg("ps").arg("--quiet").arg(&self.service) + compose(false).arg("ps").arg("--quiet").arg(&self.service) } } -pub struct Up; +pub struct Up { + pub quiet: bool, +} impl Cmd for Up { fn argv(&self) -> Argv { - compose().arg("up").arg("--detach") + compose(self.quiet).arg("up").arg("--detach") } } -pub struct Start { - service: String, +pub struct Start<'a> { + pub service: &'a str, + pub quiet: bool, } -impl Start { - pub fn service(service: &str) -> Self { - Self { - service: service.to_string(), - } - } -} - -impl Cmd for Start { +impl Cmd for Start<'_> { fn argv(&self) -> Argv { - compose().arg("start").arg(&self.service) + compose(self.quiet).arg("start").arg(self.service) } } -pub struct Stop; + +pub struct Stop { + pub quiet: bool, +} impl Cmd for Stop { fn argv(&self) -> Argv { - compose().arg("stop") + compose(self.quiet).arg("stop") } } diff --git a/src/commands/django.rs b/src/commands/django.rs index 704e200..997c3d4 100644 --- a/src/commands/django.rs +++ b/src/commands/django.rs @@ -24,7 +24,7 @@ pub fn make_command(ctx: &Ctx, app: &PathBuf, name: &str) -> Result<()> { return Err(anyhow!("directory {app_name} does not exist")); } - note!("found app {app_name}"); + note!(ctx, "found app {app_name}"); let management_dir = app_dir.join("management"); @@ -32,7 +32,7 @@ pub fn make_command(ctx: &Ctx, app: &PathBuf, name: &str) -> Result<()> { create_dir(ctx, &management_dir)?; touch(ctx, &management_dir.join("__init__.py"))?; - note!("created module {app_name}.management") + note!(ctx, "created module {app_name}.management") }; let commands_dir = management_dir.join("commands"); @@ -41,7 +41,7 @@ pub fn make_command(ctx: &Ctx, app: &PathBuf, name: &str) -> Result<()> { create_dir(ctx, &commands_dir)?; touch(ctx, &commands_dir.join("__init__.py"))?; - note!("created module {app_name}.management.commands") + note!(ctx, "created module {app_name}.management.commands") }; write_new( @@ -50,7 +50,7 @@ pub fn make_command(ctx: &Ctx, app: &PathBuf, name: &str) -> Result<()> { DEBUG_TEMPLATE.as_bytes(), )?; - note!("created command {app_name}.management.commands.{name}"); + note!(ctx, "created command {app_name}.management.commands.{name}"); Ok(()) } diff --git a/src/commands/link.rs b/src/commands/link.rs index d2a91df..e7533d1 100644 --- a/src/commands/link.rs +++ b/src/commands/link.rs @@ -28,7 +28,7 @@ pub fn add(ctx: &Ctx, paths: &[PathBuf], force: bool, store: Option<&Path>) -> R let mut failed = 0; for path in paths { if let Err(e) = link_one(ctx, &repo, path, force, &report) { - note!("error: {e:#}"); + note!(ctx, "error: {e:#}"); failed += 1; } } @@ -52,7 +52,7 @@ pub fn restore(ctx: &Ctx, paths: &[PathBuf], all: bool, store: Option<&Path>) -> }; if paths.is_empty() { - note!("nothing in the store for this repository"); + note!(ctx, "nothing in the store for this repository"); return Ok(()); } @@ -63,7 +63,7 @@ pub fn restore(ctx: &Ctx, paths: &[PathBuf], all: bool, store: Option<&Path>) -> let mut failed = 0; for path in &paths { if let Err(e) = restore_one(ctx, &repo, path, &report) { - note!("error: {e:#}"); + note!(ctx, "error: {e:#}"); failed += 1; } } diff --git a/src/commands/link/store.rs b/src/commands/link/store.rs index 5683c22..ef7c89b 100644 --- a/src/commands/link/store.rs +++ b/src/commands/link/store.rs @@ -86,7 +86,10 @@ fn repo_components(ctx: &Ctx, root: &Path) -> Result { if let Some(components) = components_from_remote(&url) { return Ok(components); } - note!("could not parse git remote `{url}`, falling back to the checkout name"); + note!( + ctx, + "could not parse git remote `{url}`, falling back to the checkout name" + ); } let name = root diff --git a/src/commands/postgres.rs b/src/commands/postgres.rs index 3ef52cf..8a5fe1a 100644 --- a/src/commands/postgres.rs +++ b/src/commands/postgres.rs @@ -45,11 +45,12 @@ fn restore_cluster(ctx: &Ctx, db: &Database, restore: &dyn Cmd, file: &Path) -> continue; } - note!("{line}"); + note!(ctx, "{line}"); } if existing > 0 { note!( + ctx, "left {existing} existing role{} alone", if existing == 1 { "" } else { "s" } ); @@ -66,11 +67,15 @@ pub fn import(ctx: &Ctx, file: &Path) -> Result<()> { let dump = Dump::of(file)?; let db = Database::resolve(ctx)?; - note!("stopping all containers"); - Stop.run(ctx)?; + note!(ctx, "stopping all containers"); + Stop { quiet: ctx.quiet }.run(ctx)?; - note!("starting db container"); - Start::service(&db.service).run(ctx)?; + note!(ctx, "starting db container"); + Start { + service: &db.service, + quiet: ctx.quiet, + } + .run(ctx)?; let remote = remote_dump(); @@ -121,7 +126,7 @@ pub fn import(ctx: &Ctx, file: &Path) -> Result<()> { let restore = db.restore_with(kind); // the name of what actually runs, so the message cannot drift from it let tool = restore.argv().program().to_string(); - note!("restoring database with {tool}"); + note!(ctx, "restoring database with {tool}"); when_ready( ctx, @@ -149,7 +154,7 @@ pub fn import(ctx: &Ctx, file: &Path) -> Result<()> { wait_until_ready(ctx, &db)?; if ctx.dry_run { - note!("would restore with {tool}"); + note!(ctx, "would restore with {tool}"); } else { // a directory dump was copied in whole, so pg_restore reads it from the // container; every other shape is fed in on stdin, through gunzip when it @@ -190,9 +195,9 @@ pub fn import(ctx: &Ctx, file: &Path) -> Result<()> { let _ = Rm::recursive(&remote).in_container(&db.container).run(ctx); } - note!("restarting containers"); - Stop.run(ctx)?; - Up.run(ctx)?; + note!(ctx, "restarting containers"); + Stop { quiet: ctx.quiet }.run(ctx)?; + Up { quiet: ctx.quiet }.run(ctx)?; Ok(()) } @@ -208,7 +213,7 @@ pub fn dump(ctx: &Ctx, file: &Path, format: Format, gzip: bool) -> Result<()> { return dump_directory(ctx, &db, file); } - note!("dumping to local file {}", file.to_string_lossy()); + note!(ctx, "dumping to local file {}", file.to_string_lossy()); // written beside the target and renamed once the dump succeeds, so a failure // cannot destroy the dump that is already there @@ -259,7 +264,7 @@ fn dump_directory(ctx: &Ctx, db: &Database, target: &Path) -> Result<()> { ); } - note!("dumping to local directory {}", target.display()); + note!(ctx, "dumping to local directory {}", target.display()); let remote = remote_dump(); PgDump::new(&db.user, &db.name, "d") diff --git a/src/ctx.rs b/src/ctx.rs index 38e7303..1099bc6 100644 --- a/src/ctx.rs +++ b/src/ctx.rs @@ -2,4 +2,5 @@ pub struct Ctx { pub verbose: bool, pub dry_run: bool, + pub quiet: bool, } diff --git a/src/main.rs b/src/main.rs index b40d113..34fa775 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,12 +20,13 @@ fn main() -> ExitCode { let ctx = Ctx { verbose: args.verbose, dry_run: args.dry_run, + quiet: args.quiet, }; // said once here rather than by each command, so every line that follows // reads as the plan it is if ctx.dry_run { - note!("dry run, nothing will be changed"); + note!(ctx, "dry run, nothing will be changed"); } match run(&ctx, args.command) { diff --git a/src/output.rs b/src/output.rs index 7ef0543..5b7e7c5 100644 --- a/src/output.rs +++ b/src/output.rs @@ -1,9 +1,13 @@ -// progress and warnings go to stderr, so what a caller pipes is only ever the -// data a command was asked for +// progress, on stderr and only when it was asked for macro_rules! note { - ($($arg:tt)*) => { eprintln!($($arg)*) }; + ($ctx:expr, $($arg:tt)*) => { + if !$ctx.quiet { + eprintln!($($arg)*) + } + }; } +// not progress: it speaks about the result, so --quiet keeps it macro_rules! warning { ($($arg:tt)*) => { eprintln!("warning: {}", format_args!($($arg)*)) }; }