use serde::Serialize;
use crate::instructions::render_template_str;
#[derive(Debug, Clone, Serialize)]
pub struct WorktreeTemplateContext {
pub enabled: bool,
pub strategy: String,
pub layout_dir_name: String,
pub integration_mode: String,
pub default_branch: String,
pub project_root: String,
}
impl Default for WorktreeTemplateContext {
fn default() -> Self {
Self {
enabled: false,
strategy: String::new(),
layout_dir_name: "ito-worktrees".to_string(),
integration_mode: String::new(),
default_branch: "main".to_string(),
project_root: String::new(),
}
}
}
pub fn render_project_template(
template_bytes: &[u8],
ctx: &WorktreeTemplateContext,
) -> Result<Vec<u8>, minijinja::Error> {
let Ok(text) = std::str::from_utf8(template_bytes) else {
return Ok(template_bytes.to_vec());
};
if !contains_jinja2_syntax(text) {
return Ok(template_bytes.to_vec());
}
let rendered = render_template_str(text, ctx)?;
Ok(rendered.into_bytes())
}
fn contains_jinja2_syntax(text: &str) -> bool {
text.contains("{%") || text.contains("{{")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn render_project_template_passes_plain_text_through() {
let bytes = b"Hello, this is plain text.";
let ctx = WorktreeTemplateContext::default();
let result = render_project_template(bytes, &ctx).unwrap();
assert_eq!(result, bytes);
}
#[test]
fn render_project_template_passes_non_utf8_through() {
let bytes = [0xff, 0x00, 0x41];
let ctx = WorktreeTemplateContext::default();
let result = render_project_template(&bytes, &ctx).unwrap();
assert_eq!(result, bytes);
}
#[test]
fn render_project_template_renders_simple_variable() {
let template = b"Strategy: {{ strategy }}";
let ctx = WorktreeTemplateContext {
strategy: "checkout_subdir".to_string(),
..Default::default()
};
let result = render_project_template(template, &ctx).unwrap();
assert_eq!(
String::from_utf8(result).unwrap(),
"Strategy: checkout_subdir"
);
}
#[test]
fn render_project_template_renders_conditional() {
let template = b"{% if enabled %}Worktrees ON{% else %}Worktrees OFF{% endif %}";
let ctx_enabled = WorktreeTemplateContext {
enabled: true,
strategy: "checkout_subdir".to_string(),
..Default::default()
};
let ctx_disabled = WorktreeTemplateContext::default();
let on = render_project_template(template, &ctx_enabled).unwrap();
assert_eq!(String::from_utf8(on).unwrap(), "Worktrees ON");
let off = render_project_template(template, &ctx_disabled).unwrap();
assert_eq!(String::from_utf8(off).unwrap(), "Worktrees OFF");
}
#[test]
fn render_project_template_strict_on_undefined() {
let template = b"{{ missing_var }}";
let ctx = WorktreeTemplateContext::default();
let err = render_project_template(template, &ctx).unwrap_err();
assert_eq!(err.kind(), minijinja::ErrorKind::UndefinedError);
}
#[test]
fn default_context_is_disabled() {
let ctx = WorktreeTemplateContext::default();
assert!(!ctx.enabled);
assert!(ctx.strategy.is_empty());
assert!(ctx.integration_mode.is_empty());
assert_eq!(ctx.layout_dir_name, "ito-worktrees");
assert_eq!(ctx.default_branch, "main");
assert!(ctx.project_root.is_empty());
}
#[test]
fn render_agents_md_with_checkout_subdir() {
let agents_md = crate::default_project_files()
.into_iter()
.find(|f| f.relative_path == "AGENTS.md")
.expect("AGENTS.md should exist in project templates");
let ctx = WorktreeTemplateContext {
enabled: true,
strategy: "checkout_subdir".to_string(),
layout_dir_name: "ito-worktrees".to_string(),
integration_mode: "commit_pr".to_string(),
default_branch: "main".to_string(),
project_root: "/home/user/project".to_string(),
};
let rendered = render_project_template(agents_md.contents, &ctx).unwrap();
let text = String::from_utf8(rendered).unwrap();
assert!(text.contains("## Worktree Workflow"));
assert!(text.contains("**Strategy:** `checkout_subdir`"));
assert!(
text.contains("git worktree add \".ito-worktrees/<change-name>\" -b <change-name>")
);
assert!(
text.contains(".ito-worktrees/<change-name>/"),
"should contain repo-relative worktree path"
);
assert!(
!text.contains(&ctx.project_root),
"should not embed machine-specific absolute project_root"
);
}
#[test]
fn render_agents_md_with_checkout_siblings() {
let agents_md = crate::default_project_files()
.into_iter()
.find(|f| f.relative_path == "AGENTS.md")
.expect("AGENTS.md should exist in project templates");
let ctx = WorktreeTemplateContext {
enabled: true,
strategy: "checkout_siblings".to_string(),
layout_dir_name: "worktrees".to_string(),
integration_mode: "merge_parent".to_string(),
default_branch: "develop".to_string(),
project_root: "/home/user/project".to_string(),
};
let rendered = render_project_template(agents_md.contents, &ctx).unwrap();
let text = String::from_utf8(rendered).unwrap();
assert!(text.contains("**Strategy:** `checkout_siblings`"));
assert!(text.contains(
"git worktree add \"../<project-name>-worktrees/<change-name>\" -b <change-name>"
));
assert!(
text.contains("../<project-name>-worktrees/<change-name>/"),
"should contain repo-relative sibling worktree path"
);
assert!(
!text.contains(&ctx.project_root),
"should not embed machine-specific absolute project_root"
);
}
#[test]
fn render_agents_md_with_bare_control_siblings() {
let agents_md = crate::default_project_files()
.into_iter()
.find(|f| f.relative_path == "AGENTS.md")
.expect("AGENTS.md should exist in project templates");
let ctx = WorktreeTemplateContext {
enabled: true,
strategy: "bare_control_siblings".to_string(),
layout_dir_name: "ito-worktrees".to_string(),
integration_mode: "commit_pr".to_string(),
default_branch: "main".to_string(),
project_root: "/home/user/project".to_string(),
};
let rendered = render_project_template(agents_md.contents, &ctx).unwrap();
let text = String::from_utf8(rendered).unwrap();
assert!(text.contains("**Strategy:** `bare_control_siblings`"));
assert!(text.contains(".bare/"));
assert!(text.contains("ito-worktrees/"));
assert!(
text.contains(
"git worktree add \"../ito-worktrees/<change-name>\" -b <change-name> main"
)
);
assert!(text.contains("Do not create them from the bare/control repo placeholder `HEAD`"));
let layout_line = text
.lines()
.find(|l| l.contains("# bare/control repo"))
.expect("should contain bare/control repo layout line");
assert!(
layout_line.contains("../"),
"should contain repo-relative bare/control layout"
);
assert!(
!text.contains(&ctx.project_root),
"should not embed machine-specific absolute project_root"
);
}
#[test]
fn render_agents_md_with_worktrees_disabled() {
let agents_md = crate::default_project_files()
.into_iter()
.find(|f| f.relative_path == "AGENTS.md")
.expect("AGENTS.md should exist in project templates");
let ctx = WorktreeTemplateContext::default();
let rendered = render_project_template(agents_md.contents, &ctx).unwrap();
let text = String::from_utf8(rendered).unwrap();
assert!(text.contains("Worktrees are not configured for this project."));
assert!(text.contains("Do NOT create git worktrees by default."));
}
}