use crate::cli::global::GlobalFlags;
use crate::diff::{FileDiff, render_diffs_colored, render_diffs_plain};
use crate::exit;
use crate::tx::engine::ExecutionResult;
use serde::Serialize;
use std::path::Path;
#[derive(Debug, Clone, Copy)]
pub enum WritePhase {
Check(bool),
Applied,
Confirmed(bool),
Preview,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WriteMode {
Check,
Apply,
ConfirmJson,
Preview,
}
#[must_use]
pub fn classify_write_mode(global: &GlobalFlags) -> WriteMode {
if global.check {
WriteMode::Check
} else if global.apply {
WriteMode::Apply
} else if global.confirm && (global.json || global.jsonl) {
WriteMode::ConfirmJson
} else {
WriteMode::Preview
}
}
#[must_use]
pub fn write_exit_code(has_changes: bool, applied: bool) -> u8 {
if applied {
exit::SUCCESS
} else if has_changes {
exit::CHANGES_DETECTED
} else {
exit::SUCCESS
}
}
pub struct WriteMessages<'a> {
pub check: &'a str,
pub apply: &'a str,
pub post_confirm: Option<&'a str>,
}
#[derive(Debug, Clone, Copy)]
pub struct RenderPolicy {
pub preview_diffs: bool,
}
impl Default for RenderPolicy {
fn default() -> Self {
Self {
preview_diffs: true,
}
}
}
pub struct FinalizeCallbacks<OnCheck, OnApply, OnPreview, AfterEmit, AfterApply> {
pub on_check: OnCheck,
pub on_apply: OnApply,
pub on_preview: OnPreview,
pub after_preview_emit: AfterEmit,
pub after_preview_apply: AfterApply,
}
pub fn finalize_report<OnCheck, OnApply, OnPreview, AfterEmit, AfterApply>(
global: &GlobalFlags,
cwd: &Path,
result: ExecutionResult,
preview_diffs: bool,
mut cb: FinalizeCallbacks<OnCheck, OnApply, OnPreview, AfterEmit, AfterApply>,
) -> anyhow::Result<u8>
where
OnCheck: FnMut(&GlobalFlags, bool, &[FileDiff]) -> anyhow::Result<()>,
OnApply: FnMut(&GlobalFlags, bool, &[FileDiff], Option<String>) -> anyhow::Result<()>,
OnPreview: FnMut(&GlobalFlags, bool, &[FileDiff], Option<String>) -> anyhow::Result<()>,
AfterEmit: FnMut(&GlobalFlags),
AfterApply: FnMut(&GlobalFlags),
{
let has_changes = result.has_changes;
match classify_write_mode(global) {
WriteMode::Check => {
let diffs = result.build_diffs();
(cb.on_check)(global, has_changes, &diffs)?;
Ok(write_exit_code(has_changes, false))
}
WriteMode::Apply => {
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
};
(cb.on_apply)(global, has_changes, &diffs, diff_text)?;
Ok(write_exit_code(has_changes, true))
}
WriteMode::ConfirmJson | WriteMode::Preview => {
let diffs = if preview_diffs {
result.build_diffs()
} else {
Vec::new()
};
let diff_text = plain_diff_opt(&diffs);
(cb.on_preview)(global, has_changes, &diffs, diff_text)?;
(cb.after_preview_emit)(global);
let applied = global.should_apply();
if applied {
result.commit()?;
crate::write::run_format_command(global, cwd)?;
(cb.after_preview_apply)(global);
}
Ok(write_exit_code(has_changes, applied))
}
}
}
pub fn finalize_execution_result<T: Serialize>(
global: &GlobalFlags,
cwd: &Path,
result: ExecutionResult,
make_output: impl Fn(WritePhase, Option<String>) -> T,
msgs: WriteMessages<'_>,
render: RenderPolicy,
) -> anyhow::Result<u8> {
let has_changes = result.has_changes;
match classify_write_mode(global) {
WriteMode::Check => {
let output = make_output(WritePhase::Check(has_changes), None);
if !global.emit_json(&output)? && !global.quiet && has_changes {
println!("{}", msgs.check);
}
Ok(write_exit_code(has_changes, false))
}
WriteMode::Apply => {
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!("{}", msgs.apply);
}
}
Ok(write_exit_code(has_changes, true))
}
WriteMode::ConfirmJson => {
let diffs = if render.preview_diffs {
result.build_diffs()
} else {
Vec::new()
};
let diff_text = plain_diff_opt(&diffs);
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)?;
Ok(write_exit_code(has_changes, applied))
}
WriteMode::Preview => {
let diffs = if render.preview_diffs {
result.build_diffs()
} else {
Vec::new()
};
let diff_text = plain_diff_opt(&diffs);
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 has_changes && !global.quiet {
println!("{}", msgs.check);
}
}
let applied = global.should_apply();
if applied {
result.commit()?;
crate::write::run_format_command(global, cwd)?;
if let Some(msg) = msgs.post_confirm
&& global.show_status()
{
eprintln!("{msg}");
}
}
Ok(write_exit_code(has_changes, applied))
}
}
}
pub fn finalize_callback_write<T: Serialize>(
global: &GlobalFlags,
cwd: &Path,
has_changes: bool,
make_output: impl Fn(WritePhase, Option<String>) -> T,
diff_fn: Option<&dyn Fn(bool) -> String>,
mut apply_fn: impl FnMut() -> anyhow::Result<()>,
msgs: WriteMessages<'_>,
) -> anyhow::Result<u8> {
match classify_write_mode(global) {
WriteMode::Check => {
let output = make_output(WritePhase::Check(has_changes), None);
if !global.emit_json(&output)? && !global.quiet && has_changes {
println!("{}", msgs.check);
}
Ok(write_exit_code(has_changes, false))
}
WriteMode::Apply => {
apply_fn()?;
crate::write::run_format_command(global, cwd)?;
let diff_text = if global.diff {
diff_fn.map(|f| f(false))
} else {
None
};
let output = make_output(WritePhase::Applied, diff_text);
if !global.emit_json(&output)? {
if global.diff {
if let Some(f) = diff_fn {
print!("{}", f(global.should_color()));
}
} else if !global.quiet && has_changes {
println!("{}", msgs.apply);
}
}
Ok(write_exit_code(has_changes, true))
}
WriteMode::ConfirmJson => {
let diff_text = diff_fn.map(|f| f(false));
let applied = global.should_apply();
if applied {
apply_fn()?;
crate::write::run_format_command(global, cwd)?;
}
let output = make_output(WritePhase::Confirmed(applied), diff_text);
global.emit_json(&output)?;
Ok(write_exit_code(has_changes, applied))
}
WriteMode::Preview => {
let diff_text = diff_fn.map(|f| f(false));
let output = make_output(WritePhase::Preview, diff_text);
if !global.emit_json(&output)? {
if let Some(f) = diff_fn {
print!("{}", f(global.should_color()));
} else if has_changes && !global.quiet {
println!("{}", msgs.check);
}
}
let applied = global.should_apply();
if applied {
apply_fn()?;
crate::write::run_format_command(global, cwd)?;
if let Some(msg) = msgs.post_confirm
&& global.show_status()
{
eprintln!("{msg}");
}
}
Ok(write_exit_code(has_changes, applied))
}
}
}
fn plain_diff_opt(diffs: &[FileDiff]) -> Option<String> {
if diffs.is_empty() {
None
} else {
Some(render_diffs_plain(diffs))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classify_priority_check_over_apply() {
let g = GlobalFlags {
check: true,
apply: true,
..GlobalFlags::default()
};
assert_eq!(classify_write_mode(&g), WriteMode::Check);
}
#[test]
fn classify_confirm_json() {
let g = GlobalFlags {
confirm: true,
json: true,
..GlobalFlags::default()
};
assert_eq!(classify_write_mode(&g), WriteMode::ConfirmJson);
}
#[test]
fn classify_default_preview() {
assert_eq!(
classify_write_mode(&GlobalFlags::default()),
WriteMode::Preview
);
}
#[test]
fn exit_code_matrix() {
assert_eq!(write_exit_code(true, true), exit::SUCCESS);
assert_eq!(write_exit_code(false, true), exit::SUCCESS);
assert_eq!(write_exit_code(true, false), exit::CHANGES_DETECTED);
assert_eq!(write_exit_code(false, false), exit::SUCCESS);
}
#[test]
fn finalize_report_check_does_not_commit() {
use crate::plan::Operation;
use crate::tx::engine::{ExecuteOptions, WriteRequest, WriteSource, stage};
use std::sync::atomic::{AtomicUsize, Ordering};
let dir = tempfile::TempDir::new().unwrap();
let mut global = GlobalFlags::test_with_cwd(dir.path());
global.check = true;
let options = ExecuteOptions::from_global(dir.path(), &global, None);
let report = stage(WriteRequest {
source: WriteSource::Operations(vec![Operation::FileCreate {
path: "f.txt".to_string(),
content: "x\n".to_string(),
force: None,
}]),
options,
})
.unwrap();
let checks = AtomicUsize::new(0);
let code = finalize_report(
&global,
dir.path(),
report,
true,
FinalizeCallbacks {
on_check: |_g: &GlobalFlags, has: bool, _d: &[FileDiff]| {
assert!(has);
checks.fetch_add(1, Ordering::SeqCst);
Ok(())
},
on_apply: |_g: &GlobalFlags, _: bool, _: &[FileDiff], _: Option<String>| {
panic!("apply must not run")
},
on_preview: |_g: &GlobalFlags, _: bool, _: &[FileDiff], _: Option<String>| {
panic!("preview must not run")
},
after_preview_emit: |_: &GlobalFlags| {},
after_preview_apply: |_: &GlobalFlags| {},
},
)
.unwrap();
assert_eq!(code, exit::CHANGES_DETECTED);
assert_eq!(checks.load(Ordering::SeqCst), 1);
assert!(!dir.path().join("f.txt").exists());
}
#[test]
fn finalize_report_apply_commits_and_runs_apply_hook() {
use crate::plan::Operation;
use crate::tx::engine::{ExecuteOptions, WriteRequest, WriteSource, stage};
use std::sync::atomic::{AtomicBool, Ordering};
let dir = tempfile::TempDir::new().unwrap();
let mut global = GlobalFlags::test_with_cwd(dir.path());
global.apply = true;
let options = ExecuteOptions::from_global(dir.path(), &global, None);
let report = stage(WriteRequest {
source: WriteSource::Operations(vec![Operation::FileCreate {
path: "applied.txt".to_string(),
content: "ok\n".to_string(),
force: None,
}]),
options,
})
.unwrap();
let applied_hook = AtomicBool::new(false);
let code = finalize_report(
&global,
dir.path(),
report,
true,
FinalizeCallbacks {
on_check: |_g: &GlobalFlags, _: bool, _: &[FileDiff]| panic!("check must not run"),
on_apply: |_g: &GlobalFlags,
has: bool,
diffs: &[FileDiff],
_plain: Option<String>| {
assert!(has);
assert!(!diffs.is_empty());
applied_hook.store(true, Ordering::SeqCst);
Ok(())
},
on_preview: |_g: &GlobalFlags, _: bool, _: &[FileDiff], _: Option<String>| {
panic!("preview must not run")
},
after_preview_emit: |_: &GlobalFlags| {},
after_preview_apply: |_: &GlobalFlags| {},
},
)
.unwrap();
assert_eq!(code, exit::SUCCESS);
assert!(applied_hook.load(Ordering::SeqCst));
assert_eq!(
std::fs::read_to_string(dir.path().join("applied.txt")).unwrap(),
"ok\n"
);
}
#[test]
fn finalize_report_preview_does_not_commit_without_apply() {
use crate::plan::Operation;
use crate::tx::engine::{ExecuteOptions, WriteRequest, WriteSource, stage};
let dir = tempfile::TempDir::new().unwrap();
let global = GlobalFlags::test_with_cwd(dir.path());
let options = ExecuteOptions::from_global(dir.path(), &global, None);
let report = stage(WriteRequest {
source: WriteSource::Operations(vec![Operation::FileCreate {
path: "preview.txt".to_string(),
content: "p\n".to_string(),
force: None,
}]),
options,
})
.unwrap();
let code = finalize_report(
&global,
dir.path(),
report,
true,
FinalizeCallbacks {
on_check: |_g: &GlobalFlags, _: bool, _: &[FileDiff]| panic!("check must not run"),
on_apply: |_g: &GlobalFlags, _: bool, _: &[FileDiff], _: Option<String>| {
panic!("apply must not run")
},
on_preview: |_g: &GlobalFlags, has: bool, _d: &[FileDiff], _p: Option<String>| {
assert!(has);
Ok(())
},
after_preview_emit: |_: &GlobalFlags| {},
after_preview_apply: |_: &GlobalFlags| {},
},
)
.unwrap();
assert_eq!(code, exit::CHANGES_DETECTED);
assert!(!dir.path().join("preview.txt").exists());
}
}