fix(bugs): bugs in template apply
This commit is contained in:
parent
a7548dc356
commit
77b25771c3
3 changed files with 155 additions and 17 deletions
|
|
@ -1,4 +1,5 @@
|
||||||
use super::{find_config_file, parse_config, type_check};
|
use super::{decrypt_encrypted_vars_with_source_dir, find_config_file, parse_config, type_check};
|
||||||
|
use doot_core::deploy::TemplateEngine;
|
||||||
use doot_core::state::{StateStore, SyncStatus};
|
use doot_core::state::{StateStore, SyncStatus};
|
||||||
use doot_core::{Config, DeployAction, Deployer};
|
use doot_core::{Config, DeployAction, Deployer};
|
||||||
use doot_lang::ast::HookStage;
|
use doot_lang::ast::HookStage;
|
||||||
|
|
@ -99,6 +100,19 @@ pub fn run(config_path: Option<PathBuf>, dry_run: bool, prune: bool) -> anyhow::
|
||||||
let state_file = config.state_file.clone();
|
let state_file = config.state_file.clone();
|
||||||
let state = StateStore::new(&state_file);
|
let state = StateStore::new(&state_file);
|
||||||
|
|
||||||
|
// Prepare template variables early for use in both deployer and preview
|
||||||
|
let mut template_vars = evaluator.get_template_variables();
|
||||||
|
decrypt_encrypted_vars_with_source_dir(
|
||||||
|
&result,
|
||||||
|
&config,
|
||||||
|
&mut template_vars,
|
||||||
|
Some(&source_dir),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Initialize preview TemplateEngine
|
||||||
|
let mut preview_engine = TemplateEngine::new();
|
||||||
|
preview_engine.set_doot_variables(&template_vars);
|
||||||
|
|
||||||
// Check for conflicts before deploying (track by original index)
|
// Check for conflicts before deploying (track by original index)
|
||||||
let mut deploy_set: HashSet<usize> = HashSet::new();
|
let mut deploy_set: HashSet<usize> = HashSet::new();
|
||||||
let mut conflicts: Vec<(usize, SyncStatus)> = Vec::new();
|
let mut conflicts: Vec<(usize, SyncStatus)> = Vec::new();
|
||||||
|
|
@ -195,7 +209,16 @@ pub fn run(config_path: Option<PathBuf>, dry_run: bool, prune: bool) -> anyhow::
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Single file handling
|
// Single file handling
|
||||||
match status {
|
// Check for template rendering drift when sync status is Synced and file is a template
|
||||||
|
let mut final_status = status;
|
||||||
|
if status == SyncStatus::Synced
|
||||||
|
&& dotfile.template
|
||||||
|
&& template_outdated(&state, &preview_engine, &full_source, &dotfile.target)?
|
||||||
|
{
|
||||||
|
final_status = SyncStatus::SourceChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
match final_status {
|
||||||
SyncStatus::Synced => {
|
SyncStatus::Synced => {
|
||||||
tracing::debug!(target = %dotfile.target.display(), "synced");
|
tracing::debug!(target = %dotfile.target.display(), "synced");
|
||||||
}
|
}
|
||||||
|
|
@ -435,13 +458,6 @@ pub fn run(config_path: Option<PathBuf>, dry_run: bool, prune: bool) -> anyhow::
|
||||||
.filter(|batch| !batch.is_empty())
|
.filter(|batch| !batch.is_empty())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let mut template_vars = evaluator.get_template_variables();
|
|
||||||
super::decrypt_encrypted_vars_with_source_dir(
|
|
||||||
&result,
|
|
||||||
&config,
|
|
||||||
&mut template_vars,
|
|
||||||
Some(&source_dir),
|
|
||||||
)?;
|
|
||||||
let deployer = Deployer::new(config, result.sandbox, Some(&template_vars));
|
let deployer = Deployer::new(config, result.sandbox, Some(&template_vars));
|
||||||
|
|
||||||
let progress = if !dry_run {
|
let progress = if !dry_run {
|
||||||
|
|
@ -869,6 +885,48 @@ fn common_path_prefix(paths: &[PathBuf]) -> PathBuf {
|
||||||
prefix
|
prefix
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Renders a template file and returns its BLAKE3 hash.
|
||||||
|
fn rendered_template_hash(
|
||||||
|
engine: &TemplateEngine,
|
||||||
|
source_path: &Path,
|
||||||
|
) -> anyhow::Result<Option<String>> {
|
||||||
|
if !source_path.is_file() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let content = std::fs::read_to_string(source_path)?;
|
||||||
|
let rendered = engine
|
||||||
|
.render(&content)
|
||||||
|
.map_err(|e| anyhow::anyhow!("template render error: {}", e))?;
|
||||||
|
let hash = blake3::hash(rendered.as_bytes()).to_hex().to_string();
|
||||||
|
Ok(Some(hash))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the rendered template output differs from the last deployed content.
|
||||||
|
pub(crate) fn template_outdated(
|
||||||
|
state: &StateStore,
|
||||||
|
engine: &TemplateEngine,
|
||||||
|
source_path: &Path,
|
||||||
|
target_path: &Path,
|
||||||
|
) -> anyhow::Result<bool> {
|
||||||
|
if !source_path.is_file() {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(record) = state.get_deployment(target_path) else {
|
||||||
|
return Ok(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if !record.template {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(rendered_hash) = rendered_template_hash(engine, source_path)? {
|
||||||
|
return Ok(rendered_hash != record.target_hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
/// Expands dotfile patterns (from `dotfiles:` blocks) into individual DotfileConfig entries.
|
/// Expands dotfile patterns (from `dotfiles:` blocks) into individual DotfileConfig entries.
|
||||||
/// Returns the number of entries added.
|
/// Returns the number of entries added.
|
||||||
fn expand_dotfile_patterns(
|
fn expand_dotfile_patterns(
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,9 @@
|
||||||
use super::{find_config_file, parse_config, type_check};
|
use super::{
|
||||||
|
apply::template_outdated, decrypt_encrypted_vars_with_source_dir, find_config_file,
|
||||||
|
parse_config, type_check,
|
||||||
|
};
|
||||||
|
use doot_core::Config;
|
||||||
|
use doot_core::deploy::TemplateEngine;
|
||||||
use doot_core::state::{StateStore, SyncStatus};
|
use doot_core::state::{StateStore, SyncStatus};
|
||||||
use doot_lang::Evaluator;
|
use doot_lang::Evaluator;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
@ -16,9 +21,24 @@ pub fn run(config_path: Option<PathBuf>) -> anyhow::Result<()> {
|
||||||
|
|
||||||
let mut evaluator = Evaluator::new().with_source_dir(source_dir.clone());
|
let mut evaluator = Evaluator::new().with_source_dir(source_dir.clone());
|
||||||
let result = evaluator.eval_sync(&program)?;
|
let result = evaluator.eval_sync(&program)?;
|
||||||
let state_file = source_dir.join(".doot-state.json");
|
|
||||||
|
// Prepare template variables early for preview rendering
|
||||||
|
let config = Config::new(source_dir.clone());
|
||||||
|
let state_file = config.state_file.clone();
|
||||||
let state = StateStore::new(&state_file);
|
let state = StateStore::new(&state_file);
|
||||||
|
|
||||||
|
let mut template_vars = evaluator.get_template_variables();
|
||||||
|
decrypt_encrypted_vars_with_source_dir(
|
||||||
|
&result,
|
||||||
|
&config,
|
||||||
|
&mut template_vars,
|
||||||
|
Some(&source_dir),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Initialize preview TemplateEngine
|
||||||
|
let mut preview_engine = TemplateEngine::new();
|
||||||
|
preview_engine.set_doot_variables(&template_vars);
|
||||||
|
|
||||||
println!("doot status");
|
println!("doot status");
|
||||||
println!("===========\n");
|
println!("===========\n");
|
||||||
|
|
||||||
|
|
@ -27,7 +47,7 @@ pub fn run(config_path: Option<PathBuf>) -> anyhow::Result<()> {
|
||||||
let target = &dotfile.target;
|
let target = &dotfile.target;
|
||||||
let full_source = source_dir.join(&dotfile.source);
|
let full_source = source_dir.join(&dotfile.source);
|
||||||
|
|
||||||
let sync = state.check_sync_status_with_config(
|
let mut sync = state.check_sync_status_with_config(
|
||||||
&full_source,
|
&full_source,
|
||||||
target,
|
target,
|
||||||
Some(dotfile.template),
|
Some(dotfile.template),
|
||||||
|
|
@ -36,6 +56,13 @@ pub fn run(config_path: Option<PathBuf>) -> anyhow::Result<()> {
|
||||||
dotfile.owner.as_deref(),
|
dotfile.owner.as_deref(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Check for template rendering changes
|
||||||
|
if sync == SyncStatus::Synced && dotfile.template {
|
||||||
|
if template_outdated(&state, &preview_engine, &full_source, target)? {
|
||||||
|
sync = SyncStatus::SourceChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let (status, extra) = if target.is_symlink() {
|
let (status, extra) = if target.is_symlink() {
|
||||||
let link_target = std::fs::read_link(target).ok();
|
let link_target = std::fs::read_link(target).ok();
|
||||||
if link_target.as_ref() == Some(&full_source) {
|
if link_target.as_ref() == Some(&full_source) {
|
||||||
|
|
|
||||||
|
|
@ -15,14 +15,22 @@ impl Sandbox {
|
||||||
Self { path }
|
Self { path }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run(&self, args: &[&str]) -> std::process::Output {
|
fn run_with_env<F>(&self, args: &[&str], configure: F) -> std::process::Output
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut Command),
|
||||||
|
{
|
||||||
let doot = env!("CARGO_BIN_EXE_doot");
|
let doot = env!("CARGO_BIN_EXE_doot");
|
||||||
Command::new(doot)
|
let mut command = Command::new(doot);
|
||||||
|
command
|
||||||
.args(args)
|
.args(args)
|
||||||
.env("DOOT_HOME", &self.path)
|
.env("DOOT_HOME", &self.path)
|
||||||
.env("DOOT_TEST_MODE", "1")
|
.env("DOOT_TEST_MODE", "1");
|
||||||
.output()
|
configure(&mut command);
|
||||||
.expect("failed to run doot")
|
command.output().expect("failed to run doot")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, args: &[&str]) -> std::process::Output {
|
||||||
|
self.run_with_env(args, |_| {})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn config_dir(&self) -> PathBuf {
|
fn config_dir(&self) -> PathBuf {
|
||||||
|
|
@ -213,6 +221,51 @@ fn test_apply_copy_unchanged_on_rerun() {
|
||||||
assert!(!target.is_symlink(), "target should still be a copy");
|
assert!(!target.is_symlink(), "target should still be a copy");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_template_redeploys_when_env_changes() {
|
||||||
|
let sandbox = Sandbox::new("template-env-change");
|
||||||
|
sandbox.write_config(
|
||||||
|
r#"
|
||||||
|
dotfile:
|
||||||
|
source = "templates/app.conf"
|
||||||
|
target = "~/.config/app/app.conf"
|
||||||
|
template = true
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
sandbox.write_source("templates/app.conf", "value = {{ env.TEMPLATE_VAL }}\n");
|
||||||
|
|
||||||
|
let first = sandbox.run_with_env(&["apply"], |cmd| {
|
||||||
|
cmd.env("TEMPLATE_VAL", "one");
|
||||||
|
});
|
||||||
|
assert!(first.status.success(), "first apply failed: {:?}", first);
|
||||||
|
|
||||||
|
let target = sandbox.path.join(".config/app/app.conf");
|
||||||
|
assert!(target.exists(), "template target missing after first apply");
|
||||||
|
let first_content = std::fs::read_to_string(&target).unwrap();
|
||||||
|
assert!(
|
||||||
|
first_content.contains("one"),
|
||||||
|
"expected first render to contain env value"
|
||||||
|
);
|
||||||
|
|
||||||
|
let second = sandbox.run_with_env(&["apply"], |cmd| {
|
||||||
|
cmd.env("TEMPLATE_VAL", "two");
|
||||||
|
});
|
||||||
|
assert!(second.status.success(), "second apply failed: {:?}", second);
|
||||||
|
|
||||||
|
let stdout = String::from_utf8_lossy(&second.stdout);
|
||||||
|
assert!(
|
||||||
|
stdout.contains("[source changed]"),
|
||||||
|
"expected source change notice, got: {}",
|
||||||
|
stdout
|
||||||
|
);
|
||||||
|
|
||||||
|
let updated = std::fs::read_to_string(&target).unwrap();
|
||||||
|
assert!(
|
||||||
|
updated.contains("two"),
|
||||||
|
"template should reflect new env value"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_status_shows_state() {
|
fn test_status_shows_state() {
|
||||||
let sandbox = Sandbox::new("status");
|
let sandbox = Sandbox::new("status");
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue