diff --git a/crates/mergify-config/src/simulate.rs b/crates/mergify-config/src/simulate.rs index 511d7d81..ed5bef15 100644 --- a/crates/mergify-config/src/simulate.rs +++ b/crates/mergify-config/src/simulate.rs @@ -119,16 +119,14 @@ pub async fn run(opts: SimulateOptions<'_>, output: &mut dyn Output) -> Result<( } fn emit_result(output: &mut dyn Output, response: &SimulatorResponse) -> std::io::Result<()> { - let title = response.title.clone(); - let summary = response.summary.clone(); output.emit(&(), &mut |w: &mut dyn Write| { - writeln!(w, "{title}")?; + writeln!(w, "{title}", title = response.title)?; writeln!(w)?; // Intentional drift from Python: we print raw Markdown // instead of rich-rendering it. Machine-readable output is // still locked; human rendering is flexible per the compat // contract. - writeln!(w, "{summary}") + writeln!(w, "{summary}", summary = response.summary) }) } diff --git a/crates/mergify-config/src/validate.rs b/crates/mergify-config/src/validate.rs index 4789779c..4ba516f0 100644 --- a/crates/mergify-config/src/validate.rs +++ b/crates/mergify-config/src/validate.rs @@ -129,23 +129,23 @@ fn emit_result( config_path: &Path, errors: &[ValidationError], ) -> std::io::Result<()> { - let path_display = config_path.display().to_string(); - let errors_copy: Vec<(String, String)> = errors - .iter() - .map(|e| (e.path.clone(), e.message.clone())) - .collect(); output.emit(&(), &mut |w: &mut dyn Write| { - if errors_copy.is_empty() { + let path_display = config_path.display(); + if errors.is_empty() { writeln!(w, "Configuration file '{path_display}' is valid.")?; } else { writeln!( w, - "configuration file '{}' has {} error(s):", - path_display, - errors_copy.len(), + "configuration file '{path_display}' has {n} error(s):", + n = errors.len(), )?; - for (path, message) in &errors_copy { - writeln!(w, " - {path}: {message}")?; + for err in errors { + writeln!( + w, + " - {path}: {message}", + path = err.path, + message = err.message, + )?; } } Ok(()) diff --git a/crates/mergify-core/src/output.rs b/crates/mergify-core/src/output.rs index 689e1e2f..a8892566 100644 --- a/crates/mergify-core/src/output.rs +++ b/crates/mergify-core/src/output.rs @@ -42,6 +42,23 @@ pub trait Output { /// no-op to preserve stdout purity; in human mode it writes to /// stderr so piping stdout into a file is unaffected. fn status(&mut self, message: &str) -> io::Result<()>; + + /// Emit a raw [`serde_json::Value`] as the command's only output. + /// + /// Convenience wrapper for the `--json` passthrough commands + /// (`queue status`, `queue show`, `freeze list`, …) — every such + /// command needs the same shape: the value goes out as one JSON + /// document, both when the [`OutputMode`] is `Json` and (via the + /// human closure) when the user passed `--json` but the + /// [`OutputMode`] is still `Human`. Implemented in terms of + /// [`Output::emit`]. + fn emit_json_value(&mut self, value: &serde_json::Value) -> io::Result<()> { + self.emit(value, &mut |w| { + let rendered = serde_json::to_string_pretty(value) + .map_err(|err| io::Error::other(err.to_string()))?; + writeln!(w, "{rendered}") + }) + } } /// `dyn Serialize` cannot be constructed directly because diff --git a/crates/mergify-freeze/src/list.rs b/crates/mergify-freeze/src/list.rs index e205a5e2..e96efafc 100644 --- a/crates/mergify-freeze/src/list.rs +++ b/crates/mergify-freeze/src/list.rs @@ -59,7 +59,7 @@ pub async fn run(opts: ListOptions<'_>, output: &mut dyn Output) -> Result<(), C .unwrap_or_else(|| serde_json::Value::Array(Vec::new())); if opts.output_json { - emit_json(output, &freezes)?; + output.emit_json_value(&freezes)?; return Ok(()); } @@ -69,14 +69,6 @@ pub async fn run(opts: ListOptions<'_>, output: &mut dyn Output) -> Result<(), C Ok(()) } -fn emit_json(output: &mut dyn Output, value: &serde_json::Value) -> std::io::Result<()> { - output.emit(value, &mut |w: &mut dyn Write| { - let rendered = serde_json::to_string_pretty(value) - .map_err(|e| std::io::Error::other(e.to_string()))?; - writeln!(w, "{rendered}") - }) -} - fn emit_human( output: &mut dyn Output, freezes: &[ScheduledFreeze], diff --git a/crates/mergify-queue/src/pause.rs b/crates/mergify-queue/src/pause.rs index 02ea2ed9..201d4140 100644 --- a/crates/mergify-queue/src/pause.rs +++ b/crates/mergify-queue/src/pause.rs @@ -141,14 +141,12 @@ fn confirm(skip: bool, is_tty: bool, repository: &str) -> Result<(), CliError> { } fn emit_confirmation(output: &mut dyn Output, response: &PauseResponse) -> std::io::Result<()> { - let reason = response.reason.clone(); - let paused_at = response.paused_at.clone(); output.emit(&(), &mut |w: &mut dyn Write| { - match &reason { + match &response.reason { Some(r) => write!(w, "Queue paused: \"{r}\"")?, None => write!(w, "Queue paused")?, } - if let Some(ts) = &paused_at { + if let Some(ts) = &response.paused_at { write!(w, " (since {ts})")?; } writeln!(w) diff --git a/crates/mergify-queue/src/show.rs b/crates/mergify-queue/src/show.rs index 81c6a618..add4e700 100644 --- a/crates/mergify-queue/src/show.rs +++ b/crates/mergify-queue/src/show.rs @@ -129,7 +129,7 @@ pub async fn run(opts: ShowOptions<'_>, output: &mut dyn Output) -> Result<(), C }; if opts.output_json { - emit_json(output, &raw)?; + output.emit_json_value(&raw)?; return Ok(()); } @@ -139,14 +139,6 @@ pub async fn run(opts: ShowOptions<'_>, output: &mut dyn Output) -> Result<(), C Ok(()) } -fn emit_json(output: &mut dyn Output, value: &serde_json::Value) -> std::io::Result<()> { - output.emit(value, &mut |w: &mut dyn Write| { - let rendered = serde_json::to_string_pretty(value) - .map_err(|e| std::io::Error::other(e.to_string()))?; - writeln!(w, "{rendered}") - }) -} - fn emit_human(output: &mut dyn Output, view: &PullView, verbose: bool) -> std::io::Result<()> { let now = Utc::now(); let theme = Theme::detect(); @@ -310,14 +302,14 @@ fn print_checks_summary(w: &mut dyn Write, theme: &Theme, checks: &[Check]) -> s write!( w, "{S}{passed} passed{R}", - S = enabled_fg(theme, AnsiColor::Green), + S = theme.fg(AnsiColor::Green), R = theme.reset, )?; if pending > 0 { write!( w, ", {S}{pending} pending{R}", - S = enabled_fg(theme, AnsiColor::Blue), + S = theme.fg(AnsiColor::Blue), R = theme.reset, )?; } @@ -325,7 +317,7 @@ fn print_checks_summary(w: &mut dyn Write, theme: &Theme, checks: &[Check]) -> s write!( w, ", {S}{failed} failed{R}", - S = enabled_fg(theme, AnsiColor::Red), + S = theme.fg(AnsiColor::Red), R = theme.reset, )?; } @@ -356,23 +348,15 @@ fn print_checks_summary(w: &mut dyn Write, theme: &Theme, checks: &[Check]) -> s /// the renderer never crashes on a new API code. fn check_state_glyph(theme: &Theme, state: &str) -> (&'static str, Style) { match state { - "success" => ("✓", enabled_fg(theme, AnsiColor::Green)), - "pending" => ("◌", enabled_fg(theme, AnsiColor::Yellow)), - "failure" | "error" | "action_required" => ("✗", enabled_fg(theme, AnsiColor::Red)), - "timed_out" => ("⏰", enabled_fg(theme, AnsiColor::Red)), + "success" => ("✓", theme.fg(AnsiColor::Green)), + "pending" => ("◌", theme.fg(AnsiColor::Yellow)), + "failure" | "error" | "action_required" => ("✗", theme.fg(AnsiColor::Red)), + "timed_out" => ("⏰", theme.fg(AnsiColor::Red)), "cancelled" | "neutral" | "skipped" | "stale" => ("○", theme.dim), _ => ("?", theme.dim), } } -fn enabled_fg(theme: &Theme, color: AnsiColor) -> Style { - if theme.enabled { - theme.fg(color) - } else { - Style::new() - } -} - fn print_conditions_section( w: &mut dyn Write, theme: &Theme, @@ -394,9 +378,9 @@ fn print_conditions_section( let met = top.iter().filter(|s| s.r#match).count(); let total = top.len(); let style = if met == total { - enabled_fg(theme, AnsiColor::Green) + theme.fg(AnsiColor::Green) } else { - enabled_fg(theme, AnsiColor::Yellow) + theme.fg(AnsiColor::Yellow) }; writeln!( w, @@ -417,7 +401,7 @@ fn print_conditions_section( writeln!( w, " {S}✗{R} {summary}", - S = enabled_fg(theme, AnsiColor::Red), + S = theme.fg(AnsiColor::Red), R = theme.reset, )?; } @@ -466,9 +450,9 @@ fn write_condition_tree( for (i, node) in nodes.iter().enumerate() { let (branch, continuation) = tree::branch_chars(i == last); let (icon, style) = if node.r#match { - ("✓", enabled_fg(theme, AnsiColor::Green)) + ("✓", theme.fg(AnsiColor::Green)) } else { - ("✗", enabled_fg(theme, AnsiColor::Red)) + ("✗", theme.fg(AnsiColor::Red)) }; writeln!( w, diff --git a/crates/mergify-queue/src/status.rs b/crates/mergify-queue/src/status.rs index f5e94a55..b88d0303 100644 --- a/crates/mergify-queue/src/status.rs +++ b/crates/mergify-queue/src/status.rs @@ -137,7 +137,7 @@ pub async fn run(opts: StatusOptions<'_>, output: &mut dyn Output) -> Result<(), let raw: serde_json::Value = client.get(&path).await?; if opts.output_json { - emit_json(output, &raw)?; + output.emit_json_value(&raw)?; } else { let view: StatusView = serde_json::from_value(raw) .map_err(|e| CliError::Generic(format!("decode merge queue status response: {e}")))?; @@ -159,14 +159,6 @@ fn build_path(repository: &str, branch: Option<&str>) -> String { path } -fn emit_json(output: &mut dyn Output, value: &serde_json::Value) -> std::io::Result<()> { - output.emit(value, &mut |w: &mut dyn Write| { - let rendered = serde_json::to_string_pretty(value) - .map_err(|e| std::io::Error::other(e.to_string()))?; - writeln!(w, "{rendered}") - }) -} - fn emit_human(output: &mut dyn Output, repository: &str, view: &StatusView) -> std::io::Result<()> { let now = Utc::now(); let theme = Theme::detect();