use std::fs;
use std::path::Path;
use anyhow::{Context, Result};
use crate::plan::{OpKind, Plan};
#[derive(Debug, Clone, Copy, Default)]
pub struct Options {
pub force: bool,
pub dry_run: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Report {
pub created: Vec<String>,
pub overwritten: Vec<String>,
pub skipped: Vec<String>,
}
impl Report {
fn is_empty(&self) -> bool {
self.created.is_empty() && self.overwritten.is_empty() && self.skipped.is_empty()
}
pub fn render(&self, dry_run: bool) -> String {
if self.is_empty() {
return "nothing to do".to_string();
}
let mut out = String::new();
if dry_run {
out.push_str("dry-run: no files written\n");
}
for p in &self.created {
out.push_str(&format!(" create {p}\n"));
}
for p in &self.overwritten {
out.push_str(&format!(" force {p}\n"));
}
for p in &self.skipped {
out.push_str(&format!(
" skip {p} (exists; use --force to overwrite)\n"
));
}
out
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct Writer {
opts: Options,
}
impl Writer {
pub fn new(opts: Options) -> Self {
Self { opts }
}
pub fn apply(&self, root: &Path, plan: &Plan) -> Result<Report> {
let mut report = Report::default();
for op in &plan.ops {
let path = root.join(&op.path);
let display = op.path.display().to_string();
match op.kind {
OpKind::Mkdir => {
if !self.opts.dry_run {
fs::create_dir_all(&path)
.with_context(|| format!("creating directory {display}"))?;
}
}
OpKind::Create => {
let exists = path.exists();
if exists && !self.opts.force {
report.skipped.push(display);
continue;
}
if !self.opts.dry_run {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("creating parent for {display}"))?;
}
fs::write(&path, &op.contents)
.with_context(|| format!("writing {display}"))?;
}
if exists {
report.overwritten.push(display);
} else {
report.created.push(display);
}
}
}
}
Ok(report)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::plan::Plan;
fn tmpdir() -> std::path::PathBuf {
let base = std::env::temp_dir().join(format!("gize-writer-{}", std::process::id()));
let unique = base.join(format!(
"{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
fs::create_dir_all(&unique).unwrap();
unique
}
#[test]
fn creates_new_files() {
let root = tmpdir();
let plan = Plan::new().create("a.txt", "hello");
let report = Writer::new(Options::default()).apply(&root, &plan).unwrap();
assert_eq!(report.created, vec!["a.txt".to_string()]);
assert_eq!(fs::read_to_string(root.join("a.txt")).unwrap(), "hello");
}
#[test]
fn skips_existing_without_force() {
let root = tmpdir();
fs::write(root.join("a.txt"), "original").unwrap();
let plan = Plan::new().create("a.txt", "new");
let report = Writer::new(Options::default()).apply(&root, &plan).unwrap();
assert_eq!(report.skipped, vec!["a.txt".to_string()]);
assert_eq!(fs::read_to_string(root.join("a.txt")).unwrap(), "original");
}
#[test]
fn overwrites_with_force() {
let root = tmpdir();
fs::write(root.join("a.txt"), "original").unwrap();
let plan = Plan::new().create("a.txt", "new");
let opts = Options {
force: true,
dry_run: false,
};
let report = Writer::new(opts).apply(&root, &plan).unwrap();
assert_eq!(report.overwritten, vec!["a.txt".to_string()]);
assert_eq!(fs::read_to_string(root.join("a.txt")).unwrap(), "new");
}
#[test]
fn dry_run_writes_nothing() {
let root = tmpdir();
let plan = Plan::new().create("a.txt", "hello");
let opts = Options {
force: false,
dry_run: true,
};
let report = Writer::new(opts).apply(&root, &plan).unwrap();
assert_eq!(report.created, vec!["a.txt".to_string()]);
assert!(!root.join("a.txt").exists());
}
}