diff --git a/Cargo.lock b/Cargo.lock index 4d09dc1..aed8297 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -160,6 +160,193 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chksum" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56300de3d4b6e12639a862e3a9320b4ea50321689f869c7e3d7bbf2f0f5f95c" +dependencies = [ + "chksum-core", + "chksum-hash", + "chksum-md5", + "chksum-sha1", + "chksum-sha2", +] + +[[package]] +name = "chksum-core" +version = "0.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6db20071fdeca52ed6a7745519fb2d343fddcb93af81448373b851f072aaec5" +dependencies = [ + "chksum-hash-core", + "thiserror", +] + +[[package]] +name = "chksum-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74fca0f0064c8c996eb06d803eca772b12cc8f77b53d92103fc457713fe18389" +dependencies = [ + "chksum-hash-core", + "chksum-hash-md5", + "chksum-hash-sha1", + "chksum-hash-sha2", +] + +[[package]] +name = "chksum-hash-core" +version = "0.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "221456234d441c788a2c51a27b91c4380f499de560670a67d3303e621d37b3bd" + +[[package]] +name = "chksum-hash-md5" +version = "0.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80c33d01c33c9e193fe33e719a29a7eb900c08583375dd1d3269991aacbe434a" +dependencies = [ + "chksum-hash-core", + "thiserror", +] + +[[package]] +name = "chksum-hash-sha1" +version = "0.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62ae4186244a0990f1cde56d4bb8cc32571a601b065991fd48cce32556aaa1a9" +dependencies = [ + "chksum-hash-core", + "thiserror", +] + +[[package]] +name = "chksum-hash-sha2" +version = "0.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b18cf27af5d9792412533d94b6ac54b5022d5e53521832a5c5d3bdc2decdcffc" +dependencies = [ + "chksum-hash-sha2-224", + "chksum-hash-sha2-256", + "chksum-hash-sha2-384", + "chksum-hash-sha2-512", +] + +[[package]] +name = "chksum-hash-sha2-224" +version = "0.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb4a205ae2433218c6d8677a384ac0bd27a794c49d9b3febbecd1ebaa9864b44" +dependencies = [ + "chksum-hash-core", + "thiserror", +] + +[[package]] +name = "chksum-hash-sha2-256" +version = "0.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99b9b29d0cf25fbbb72ea8ff4b89492eb1a8d72a4599b64ec3699389cae81f9" +dependencies = [ + "chksum-hash-core", + "thiserror", +] + +[[package]] +name = "chksum-hash-sha2-384" +version = "0.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317fbe67e0f9fedff9994655e840e4b7495333680a5dde7c2dddb43e46bffcfa" +dependencies = [ + "chksum-hash-core", + "thiserror", +] + +[[package]] +name = "chksum-hash-sha2-512" +version = "0.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9426c7c11aef475fddb9d1ad95539678ba882ff274e4b5d62e0c89b75b41ee5f" +dependencies = [ + "chksum-hash-core", + "thiserror", +] + +[[package]] +name = "chksum-md5" +version = "0.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95dda0f76fbb6069e042c370a928457086e1b4eabc7e75f5f49fe1b913634351" +dependencies = [ + "chksum-core", + "chksum-hash-md5", +] + +[[package]] +name = "chksum-sha1" +version = "0.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d01c47aa947fb8d8649bf9e942a5ff4ec63a550df087433976e266512d2b38dc" +dependencies = [ + "chksum-core", + "chksum-hash-sha1", +] + +[[package]] +name = "chksum-sha2" +version = "0.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac0e41b69be74dad939bb277d2b13b6bc9b1e406c62ce6e62eff8a29a106e68" +dependencies = [ + "chksum-core", + "chksum-hash-sha2", + "chksum-sha2-224", + "chksum-sha2-256", + "chksum-sha2-384", + "chksum-sha2-512", +] + +[[package]] +name = "chksum-sha2-224" +version = "0.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc70344710cfe7cc696cfbbebbc50584f76a46d7bf29737a58b48e61a15ae31f" +dependencies = [ + "chksum-core", + "chksum-hash-sha2-224", +] + +[[package]] +name = "chksum-sha2-256" +version = "0.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38ac7f7e93115d4edc71e73c83b1bd3038fdd01525f635f78815956567d0f7ab" +dependencies = [ + "chksum-core", + "chksum-hash-sha2-256", +] + +[[package]] +name = "chksum-sha2-384" +version = "0.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ace9dacad0683db9cc7d71c270ff858c86b939729074d6ab6379e8e2c1c7fc" +dependencies = [ + "chksum-core", + "chksum-hash-sha2-384", +] + +[[package]] +name = "chksum-sha2-512" +version = "0.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5c7acaafd327334c22d591177e5eb3f36eb85a42489fa1544c1f4d867bc7899" +dependencies = [ + "chksum-core", + "chksum-hash-sha2-512", +] + [[package]] name = "clap" version = "4.4.14" @@ -206,6 +393,19 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "unicode-width", + "windows-sys 0.52.0", +] + [[package]] name = "cpufeatures" version = "0.2.12" @@ -225,6 +425,19 @@ dependencies = [ "typenum", ] +[[package]] +name = "dialoguer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" +dependencies = [ + "console", + "shell-words", + "tempfile", + "thiserror", + "zeroize", +] + [[package]] name = "digest" version = "0.10.7" @@ -256,6 +469,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "env_logger" version = "0.10.1" @@ -285,6 +504,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + [[package]] name = "figment" version = "0.10.13" @@ -884,11 +1109,19 @@ dependencies = [ "digest", ] +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + [[package]] name = "silo" version = "0.1.0" dependencies = [ + "chksum", "clap", + "dialoguer", "dirs", "figment", "git2", @@ -902,6 +1135,7 @@ dependencies = [ "serde", "serde_json", "sys-info", + "tempfile", "toml", ] @@ -966,6 +1200,19 @@ dependencies = [ "libc", ] +[[package]] +name = "tempfile" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys 0.52.0", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -1331,3 +1578,9 @@ name = "yansi" version = "1.0.0-rc.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1367295b8f788d371ce2dbc842c7b709c73ee1364d30351dd300ec2203b12377" + +[[package]] +name = "zeroize" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" diff --git a/Cargo.toml b/Cargo.toml index b2e2fc5..41c3551 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,9 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +chksum = "0.3.0" clap = { version = "4.4.14", features = ["derive", "env"] } +dialoguer = "0.11.0" dirs = "5.0.1" figment = { version = "0.10.13", features = ["toml", "env"] } git2 = "0.18.1" @@ -20,4 +22,5 @@ pretty_env_logger = "0.5.0" serde = { version = "1.0.195", features = ["derive"] } serde_json = "1.0.111" sys-info = "0.9.1" +tempfile = "3.9.0" toml = "0.8.8" diff --git a/src/repo.rs b/src/repo.rs index 3ca91fb..7ccded8 100644 --- a/src/repo.rs +++ b/src/repo.rs @@ -1,12 +1,17 @@ +use chksum::sha2_256::chksum; +use dialoguer::Confirm; use globset::{Glob, GlobSet, GlobSetBuilder}; use miette::{bail, Context, IntoDiagnostic, Result}; use serde::{Deserialize, Serialize}; use std::{ env, - fs::{self}, + fs::{self, File}, + io::Write, path::{Path, PathBuf}, + process::Command, rc::Rc, }; +use tempfile::NamedTempFile; use crate::{ config::{read_config, SiloConfig}, @@ -202,17 +207,29 @@ impl FileEntry { let filename = new_path.file_name().unwrap(); let dest = cwd.join(filename); - templating::render_to_file(&dest, &contents, &ctx.config.template_context)?; - log::info!("Render {path:?} -> {dest:?}"); + let render_contents = templating::render(&contents, &ctx.config.template_context)?; + + if confirm_changes(&ctx.config.diff_tool, &render_contents, &dest)? { + log::info!("Render {path:?} -> {dest:?}"); + fs::write(&dest, render_contents) + .into_diagnostic() + .context("writing changes")?; + } else { + log::info!("Skipping {path:?} !-> {dest:?}"); + } } FileEntry::Plain(path) => { let filename = path.file_name().unwrap(); let dest = cwd.join(filename); - log::info!("Copying {path:?} -> {dest:?}"); - fs::copy(path, &dest) - .into_diagnostic() - .with_context(|| format!("copy {path:?} to {dest:?}"))?; + if confirm_write(&ctx.config.diff_tool, path, &dest)? { + log::info!("Copying {path:?} -> {dest:?}"); + fs::copy(path, &dest) + .into_diagnostic() + .with_context(|| format!("copy {path:?} to {dest:?}"))?; + } else { + log::info!("Skipping {path:?} !-> {dest:?}"); + } } } @@ -247,3 +264,42 @@ impl RootDirData { .with_context(|| format!("parsing metadata file {path:?}")) } } + +fn confirm_changes(diff_tool: &str, changes: &str, original: &Path) -> Result { + let mut tmp = NamedTempFile::new() + .into_diagnostic() + .context("create tmp file")?; + tmp.write_all(changes.as_bytes()) + .into_diagnostic() + .context("write tmp file")?; + confirm_write(diff_tool, &tmp.into_temp_path(), original) +} + +fn confirm_write(diff_tool: &str, a: &Path, b: &Path) -> Result { + if !b.exists() { + return Ok(true); + } + let f1 = File::open(a) + .into_diagnostic() + .with_context(|| format!("opening file {a:?}"))?; + let f2 = File::open(b) + .into_diagnostic() + .with_context(|| format!("opening file {b:?}"))?; + + if chksum(f1).into_diagnostic()?.as_bytes() == chksum(f2).into_diagnostic()?.as_bytes() { + return Ok(true); + } + Command::new(diff_tool) + .arg(b) + .arg(a) + .spawn() + .into_diagnostic() + .context("spawn diff tool")? + .wait() + .into_diagnostic() + .context("wait for diff tool to exit")?; + Confirm::new() + .with_prompt("Do you want to apply these changes?") + .interact() + .into_diagnostic() +} diff --git a/src/templating.rs b/src/templating.rs index df68d4e..b7d7ecc 100644 --- a/src/templating.rs +++ b/src/templating.rs @@ -1,9 +1,4 @@ -use std::{ - env, - fs::File, - io::BufWriter, - path::{Path, PathBuf}, -}; +use std::{env, path::PathBuf}; use handlebars::Handlebars; use handlebars_switch::SwitchHelper; @@ -11,18 +6,6 @@ use lazy_static::lazy_static; use miette::{Context, IntoDiagnostic, Result}; use serde::Serialize; -pub fn render_to_file(path: &Path, template: &str, ctx: T) -> Result<()> { - let file = File::create(path) - .into_diagnostic() - .context("creating file")?; - let writer = BufWriter::new(file); - engine() - .render_template_to_write(template, &context(ctx), writer) - .into_diagnostic() - .context("rendering to path")?; - Ok(()) -} - pub fn render(template: &str, ctx: T) -> Result { engine() .render_template(template, &context(ctx))