Add nu hooks

main
trivernis 8 months ago
parent 55caec2fff
commit 1b30d50a11
Signed by: Trivernis
GPG Key ID: 7E6D18B61C8D2F4B

2392
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -15,6 +15,7 @@ chksum = "0.3.0"
clap = { version = "4.4.17", features = ["derive", "env"] } clap = { version = "4.4.17", features = ["derive", "env"] }
dialoguer = "0.11.0" dialoguer = "0.11.0"
dirs = "5.0.1" dirs = "5.0.1"
embed-nu = "0.9.1"
figment = { version = "0.10.13", features = ["toml", "env"] } figment = { version = "0.10.13", features = ["toml", "env"] }
gix = { version = "0.57.1", default-features = false, features = ["basic", "index", "worktree-mutation", "revision", "blocking-network-client", "prodash", "blocking-http-transport-reqwest-rust-tls"] } gix = { version = "0.57.1", default-features = false, features = ["basic", "index", "worktree-mutation", "revision", "blocking-network-client", "prodash", "blocking-http-transport-reqwest-rust-tls"] }
globset = { version = "0.4.14", features = ["serde", "serde1"] } globset = { version = "0.4.14", features = ["serde", "serde1"] }
@ -24,6 +25,7 @@ lazy_static = "1.4.0"
log = "0.4.20" log = "0.4.20"
miette = { version = "5.10.0", features = ["serde", "fancy"] } miette = { version = "5.10.0", features = ["serde", "fancy"] }
pretty_env_logger = "0.5.0" pretty_env_logger = "0.5.0"
rusty-value = "0.6.0"
serde = { version = "1.0.195", features = ["derive"] } serde = { version = "1.0.195", features = ["derive"] }
serde_json = "1.0.111" serde_json = "1.0.111"
sys-info = "0.9.1" sys-info = "0.9.1"

@ -40,7 +40,7 @@ Notice the use of templating for the path. The `dirs` variable contains paths sp
`home` in this case would either be `{FOLDERID_Profile}` on Windows or `$HOME` on Linux and MacOS. `home` in this case would either be `{FOLDERID_Profile}` on Windows or `$HOME` on Linux and MacOS.
The `ignored` setting can be used to ignore certain files using an array of glob-strings. The `ignored` setting can be used to ignore certain files using an array of glob-strings.
Now add some files to the repos root directory. Now add some files to a directory `content` in the repo.
Normal files get just copied over. Subdirectories are created and copied as well, unless they themselves Normal files get just copied over. Subdirectories are created and copied as well, unless they themselves
contain a `dirs.toml` file that specifies a different location. contain a `dirs.toml` file that specifies a different location.
@ -87,6 +87,30 @@ File permissions are persisted the way git stored them. This is true for templat
execute permission will result in a rendered file with the same permission. execute permission will result in a rendered file with the same permission.
#### Hooks
All `.nu` files in the `hooks` folder in the repos root are interpreted as hook scripts.
Currently there's four functions that can be defined in these scripts that correspond to
events of the same name:
```
before_apply_all
after_apply_all
before_apply_each
after_apply_each
```
These functions will be called with a single argument, the event context, that can be used
to change certain properties of files or inspect the entire list of files that are about to be written.
For example one could change the attributes of script files with the following hook
```nu
# Make `test-2/main` executable
def after_apply_each [ctx] {
if $ctx.dst =~ "test-2/main" {
chmod +x $ctx.dst
}
}
```
### License ### License
CNPL-v7+ CNPL-v7+

@ -13,18 +13,24 @@ use miette::{Context, IntoDiagnostic, Result};
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
use crate::repo::hooks::{ApplyAllContext, ApplyEachContext, Hooks};
use super::FsAccess; use super::FsAccess;
pub struct BufferedFsAccess { pub struct BufferedFsAccess {
repo: PathBuf,
mappings: Vec<(NamedTempFile, PathBuf)>, mappings: Vec<(NamedTempFile, PathBuf)>,
diff_tool: String, diff_tool: String,
hooks: Hooks,
} }
impl BufferedFsAccess { impl BufferedFsAccess {
pub fn with_difftool(diff_tool: String) -> Self { pub fn new(repo: PathBuf, diff_tool: String, hooks: Hooks) -> Self {
Self { Self {
mappings: Vec::new(), mappings: Vec::new(),
repo,
diff_tool, diff_tool,
hooks,
} }
} }
} }
@ -69,13 +75,32 @@ impl FsAccess for BufferedFsAccess {
fn persist(&mut self) -> Result<()> { fn persist(&mut self) -> Result<()> {
let mappings = mem::take(&mut self.mappings); let mappings = mem::take(&mut self.mappings);
let mut drop_list = Vec::new(); let mut drop_list = Vec::new();
let paths: Vec<_> = mappings.iter().map(|(_, p)| p.to_owned()).collect();
self.hooks.before_apply_all(ApplyAllContext {
repo: self.repo.clone(),
paths: paths.clone(),
})?;
for (tmp, dst) in mappings { for (tmp, dst) in mappings {
if confirm_write(&self.diff_tool, tmp.path(), &dst)? { if confirm_write(&self.diff_tool, tmp.path(), &dst)? {
ensure_parent(dst.parent().unwrap())?; ensure_parent(dst.parent().unwrap())?;
self.hooks.before_apply_each(ApplyEachContext {
repo: self.repo.clone(),
src: tmp.path().to_owned(),
dst: dst.clone(),
})?;
fs::copy(tmp.path(), &dst) fs::copy(tmp.path(), &dst)
.into_diagnostic() .into_diagnostic()
.with_context(|| format!("copying {:?} to {dst:?}", tmp.path()))?; .with_context(|| format!("copying {:?} to {dst:?}", tmp.path()))?;
self.hooks.after_apply_each(ApplyEachContext {
repo: self.repo.clone(),
src: tmp.path().to_owned(),
dst: dst.clone(),
})?;
log::info!("Updated {dst:?}"); log::info!("Updated {dst:?}");
} else { } else {
log::info!("Skipping {dst:?}"); log::info!("Skipping {dst:?}");
@ -84,6 +109,11 @@ impl FsAccess for BufferedFsAccess {
} }
mem::drop(drop_list); mem::drop(drop_list);
self.hooks.after_apply_all(ApplyAllContext {
repo: self.repo.clone(),
paths,
})?;
Ok(()) Ok(())
} }
} }

@ -50,7 +50,7 @@ fn init_logging(verbose: bool) {
} }
fn apply(args: &Args) -> Result<()> { fn apply(args: &Args) -> Result<()> {
let repo = SiloRepo::open(&args.repo)?; let mut repo = SiloRepo::open(&args.repo)?;
repo.apply()?; repo.apply()?;
log::info!("Applied all configurations in {:?}", args.repo); log::info!("Applied all configurations in {:?}", args.repo);

@ -0,0 +1,172 @@
use embed_nu::{CommandGroupConfig, Context};
use rusty_value::*;
use std::{
fs, mem,
path::{Path, PathBuf},
};
use miette::{IntoDiagnostic, Result};
#[derive(Clone, Debug)]
pub struct Hooks {
scripts: Vec<HookScript>,
}
#[derive(Clone)]
pub struct HookScript {
script: embed_nu::Context,
}
impl std::fmt::Debug for HookScript {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("HookScript")
}
}
#[derive(Clone, Debug, RustyValue)]
pub struct ApplyAllContext {
pub repo: PathBuf,
pub paths: Vec<PathBuf>,
}
#[derive(Clone, Debug, RustyValue)]
pub struct ApplyEachContext {
pub repo: PathBuf,
pub src: PathBuf,
pub dst: PathBuf,
}
impl Hooks {
pub fn take(&mut self) -> Self {
Hooks {
scripts: mem::take(&mut self.scripts),
}
}
pub fn parse(path: &Path) -> Result<Self> {
log::debug!("Parsing hooks in {path:?}");
let mut readdir = fs::read_dir(path).into_diagnostic()?;
let mut scripts = Vec::new();
while let Some(entry) = readdir.next() {
let path = entry.into_diagnostic()?.path();
if path.is_file() && path.extension().is_some_and(|e| e == "nu") {
log::debug!("Found hook {path:?}");
scripts.push(HookScript::parse(&path)?)
}
}
Ok(Self { scripts })
}
pub fn before_apply_all(&mut self, ctx: ApplyAllContext) -> Result<()> {
for script in &mut self.scripts {
script.before_apply_all(ctx.clone())?;
}
Ok(())
}
pub fn after_apply_all(&mut self, ctx: ApplyAllContext) -> Result<()> {
for script in &mut self.scripts {
script.after_apply_all(ctx.clone())?;
}
Ok(())
}
pub fn before_apply_each(&mut self, ctx: ApplyEachContext) -> Result<()> {
for script in &mut self.scripts {
script.before_apply_each(ctx.clone())?;
}
Ok(())
}
pub fn after_apply_each(&mut self, ctx: ApplyEachContext) -> Result<()> {
for script in &mut self.scripts {
script.after_apply_each(ctx.clone())?;
}
Ok(())
}
pub(crate) fn empty() -> Hooks {
Self {
scripts: Vec::new(),
}
}
}
impl HookScript {
pub fn parse(path: &Path) -> Result<Self> {
let contents = fs::read_to_string(path).into_diagnostic()?;
let ctx = Context::builder()
.with_command_groups(CommandGroupConfig::default().all_groups(true))
.into_diagnostic()?
.add_script(contents)
.into_diagnostic()?
.add_parent_env_vars()
.build()
.into_diagnostic()?;
Ok(Self { script: ctx })
}
pub fn before_apply_all(&mut self, ctx: ApplyAllContext) -> Result<()> {
if self.script.has_fn("before_apply_all") {
let pipeline = self
.script
.call_fn("before_apply_all", [ctx])
.into_diagnostic()?;
self.script.print_pipeline(pipeline).into_diagnostic()?;
} else {
log::debug!("No `before_apply_all` in script");
}
Ok(())
}
pub fn after_apply_all(&mut self, ctx: ApplyAllContext) -> Result<()> {
if self.script.has_fn("after_apply_all") {
let pipeline = self
.script
.call_fn("after_apply_all", [ctx])
.into_diagnostic()?;
self.script.print_pipeline(pipeline).into_diagnostic()?;
} else {
log::debug!("No `after_apply_all` in script");
}
Ok(())
}
pub fn before_apply_each(&mut self, ctx: ApplyEachContext) -> Result<()> {
if self.script.has_fn("before_apply_each") {
let pipeline = self
.script
.call_fn("before_apply_each", [ctx])
.into_diagnostic()?;
self.script.print_pipeline(pipeline).into_diagnostic()?;
} else {
log::debug!("No `before_apply_each` in script");
}
Ok(())
}
pub fn after_apply_each(&mut self, ctx: ApplyEachContext) -> Result<()> {
if self.script.has_fn("after_apply_each") {
let pipeline = self
.script
.call_fn("after_apply_each", [ctx])
.into_diagnostic()?;
self.script.print_pipeline(pipeline).into_diagnostic()?;
} else {
log::debug!("No `after_apply_each` in script");
}
Ok(())
}
}

@ -1,21 +1,27 @@
mod contents; mod contents;
pub(crate) mod hooks;
use globset::GlobSet; use globset::GlobSet;
use miette::{bail, IntoDiagnostic, Result}; use miette::{bail, IntoDiagnostic, Result};
use std::{env, path::Path}; use std::{
env,
path::{Path, PathBuf},
};
use crate::{ use crate::{
config::{read_config, SiloConfig}, config::{read_config, SiloConfig},
fs_access::{BufferedFsAccess, FsAccess}, fs_access::{BufferedFsAccess, FsAccess},
}; };
use self::contents::Contents; use self::{contents::Contents, hooks::Hooks};
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct SiloRepo { pub struct SiloRepo {
pub config: SiloConfig, pub config: SiloConfig,
repo: PathBuf,
contents: Contents, contents: Contents,
hooks: Hooks,
} }
impl SiloRepo { impl SiloRepo {
@ -30,17 +36,28 @@ impl SiloRepo {
if !content_path.exists() { if !content_path.exists() {
bail!("No content stored in this dotfiles repo"); bail!("No content stored in this dotfiles repo");
} }
let hook_path = path.join("hooks");
let hooks = if hook_path.exists() {
Hooks::parse(&hook_path)?
} else {
Hooks::empty()
};
Ok(Self { Ok(Self {
contents: Contents::parse(pctx, content_path)?, contents: Contents::parse(pctx, content_path)?,
repo: path.to_owned(),
config, config,
hooks,
}) })
} }
pub fn apply(&self) -> Result<()> { pub fn apply(&mut self) -> Result<()> {
let cwd = dirs::home_dir().unwrap_or(env::current_dir().into_diagnostic()?); let cwd = dirs::home_dir().unwrap_or(env::current_dir().into_diagnostic()?);
let fs_access: Box<dyn FsAccess> = Box::new(BufferedFsAccess::with_difftool( let fs_access: Box<dyn FsAccess> = Box::new(BufferedFsAccess::new(
self.repo.clone(),
self.config.diff_tool.to_owned(), self.config.diff_tool.to_owned(),
self.hooks.take(),
)); ));
let mut ctx = ApplyContext { let mut ctx = ApplyContext {
config: self.config.clone(), config: self.config.clone(),

Loading…
Cancel
Save