use super::parse::{FileOp, Patch};
use super::report::{FileReport, Report, entry};
use super::seek;
use super::splice::run_hunks;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum Error {
#[error("add {path}: file already exists; update it or delete it first")]
AddExists { path: String },
#[error("{action} {path}: {source}")]
Io {
action: &'static str,
path: String,
#[source]
source: io::Error,
},
#[error("move {path} to {to}: destination already exists")]
MoveDestExists { path: String, to: String },
#[error(
"{action} {path}: destination is a symlink; refusing to write \
through it — name the link's target directly, or delete the \
link first"
)]
SymlinkDest { action: &'static str, path: String },
#[error("update {path}, hunk {hunk}: {what} {source}")]
NotFound {
path: String,
hunk: usize,
what: String,
source: seek::Error,
},
#[error(
"update {path}, hunk {hunk}: {what} is ambiguous — {source}; \
add an `@@ <enclosing symbol>` anchor line or more context"
)]
Ambiguous {
path: String,
hunk: usize,
what: String,
source: seek::Error,
},
#[error(
"update {path}, hunk {hunk}: insertion has no location; give it \
context lines, an `@@` anchor, or `*** End of File`"
)]
UnanchoredInsertion { path: String, hunk: usize },
}
enum Staged {
Add {
abs: PathBuf,
report: FileReport,
content: String,
},
Delete {
abs: PathBuf,
report: FileReport,
},
Update {
abs: PathBuf,
move_abs: Option<PathBuf>,
report: FileReport,
content: String,
},
}
pub fn apply(patch: &Patch, root: &Path) -> Result<Report, Error> {
let staged: Vec<Staged> = patch
.ops
.iter()
.map(|op| stage(op, root))
.collect::<Result<_, _>>()?;
let files = staged.into_iter().map(write).collect::<Result<_, _>>()?;
Ok(Report {
status: "applied",
files,
})
}
fn stage(op: &FileOp, root: &Path) -> Result<Staged, Error> {
match op {
FileOp::Add { path, lines } => {
let abs = root.join(path);
refuse_symlink("add", &abs, path)?;
if abs.exists() {
return Err(Error::AddExists { path: path.clone() });
}
let content = if lines.is_empty() {
String::new()
} else {
lines.join("\n") + "\n"
};
Ok(Staged::Add {
abs,
report: entry(path, "add", None, Vec::new()),
content,
})
}
FileOp::Delete { path } => {
let abs = root.join(path);
read(&abs, path)?;
Ok(Staged::Delete {
abs,
report: entry(path, "delete", None, Vec::new()),
})
}
FileOp::Update {
path,
move_to,
hunks,
} => {
let abs = root.join(path);
refuse_symlink("update", &abs, path)?;
let text = read(&abs, path)?;
let move_abs = match move_to {
Some(to) => {
let dest = root.join(to);
refuse_symlink("move to", &dest, to)?;
if dest.exists() {
return Err(Error::MoveDestExists {
path: path.clone(),
to: to.clone(),
});
}
Some(dest)
}
None => None,
};
let (content, applied) = run_hunks(&text, hunks, path)?;
Ok(Staged::Update {
abs,
move_abs,
report: entry(path, "update", move_to.clone(), applied),
content,
})
}
}
}
fn refuse_symlink(action: &'static str, abs: &Path, path: &str) -> Result<(), Error> {
let is_link = fs::symlink_metadata(abs).is_ok_and(|m| m.file_type().is_symlink());
if is_link {
return Err(Error::SymlinkDest {
action,
path: path.to_string(),
});
}
Ok(())
}
fn read(abs: &Path, path: &str) -> Result<String, Error> {
fs::read_to_string(abs).map_err(|source| Error::Io {
action: "read",
path: path.to_string(),
source,
})
}
fn io_err(action: &'static str, abs: &Path) -> impl FnOnce(io::Error) -> Error {
let path = abs.display().to_string();
move |source| Error::Io {
action,
path,
source,
}
}
fn write(staged: Staged) -> Result<FileReport, Error> {
match staged {
Staged::Add {
abs,
report,
content,
} => {
if let Some(parent) = abs.parent().filter(|p| !p.as_os_str().is_empty()) {
fs::create_dir_all(parent).map_err(io_err("create directory for", &abs))?;
}
fs::write(&abs, content).map_err(io_err("write", &abs))?;
Ok(report)
}
Staged::Delete { abs, report } => {
fs::remove_file(&abs).map_err(io_err("delete", &abs))?;
Ok(report)
}
Staged::Update {
abs,
move_abs,
report,
content,
} => {
fs::write(&abs, content).map_err(io_err("write", &abs))?;
if let Some(dest) = move_abs {
if let Some(parent) = dest.parent().filter(|p| !p.as_os_str().is_empty()) {
fs::create_dir_all(parent).map_err(io_err("create directory for", &dest))?;
}
fs::rename(&abs, &dest).map_err(io_err("rename", &abs))?;
}
Ok(report)
}
}
}