mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
109 lines
3.3 KiB
Rust
109 lines
3.3 KiB
Rust
#![expect(
|
|
clippy::disallowed_types,
|
|
reason = "sync CLI `run diff` command: blocking std::io::Write is the intended output mechanism"
|
|
)]
|
|
#![expect(
|
|
clippy::disallowed_methods,
|
|
reason = "sync CLI `run diff` command: writes diff output to std::io::stdout directly"
|
|
)]
|
|
|
|
use std::io::{self, IsTerminal, Write};
|
|
|
|
use anyhow::{Context, Result, bail};
|
|
use tracing::{debug, info};
|
|
|
|
use crate::args::DiffArgs;
|
|
use crate::command_context::CommandContext;
|
|
use crate::server_client::RunProjection;
|
|
use crate::shared::print_json_pretty;
|
|
|
|
pub(crate) async fn run(args: DiffArgs, base_ctx: &CommandContext) -> Result<()> {
|
|
info!(run_id = %args.run, "Showing diff");
|
|
let ctx = base_ctx.with_target(&args.server)?;
|
|
let client = ctx.server().await?;
|
|
let run_id = client.resolve_run(&args.run).await?.id;
|
|
let state = client.get_run_state(&run_id).await?;
|
|
|
|
let patch = resolve_diff(&state, &args)?;
|
|
|
|
if ctx.json_output() {
|
|
let value = serde_json::json!({
|
|
"run_id": run_id,
|
|
"node": args.node,
|
|
"diff": patch,
|
|
});
|
|
print_json_pretty(&value)?;
|
|
return Ok(());
|
|
}
|
|
|
|
let is_tty = io::stdout().is_terminal();
|
|
let mut stdout = io::stdout().lock();
|
|
if is_tty {
|
|
for line in patch.lines() {
|
|
writeln!(stdout, "{}", colorize_diff_line(line))?;
|
|
}
|
|
} else {
|
|
stdout.write_all(patch.as_bytes())?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn resolve_diff(state: &RunProjection, args: &DiffArgs) -> Result<String> {
|
|
if let Some(ref node_id) = args.node {
|
|
if let Some(visit) = state.list_node_visits(node_id).into_iter().max() {
|
|
if let Some(node) = state.stage(&fabro_store::StageId::new(node_id, visit)) {
|
|
if let Some(patch) = node.diff.clone() {
|
|
debug!(node_id, visit, "Reading per-node diff from projected state");
|
|
return Ok(patch);
|
|
}
|
|
}
|
|
}
|
|
|
|
bail!("No diff found for node '{node_id}' — check the node ID and try again");
|
|
}
|
|
|
|
let start = state
|
|
.start
|
|
.clone()
|
|
.context("Failed to load start record from store")?;
|
|
|
|
let base_sha = start
|
|
.base_sha
|
|
.as_deref()
|
|
.ok_or_else(|| anyhow::anyhow!("This run was not git-checkpointed; no diff available"))?;
|
|
|
|
if let Some(patch) = state
|
|
.conclusion
|
|
.as_ref()
|
|
.and_then(|conclusion| conclusion.diff.patch.clone())
|
|
{
|
|
debug!("Reading stored diff from run state");
|
|
return Ok(patch);
|
|
}
|
|
|
|
if state.conclusion.is_some() {
|
|
bail!(
|
|
"Run completed but no stored diff exists — the run may not have produced any changes"
|
|
);
|
|
}
|
|
|
|
bail!(
|
|
"Run is missing stored diff output since base commit {base_sha}; live sandbox diff is no longer supported"
|
|
)
|
|
}
|
|
|
|
fn colorize_diff_line(line: &str) -> String {
|
|
if line.starts_with("+++") || line.starts_with("---") {
|
|
format!("\x1b[1m{line}\x1b[0m")
|
|
} else if line.starts_with('+') {
|
|
format!("\x1b[32m{line}\x1b[0m")
|
|
} else if line.starts_with('-') {
|
|
format!("\x1b[31m{line}\x1b[0m")
|
|
} else if line.starts_with("@@") {
|
|
format!("\x1b[36m{line}\x1b[0m")
|
|
} else if line.starts_with("diff ") {
|
|
format!("\x1b[1m{line}\x1b[0m")
|
|
} else {
|
|
line.to_string()
|
|
}
|
|
}
|