use std::io::Write;
use weave_cli::patch::{self, ApplyMode, PatchError};
type R<T> = Result<T, Box<dyn std::error::Error>>;
fn emit(out: Option<&str>, content: &str) -> R<()> {
match out {
Some(p) => std::fs::write(p, content)?,
None => {
let mut stdout = std::io::stdout();
stdout.write_all(content.as_bytes())?;
stdout.flush()?;
}
}
Ok(())
}
fn warn(value: serde_json::Value) {
eprintln!("weave-warning: {value}");
}
pub(crate) fn extract(
base_file: &str,
changed_file: &str,
path: Option<&str>,
embed_base: bool,
out: Option<&str>,
) -> R<()> {
let base = std::fs::read_to_string(base_file)?;
let changed = std::fs::read_to_string(changed_file)?;
let path = path.unwrap_or(changed_file);
let doc = patch::extract(path, &base, &changed, embed_base);
if !doc.exact {
warn(serde_json::json!({
"class": "LOSS",
"entity": patch::FILE_KEY,
"source": "derived",
"suggestion": "This change is not expressible at entity granularity, so the ops carry \
a whole-file fallback. Applying them to a drifted target will merge the \
whole file rather than the entities that changed.",
}));
}
emit(out, &format!("{}\n", serde_json::to_string_pretty(&doc)?))
}
pub(crate) fn apply(
ops_file: &str,
target_file: &str,
base_file: Option<&str>,
out: Option<&str>,
in_place: bool,
host: &weave_core::host::Host,
) -> R<()> {
let doc = patch::parse_ops_doc(&std::fs::read_to_string(ops_file)?)
.map_err(|e| format!("{ops_file}: {e}"))?;
let target = std::fs::read_to_string(target_file)?;
let base = base_file.map(std::fs::read_to_string).transpose()?;
let report = match patch::apply(&doc, &target, target_file, base.as_deref(), host) {
Ok(r) => r,
Err(PatchError::BaseMismatch { expected, actual }) => {
let short = |h: &str| h[..12.min(h.len())].to_string();
warn(serde_json::json!({
"class": "CONFLICT",
"entity": patch::FILE_KEY,
"source": "conflict",
"expected_base_sha256": expected,
"actual_base_sha256": actual,
"suggestion": format!(
"base mismatch: ops were extracted against sha256 {} but the supplied base \
hashes to {}. Re-extract the ops, or drop the base and apply with drift \
detection off.",
short(&expected),
short(&actual),
),
}));
std::process::exit(2);
}
};
for u in &report.unapplied {
warn(serde_json::json!({
"class": "DANGLING",
"entity": u,
"source": "derived",
"suggestion": "This op had no target in the file it was applied to. Nothing was \
dropped silently — decide whether it is stale or the target is wrong.",
}));
}
for f in &report.findings {
warn(serde_json::to_value(f)?);
}
let dest = if in_place { Some(target_file) } else { out };
emit(dest, &report.content)?;
if !report.clean {
eprintln!(
"weave patch apply: {} conflict(s) written as markers ({} mode)",
report.findings.len(),
report.mode.wire()
);
std::process::exit(1);
}
if report.mode == ApplyMode::NoBase {
eprintln!(
"weave patch apply: no base available — the target was treated as the ops' base, so \
drift could not be detected. Pass --base, or extract with --embed-base."
);
}
Ok(())
}