use std::path::Path;
use crate::containment::PathGuard;
use crate::plan::Operation;
use super::{ApplyMode, EditResult};
pub fn apply_patch(
path: &Path,
patch_text: &str,
mode: ApplyMode,
guard: Option<&PathGuard>,
) -> anyhow::Result<EditResult> {
let op = Operation::PatchApply {
diff: patch_text.into(),
on_stale: Default::default(),
allow_conflicts: false,
};
let abs = super::absolute_for_engine(path).map_err(|e| {
crate::fallback::EditError::new(
crate::fallback::EditErrorKind::OperationFailed,
format!("failed to resolve path {}: {e}", path.display()),
)
})?;
let cwd_owned: std::path::PathBuf;
let cwd = if path.is_absolute() {
abs.parent().unwrap_or_else(|| Path::new("."))
} else {
cwd_owned = abs
.ancestors()
.nth(path.components().count())
.unwrap_or_else(|| Path::new("."))
.to_path_buf();
cwd_owned.as_path()
};
let display = path.to_string_lossy();
patch_write(op, cwd, mode, guard, Some(display.as_ref()))
}
#[cfg(any(feature = "cli", feature = "files"))]
fn patch_write(
op: Operation,
cwd: &Path,
mode: ApplyMode,
guard: Option<&PathGuard>,
display_path: Option<&str>,
) -> anyhow::Result<EditResult> {
super::execute_as_edit_result_with_path(op, mode, cwd, guard, "patch", None, display_path)
}
#[cfg(not(any(feature = "cli", feature = "files")))]
fn patch_write(
_op: Operation,
cwd: &Path,
mode: ApplyMode,
guard: Option<&PathGuard>,
_display_path: Option<&str>,
) -> anyhow::Result<EditResult> {
use crate::ops;
if let Operation::PatchApply { diff, .. } = _op {
let patch_files = ops::patch::parse_patch(&diff).map_err(|e| {
anyhow::Error::new(crate::exit::ParseErrorError {
msg: format!("patch parse error: {e}"),
})
})?;
if patch_files.is_empty() {
return Err(anyhow::Error::new(crate::exit::ParseErrorError {
msg: "no files in patch".into(),
}));
}
let pf = &patch_files[0];
let load_rel = pf.rename_from.as_deref().unwrap_or(pf.path.as_str());
let load_path = cwd.join(load_rel);
let write_path = cwd.join(&pf.path);
if let Some(from) = pf.rename_from.as_deref()
&& crate::ops::patch::rename_would_clobber_dest(
from,
&pf.path,
crate::ops::file::path_entry_exists(&write_path),
)
{
return Err(anyhow::Error::new(crate::exit::AlreadyExistsError {
msg: crate::ops::patch::rename_dest_exists_msg(&pf.path),
}));
}
let original = if pf.is_creation {
String::new()
} else {
crate::files::load_text_strict(&load_path, load_rel)?
};
let new_content = ops::patch::apply_hunks(&original, &pf.hunks).map_err(|e| {
if e.contains("stale context") {
anyhow::Error::new(crate::exit::AmbiguousError {
msg: format!("patch apply error: {e}"),
})
} else {
anyhow::Error::new(crate::exit::InvalidInputError {
msg: format!("patch apply error: {e}"),
})
}
})?;
let policy = crate::write::WritePolicy::default();
let (applied, backup_session) =
super::write_if_apply(&write_path, &new_content, mode, &policy, guard)?;
if applied {
if let Some(ref from) = pf.rename_from {
let old = cwd.join(from);
if crate::ops::file::path_entry_exists(&old) {
let _ = std::fs::remove_file(&old);
}
}
}
{
let mut __e =
super::build_edit_result(&pf.path, original, new_content, applied, "patch", None);
__e.backup_session = backup_session;
Ok(__e)
}
} else {
anyhow::bail!("expected PatchApply operation")
}
}
pub fn apply_patch_file(
patch_text: &str,
cwd: &Path,
mode: ApplyMode,
guard: Option<&PathGuard>,
) -> anyhow::Result<Vec<EditResult>> {
let patch_files = crate::ops::patch::parse_patch(patch_text).map_err(|e| {
anyhow::Error::new(crate::exit::ParseErrorError {
msg: format!("patch parse error: {e}"),
})
})?;
#[derive(Clone)]
enum StageOp {
Write {
write_path: std::path::PathBuf,
display: String,
original: String,
new_content: String,
},
Delete {
path: std::path::PathBuf,
display: String,
original: String,
},
Rename {
from: std::path::PathBuf,
to: std::path::PathBuf,
from_display: String,
to_display: String,
original: String,
new_content: String,
},
}
let mut staged: Vec<StageOp> = Vec::new();
for pf in &patch_files {
let load_rel = pf.rename_from.as_deref().unwrap_or(pf.path.as_str());
let load_path = cwd.join(load_rel);
let write_path = cwd.join(&pf.path);
if let Some(from) = pf.rename_from.as_deref()
&& crate::ops::patch::rename_would_clobber_dest(
from,
&pf.path,
crate::ops::file::path_entry_exists(&write_path),
)
{
return Err(anyhow::Error::new(crate::exit::AlreadyExistsError {
msg: crate::ops::patch::rename_dest_exists_msg(&pf.path),
}));
}
if pf.is_deletion {
let original = if crate::ops::file::path_entry_exists(&load_path) {
crate::files::load_text_strict(&load_path, load_rel).unwrap_or_default()
} else {
String::new()
};
staged.push(StageOp::Delete {
path: load_path,
display: pf.path.clone(),
original,
});
continue;
}
let pure_rename = pf.rename_from.is_some() && pf.hunks.is_empty();
let original = if pf.is_creation {
String::new()
} else if pure_rename {
match crate::files::load_text_strict(&load_path, load_rel) {
Ok(s) => s,
Err(e) if crate::exit::is_binary(&e) || crate::exit::is_invalid_encoding(&e) => {
String::new()
}
Err(e) => return Err(e),
}
} else {
crate::files::load_text_strict(&load_path, load_rel)?
};
let new_content = if pure_rename {
original.clone()
} else {
crate::ops::patch::apply_hunks(&original, &pf.hunks).map_err(|e| {
if e.contains("stale context") {
anyhow::Error::new(crate::exit::AmbiguousError {
msg: format!("patch apply error for {}: {e}", pf.path),
})
} else {
anyhow::Error::new(crate::exit::InvalidInputError {
msg: format!("patch apply error for {}: {e}", pf.path),
})
}
})?
};
if let Some(from) = pf.rename_from.as_ref() {
let from_path = cwd.join(from);
staged.push(StageOp::Rename {
from: from_path,
to: write_path,
from_display: from.clone(),
to_display: pf.path.clone(),
original,
new_content,
});
} else {
staged.push(StageOp::Write {
write_path,
display: pf.path.clone(),
original,
new_content,
});
}
}
let policy = crate::write::WritePolicy::default();
let (applied, backup_session) = if mode == ApplyMode::Apply {
for op in &staged {
match op {
StageOp::Write { write_path, .. } => super::ensure_contained(guard, write_path)?,
StageOp::Delete { path, .. } => super::ensure_contained(guard, path)?,
StageOp::Rename { from, to, .. } => {
super::ensure_contained(guard, from)?;
super::ensure_contained(guard, to)?;
}
}
}
let mut backup = crate::backup::BackupSession::new(cwd)?;
for op in &staged {
match op {
StageOp::Write { write_path, .. } => {
if crate::ops::file::path_entry_exists(write_path) {
backup.save_before_write(write_path)?;
}
}
StageOp::Delete { path, .. } => {
if crate::ops::file::path_entry_exists(path) {
backup.save_before_write(path)?;
}
}
StageOp::Rename { from, to, .. } => {
if crate::ops::file::path_entry_exists(from) {
backup.save_before_write(from)?;
}
if crate::ops::file::path_entry_exists(to) && to != from {
backup.save_before_write(to)?;
}
}
}
}
let session = backup.finalize()?;
let write_result = (|| -> anyhow::Result<()> {
for op in &staged {
match op {
StageOp::Write {
write_path,
new_content,
..
} => {
if let Some(parent) = write_path.parent()
&& !parent.as_os_str().is_empty()
&& !parent.exists()
{
std::fs::create_dir_all(parent)?;
}
crate::write::atomic_write(write_path, new_content, &policy)?;
}
StageOp::Delete { path, .. } => {
if crate::ops::file::path_entry_exists(path) {
std::fs::remove_file(path).map_err(|e| {
anyhow::anyhow!(
"patch delete: failed to remove {}: {e}",
path.display()
)
})?;
}
}
StageOp::Rename {
from,
to,
original,
new_content,
..
} => {
if let Some(parent) = to.parent()
&& !parent.as_os_str().is_empty()
&& !parent.exists()
{
std::fs::create_dir_all(parent)?;
}
crate::ops::file::rename_or_copy(from, to)?;
if new_content != original {
crate::write::atomic_write(to, new_content, &policy)?;
}
}
}
}
Ok(())
})();
if let Err(e) = write_result {
return Err(super::mutation_err_after_backup(cwd, session.as_deref(), e));
}
(true, session)
} else {
(false, None)
};
let mut results = Vec::with_capacity(staged.len());
for op in staged {
let mut edit = match op {
StageOp::Write {
display,
original,
new_content,
..
} => super::build_edit_result(&display, original, new_content, applied, "patch", None),
StageOp::Delete {
display, original, ..
} => {
let mut e = super::build_edit_result(
&display,
original,
String::new(),
applied,
"patch",
None,
);
e.changed = true;
e
}
StageOp::Rename {
from_display,
to_display,
original,
new_content,
..
} => {
let mut e = super::build_edit_result(
&to_display,
original,
new_content,
applied,
"patch",
Some(to_display.clone()),
);
e.changed = true;
e.path = from_display;
e.dest_path = Some(to_display);
e
}
};
edit.backup_session = backup_session.clone();
results.push(edit);
}
Ok(results)
}