ralph/template/variables/
detect.rs1use std::path::Path;
23
24use anyhow::{Context, Result};
25
26use crate::runutil::{ManagedCommand, TimeoutClass, execute_checked_command};
27
28use super::context::{TemplateContext, TemplateWarning};
29
30pub fn detect_context_with_warnings(
35 target: Option<&str>,
36 repo_root: &Path,
37 needs_branch: bool,
38) -> (TemplateContext, Vec<TemplateWarning>) {
39 let mut warnings = Vec::new();
40 let target_opt = target.map(|s| s.to_string());
41
42 let file = target_opt.as_ref().map(|t| {
43 Path::new(t)
44 .file_name()
45 .map(|n| n.to_string_lossy().to_string())
46 .unwrap_or_else(|| t.clone())
47 });
48
49 let module = target_opt.as_ref().map(|t| derive_module_name(t));
50
51 let branch = if needs_branch {
52 match detect_git_branch(repo_root) {
53 Ok(branch_opt) => branch_opt,
54 Err(e) => {
55 warnings.push(TemplateWarning::GitBranchDetectionFailed {
56 error: e.to_string(),
57 });
58 None
59 }
60 }
61 } else {
62 None
63 };
64
65 let context = TemplateContext {
66 target: target_opt,
67 file,
68 module,
69 branch,
70 };
71
72 (context, warnings)
73}
74
75pub fn detect_context(target: Option<&str>, repo_root: &Path) -> TemplateContext {
77 let (context, _) = detect_context_with_warnings(target, repo_root, true);
78 context
79}
80
81pub(super) fn derive_module_name(path: &str) -> String {
88 let path_obj = Path::new(path);
89
90 let file_stem = path_obj
92 .file_stem()
93 .map(|s| s.to_string_lossy().to_string())
94 .unwrap_or_else(|| path.to_string());
95
96 let mut components: Vec<String> = Vec::new();
97
98 for component in path_obj.components() {
100 let comp_str = component.as_os_str().to_string_lossy().to_string();
101
102 if comp_str == "src"
104 || comp_str == "lib"
105 || comp_str == "bin"
106 || comp_str == "tests"
107 || comp_str == "examples"
108 || comp_str == "crates"
109 {
110 continue;
111 }
112
113 if comp_str
115 == path_obj
116 .file_name()
117 .map(|n| n.to_string_lossy())
118 .unwrap_or_default()
119 {
120 continue;
121 }
122
123 components.push(comp_str);
124 }
125
126 if !components.is_empty() {
128 components.push(file_stem);
129 components.join("::")
130 } else {
131 file_stem
132 }
133}
134
135fn detect_git_branch(repo_root: &Path) -> Result<Option<String>> {
137 let head_path = repo_root.join(".git/HEAD");
139
140 if !head_path.exists() {
141 let mut command = std::process::Command::new("git");
143 command
144 .arg("-c")
145 .arg("core.fsmonitor=false")
146 .arg("rev-parse")
147 .arg("--abbrev-ref")
148 .arg("HEAD")
149 .current_dir(repo_root);
150
151 let output = execute_checked_command(ManagedCommand::new(
152 command,
153 format!("git rev-parse --abbrev-ref HEAD in {}", repo_root.display()),
154 TimeoutClass::MetadataProbe,
155 ))
156 .context("failed to detect template git branch")?;
157
158 let branch = output.stdout_lossy();
159 if branch != "HEAD" {
160 return Ok(Some(branch));
161 }
162 return Ok(None);
163 }
164
165 let head_content = std::fs::read_to_string(&head_path)
166 .with_context(|| format!("failed to read {:?}", head_path))?;
167 let head_ref = head_content.trim();
168
169 if head_ref.starts_with("ref: refs/heads/") {
171 let branch = head_ref
172 .strip_prefix("ref: refs/heads/")
173 .unwrap_or(head_ref)
174 .to_string();
175 Ok(Some(branch))
176 } else if head_ref.len() == 40 && head_ref.chars().all(|c| c.is_ascii_hexdigit()) {
177 Ok(None)
179 } else if head_ref.is_empty() {
180 Err(anyhow::anyhow!("HEAD file is empty"))
181 } else {
182 Err(anyhow::anyhow!("invalid HEAD content: {}", head_ref))
184 }
185}