From dd5a11ae93633fe69bf222d2596967c0be30e93e Mon Sep 17 00:00:00 2001 From: Scott Linder Date: Tue, 11 Aug 2026 12:02:16 -0400 Subject: [PATCH] add verbosity levels Change-Id: I7421c5f5841d77789572ced48f3ab689a0c8f39d --- src/cli.rs | 23 ++++++++++++++++++++--- src/env.rs | 4 ++-- src/util.rs | 50 +++++++++++++++++++++++++++++++++++++++++++------- 3 files changed, 65 insertions(+), 12 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 34c5c2e..4775208 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,5 +1,14 @@ use clap::{ArgAction, Args, Parser, Subcommand}; +fn parse_verbosity(value: &str) -> Result { + match value { + "0" => Ok(0), + "1" => Ok(1), + "2" | "v" => Ok(2), + _ => Err("verbosity must be 0, 1, or 2/v".into()), + } +} + /// GitHub stacked-PR builder for those who miss Gerrit /// /// Main features: @@ -77,9 +86,17 @@ pub struct Globals { /// which have the potential to mutate remote state are skipped and printed. #[arg(short = '#', long, global = true)] pub dry_run: bool, - /// Output all commands executed, and their stdout/stderr. - #[arg(short, long, global = true)] - pub verbose: bool, + /// Output commands executed. Repeat for command output as well. + #[arg( + short, + long, + global = true, + value_parser = parse_verbosity, + num_args = 0..=1, + default_missing_value = "1", + default_value = "0" + )] + pub verbose: u8, } #[derive(Subcommand)] diff --git a/src/env.rs b/src/env.rs index d490757..5334902 100644 --- a/src/env.rs +++ b/src/env.rs @@ -94,12 +94,12 @@ impl Env { self.cli.globals.dry_run } - pub fn verbose(&self) -> bool { + pub fn verbosity(&self) -> u8 { self.cli.globals.verbose } pub fn always_echo(&self) -> bool { - self.dry_run() || self.verbose() + self.dry_run() || self.verbosity() > 0 } pub fn next_exec_id(&self) -> usize { diff --git a/src/util.rs b/src/util.rs index 9b4286f..f22cc41 100644 --- a/src/util.rs +++ b/src/util.rs @@ -5,6 +5,37 @@ use anyhow::{Context, Result, bail}; use std::fmt::Debug; use std::process::{Command, Output}; +const MAX_VERBOSE_LINE_BYTES: usize = 100; + +fn truncate_line(line: &str) -> String { + if line.len() <= MAX_VERBOSE_LINE_BYTES { + return line.to_owned(); + } + let marker = "[...]"; + let prefix_bytes = (MAX_VERBOSE_LINE_BYTES - marker.len()) / 2; + let suffix_bytes = MAX_VERBOSE_LINE_BYTES - marker.len() - prefix_bytes; + let mut prefix_end = prefix_bytes; + while !line.is_char_boundary(prefix_end) { + prefix_end -= 1; + } + let mut suffix_start = line.len() - suffix_bytes; + while !line.is_char_boundary(suffix_start) { + suffix_start += 1; + } + format!("{}{}{}", &line[..prefix_end], marker, &line[suffix_start..]) +} + +fn print_output(prefix: &str, output: &[u8], truncate: bool) { + for line in String::from_utf8_lossy(output).lines() { + let line = if truncate { + truncate_line(line) + } else { + line.to_owned() + }; + eprintln!("{prefix}{line}"); + } +} + pub fn exec_impl(env: &crate::env::Env, cmd: &mut Command) -> Result { let id = env.next_exec_id(); if env.always_echo() { @@ -13,13 +44,18 @@ pub fn exec_impl(env: &crate::env::Env, cmd: &mut Command) -> Result { let output = cmd .output() .with_context(|| format!("exec-failed: {:?}", cmd))?; - if env.always_echo() || !output.status.success() { - for line in String::from_utf8_lossy(output.stdout.as_ref()).lines() { - eprintln!("exec-{}-stdout: {}", id, line); - } - for line in String::from_utf8_lossy(output.stderr.as_ref()).lines() { - eprintln!("exec-{}-stderr: {}", id, line); - } + if env.dry_run() || env.verbosity() > 0 || !output.status.success() { + let truncate = env.verbosity() == 1; + print_output( + &format!("exec-{id}-stdout: "), + output.stdout.as_ref(), + truncate, + ); + print_output( + &format!("exec-{id}-stderr: "), + output.stderr.as_ref(), + truncate, + ); } if !output.status.success() { bail!("exec-{}-status-non-zero: {:?}", id, output.status);