use crate::cli::global::GlobalFlags;
use crate::diff::{render_diffs_colored, render_diffs_plain};
use crate::exit;
use crate::tx::engine::{ExecuteOptions, execute_single};
use serde::Serialize;
#[derive(Debug, Clone, Copy)]
pub enum WritePhase {
Check(bool),
Applied,
Confirmed(bool),
Preview,
}
pub fn execute_via_engine<T: Serialize>(
op: crate::plan::Operation,
global: &GlobalFlags,
make_output: impl Fn(WritePhase, Option<String>) -> T,
check_msg: &str,
apply_msg: &str,
) -> anyhow::Result<u8> {
execute_via_engine_inner(op, global, make_output, check_msg, apply_msg, true)
}
pub fn execute_via_engine_no_preview_diffs<T: Serialize>(
op: crate::plan::Operation,
global: &GlobalFlags,
make_output: impl Fn(WritePhase, Option<String>) -> T,
check_msg: &str,
apply_msg: &str,
) -> anyhow::Result<u8> {
execute_via_engine_inner(op, global, make_output, check_msg, apply_msg, false)
}
fn execute_via_engine_inner<T: Serialize>(
op: crate::plan::Operation,
global: &GlobalFlags,
make_output: impl Fn(WritePhase, Option<String>) -> T,
check_msg: &str,
apply_msg: &str,
preview_diffs: bool,
) -> anyhow::Result<u8> {
let cwd = global.resolve_cwd()?;
let options = ExecuteOptions {
cwd: &cwd,
global,
guard: None,
};
let result = execute_single(op, options)?;
if global.check {
let output = make_output(WritePhase::Check(result.has_changes), None);
if !global.emit_json(&output)? && !global.quiet && result.has_changes {
println!("{check_msg}");
}
return if result.has_changes {
Ok(exit::CHANGES_DETECTED)
} else {
Ok(exit::SUCCESS)
};
}
if global.apply {
let has_changes = result.has_changes;
let diffs = result.build_diffs();
result.commit()?;
crate::write::run_format_command(global, &cwd)?;
let diff_text = if global.diff {
Some(render_diffs_plain(&diffs))
} else {
None
};
let output = make_output(WritePhase::Applied, diff_text);
if !global.emit_json(&output)? && has_changes {
if global.diff {
print!("{}", render_diffs_colored(&diffs, global.should_color()));
} else if !global.quiet {
println!("{apply_msg}");
}
}
return Ok(exit::SUCCESS);
}
let diffs = if preview_diffs {
result.build_diffs()
} else {
Vec::new()
};
let diff_text = if !diffs.is_empty() {
Some(render_diffs_plain(&diffs))
} else {
None
};
if global.confirm && (global.json || global.jsonl) {
let applied = global.should_apply();
if applied {
result.commit()?;
crate::write::run_format_command(global, &cwd)?;
}
let output = make_output(WritePhase::Confirmed(applied), diff_text);
global.emit_json(&output)?;
return Ok(exit::SUCCESS);
}
let output = make_output(WritePhase::Preview, diff_text);
if !global.emit_json(&output)? {
if !diffs.is_empty() {
print!("{}", render_diffs_colored(&diffs, global.should_color()));
} else if result.has_changes && !global.quiet {
println!("{check_msg}");
}
}
if global.should_apply() {
result.commit()?;
crate::write::run_format_command(global, &cwd)?;
}
Ok(exit::SUCCESS)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::plan::Operation;
use std::fs;
use tempfile::TempDir;
#[derive(Debug, Serialize)]
struct TestOutput {
ok: bool,
path: String,
applied: Option<bool>,
}
#[test]
fn execute_via_engine_create_apply() {
let dir = TempDir::new().unwrap();
let mut global = GlobalFlags::test_with_cwd(dir.path());
global.apply = true;
let op = Operation::FileCreate {
path: "new.txt".to_string(),
content: "hello\n".to_string(),
force: None,
};
let code = execute_via_engine(
op,
&global,
|phase, _diff| TestOutput {
ok: true,
path: "new.txt".to_string(),
applied: match phase {
WritePhase::Applied => Some(true),
WritePhase::Confirmed(a) => Some(a),
_ => None,
},
},
"would create new.txt",
"created new.txt",
)
.unwrap();
assert_eq!(code, exit::SUCCESS);
assert_eq!(
fs::read_to_string(dir.path().join("new.txt")).unwrap(),
"hello\n"
);
}
#[test]
fn execute_via_engine_create_check() {
let dir = TempDir::new().unwrap();
let mut global = GlobalFlags::test_with_cwd(dir.path());
global.check = true;
let op = Operation::FileCreate {
path: "new.txt".to_string(),
content: "hello\n".to_string(),
force: None,
};
let code = execute_via_engine(
op,
&global,
|_phase, _diff| TestOutput {
ok: true,
path: "new.txt".to_string(),
applied: None,
},
"would create new.txt",
"created new.txt",
)
.unwrap();
assert_eq!(code, exit::CHANGES_DETECTED);
assert!(!dir.path().join("new.txt").exists());
}
#[test]
fn execute_via_engine_delete_apply() {
let dir = TempDir::new().unwrap();
let file = dir.path().join("doomed.txt");
fs::write(&file, "bye\n").unwrap();
let mut global = GlobalFlags::test_with_cwd(dir.path());
global.apply = true;
let op = Operation::FileDelete {
path: "doomed.txt".to_string(),
};
let code = execute_via_engine(
op,
&global,
|phase, _diff| TestOutput {
ok: true,
path: "doomed.txt".to_string(),
applied: match phase {
WritePhase::Applied => Some(true),
_ => None,
},
},
"would delete doomed.txt",
"deleted doomed.txt",
)
.unwrap();
assert_eq!(code, exit::SUCCESS);
assert!(!file.exists());
}
#[test]
fn execute_via_engine_append_apply() {
let dir = TempDir::new().unwrap();
let file = dir.path().join("log.txt");
fs::write(&file, "line1\n").unwrap();
let mut global = GlobalFlags::test_with_cwd(dir.path());
global.apply = true;
let op = Operation::FileAppend {
path: "log.txt".to_string(),
content: "line2\n".to_string(),
};
let code = execute_via_engine(
op,
&global,
|_phase, _diff| TestOutput {
ok: true,
path: "log.txt".to_string(),
applied: None,
},
"would append to log.txt",
"appended to log.txt",
)
.unwrap();
assert_eq!(code, exit::SUCCESS);
assert_eq!(fs::read_to_string(&file).unwrap(), "line1\nline2\n");
}
#[test]
fn execute_via_engine_preview_mode() {
let dir = TempDir::new().unwrap();
let global = GlobalFlags::test_with_cwd(dir.path());
let op = Operation::FileCreate {
path: "preview.txt".to_string(),
content: "data\n".to_string(),
force: None,
};
let code = execute_via_engine(
op,
&global,
|_phase, _diff| TestOutput {
ok: true,
path: "preview.txt".to_string(),
applied: None,
},
"would create preview.txt",
"created preview.txt",
)
.unwrap();
assert_eq!(code, exit::SUCCESS);
assert!(!dir.path().join("preview.txt").exists());
}
}