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::{Config, DeployAction, Deployer};
|
||||
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 = 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)
|
||||
let mut deploy_set: HashSet<usize> = HashSet::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 {
|
||||
// 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 => {
|
||||
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())
|
||||
.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 progress = if !dry_run {
|
||||
|
|
@ -869,6 +885,48 @@ fn common_path_prefix(paths: &[PathBuf]) -> PathBuf {
|
|||
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.
|
||||
/// Returns the number of entries added.
|
||||
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_lang::Evaluator;
|
||||
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 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 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!("===========\n");
|
||||
|
||||
|
|
@ -27,7 +47,7 @@ pub fn run(config_path: Option<PathBuf>) -> anyhow::Result<()> {
|
|||
let target = &dotfile.target;
|
||||
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,
|
||||
target,
|
||||
Some(dotfile.template),
|
||||
|
|
@ -36,6 +56,13 @@ pub fn run(config_path: Option<PathBuf>) -> anyhow::Result<()> {
|
|||
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 link_target = std::fs::read_link(target).ok();
|
||||
if link_target.as_ref() == Some(&full_source) {
|
||||
|
|
|
|||
|
|
@ -15,14 +15,22 @@ impl Sandbox {
|
|||
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");
|
||||
Command::new(doot)
|
||||
let mut command = Command::new(doot);
|
||||
command
|
||||
.args(args)
|
||||
.env("DOOT_HOME", &self.path)
|
||||
.env("DOOT_TEST_MODE", "1")
|
||||
.output()
|
||||
.expect("failed to run doot")
|
||||
.env("DOOT_TEST_MODE", "1");
|
||||
configure(&mut command);
|
||||
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 {
|
||||
|
|
@ -213,6 +221,51 @@ fn test_apply_copy_unchanged_on_rerun() {
|
|||
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]
|
||||
fn test_status_shows_state() {
|
||||
let sandbox = Sandbox::new("status");
|
||||
|
|
|
|||
Loading…
Reference in a new issue