1use crate::git::GitRepo;
9use crate::manifest::{DetailedDependency, ResourceDependency};
10use crate::pattern::PatternResolver;
11use anyhow::{Context, Result};
12use std::path::{Path, PathBuf};
13use tracing::debug;
14
15pub async fn expand_pattern_to_concrete_deps(
34 dep: &ResourceDependency,
35 resource_type: crate::core::ResourceType,
36 source_manager: &crate::source::SourceManager,
37 cache: &crate::cache::Cache,
38 manifest_dir: Option<&Path>,
39) -> Result<Vec<(String, ResourceDependency)>> {
40 let pattern = dep.get_path();
41
42 if dep.is_local() {
43 expand_local_pattern(dep, pattern, manifest_dir).await
44 } else {
45 expand_remote_pattern(dep, pattern, resource_type, source_manager, cache).await
46 }
47}
48
49async fn expand_local_pattern(
51 dep: &ResourceDependency,
52 pattern: &str,
53 manifest_dir: Option<&Path>,
54) -> Result<Vec<(String, ResourceDependency)>> {
55 let pattern_path = Path::new(pattern);
58 let (base_path, search_pattern) = if pattern_path.is_absolute() {
59 let components: Vec<_> = pattern_path.components().collect();
62
63 let glob_idx = components.iter().position(|c| {
65 let s = c.as_os_str().to_string_lossy();
66 s.contains('*') || s.contains('?') || s.contains('[')
67 });
68
69 if let Some(idx) = glob_idx {
70 let base_components = &components[..idx];
72 let pattern_components = &components[idx..];
73
74 let base: PathBuf = base_components.iter().collect();
75 let pattern: String = pattern_components
76 .iter()
77 .map(|c| c.as_os_str().to_string_lossy())
78 .collect::<Vec<_>>()
79 .join("/");
80
81 (base, pattern)
82 } else {
83 (PathBuf::from("."), pattern.to_string())
85 }
86 } else {
87 let base = manifest_dir.map(|p| p.to_path_buf()).unwrap_or_else(|| PathBuf::from("."));
89 (base, pattern.to_string())
90 };
91
92 let pattern_resolver = PatternResolver::new();
93 let matches = pattern_resolver.resolve(&search_pattern, &base_path)?;
94
95 debug!("Pattern '{}' matched {} files", pattern, matches.len());
96
97 let (tool, target, flatten) = match dep {
99 ResourceDependency::Detailed(d) => (d.tool.clone(), d.target.clone(), d.flatten),
100 _ => (None, None, None),
101 };
102
103 let mut concrete_deps = Vec::new();
104
105 for matched_path in matches {
106 let absolute_path = base_path.join(&matched_path);
108 let concrete_path = absolute_path.to_string_lossy().to_string();
109
110 let source_context = if let Some(manifest_dir) = manifest_dir {
112 crate::resolver::source_context::SourceContext::local(manifest_dir)
114 } else {
115 crate::resolver::source_context::SourceContext::local(&base_path)
117 };
118
119 let dep_name = generate_dependency_name(&concrete_path, &source_context);
120
121 let concrete_dep = ResourceDependency::Detailed(Box::new(DetailedDependency {
123 path: concrete_path,
124 source: None,
125 version: None,
126 branch: None,
127 rev: None,
128 command: None,
129 args: None,
130 target: target.clone(),
131 filename: None,
132 dependencies: None,
133 tool: tool.clone(),
134 flatten,
135 install: None,
136 template_vars: Some(serde_json::Value::Object(serde_json::Map::new())),
137 }));
138
139 concrete_deps.push((dep_name, concrete_dep));
140 }
141
142 Ok(concrete_deps)
143}
144
145async fn expand_remote_pattern(
147 dep: &ResourceDependency,
148 pattern: &str,
149 _resource_type: crate::core::ResourceType,
150 source_manager: &crate::source::SourceManager,
151 cache: &crate::cache::Cache,
152) -> Result<Vec<(String, ResourceDependency)>> {
153 let source_name = dep
154 .get_source()
155 .ok_or_else(|| anyhow::anyhow!("Remote pattern dependency missing source: {}", pattern))?;
156
157 let source_url = source_manager
158 .get_source_url(source_name)
159 .with_context(|| format!("Source not found: {}", source_name))?;
160
161 let repo_path = cache
163 .get_or_clone_source(source_name, &source_url, dep.get_version())
164 .await
165 .with_context(|| format!("Failed to access source repository: {}", source_name))?;
166
167 let repo = GitRepo::new(&repo_path);
168
169 let version = dep.get_version().unwrap_or("HEAD");
171 let commit_sha = repo.resolve_to_sha(Some(version)).await.with_context(|| {
172 format!("Failed to resolve version '{}' for source {}", version, source_name)
173 })?;
174
175 let worktree_path = cache
177 .get_or_create_worktree_for_sha(source_name, &source_url, &commit_sha, Some(version))
178 .await
179 .with_context(|| format!("Failed to create worktree for {}@{}", source_name, version))?;
180
181 let pattern_resolver = PatternResolver::new();
183 let matches = pattern_resolver.resolve(pattern, &worktree_path)?;
184
185 debug!("Remote pattern '{}' in {} matched {} files", pattern, source_name, matches.len());
186
187 let (tool, target, flatten) = match dep {
189 ResourceDependency::Detailed(d) => (d.tool.clone(), d.target.clone(), d.flatten),
190 _ => (None, None, None),
191 };
192
193 let mut concrete_deps = Vec::new();
194
195 for matched_path in matches {
196 let source_context = crate::resolver::source_context::SourceContext::git(&worktree_path);
199 let dep_name = generate_dependency_name(&matched_path.to_string_lossy(), &source_context);
200
201 let concrete_dep = ResourceDependency::Detailed(Box::new(DetailedDependency {
204 path: matched_path.to_string_lossy().to_string(),
205 source: Some(source_name.to_string()),
206 version: Some(commit_sha.clone()),
207 branch: None,
208 rev: None,
209 command: None,
210 args: None,
211 target: target.clone(),
212 filename: None,
213 dependencies: None,
214 tool: tool.clone(),
215 flatten,
216 install: None,
217 template_vars: Some(serde_json::Value::Object(serde_json::Map::new())),
218 }));
219
220 concrete_deps.push((dep_name, concrete_dep));
221 }
222
223 Ok(concrete_deps)
224}
225
226pub fn generate_dependency_name(
229 path: &str,
230 source_context: &crate::resolver::source_context::SourceContext,
231) -> String {
232 crate::resolver::source_context::compute_canonical_name(path, source_context)
234}
235
236use crate::core::ResourceType;
241use std::collections::HashMap;
242
243use super::types::ResolutionCore;
244use super::version_resolver::VersionResolutionService;
245
246pub struct PatternExpansionService {
251 pattern_alias_map: HashMap<(ResourceType, String), String>,
253}
254
255impl PatternExpansionService {
256 pub fn new() -> Self {
258 Self {
259 pattern_alias_map: HashMap::new(),
260 }
261 }
262
263 pub async fn expand_pattern(
279 &mut self,
280 core: &ResolutionCore,
281 dep: &ResourceDependency,
282 resource_type: ResourceType,
283 _version_service: &VersionResolutionService,
284 ) -> Result<Vec<(String, ResourceDependency)>> {
285 expand_pattern_to_concrete_deps(
287 dep,
288 resource_type,
289 &core.source_manager,
290 &core.cache,
291 None, )
293 .await
294 }
295
296 pub fn get_pattern_alias(&self, resource_type: ResourceType, name: &str) -> Option<&String> {
307 self.pattern_alias_map.get(&(resource_type, name.to_string()))
308 }
309
310 pub fn add_pattern_alias(
318 &mut self,
319 resource_type: ResourceType,
320 concrete_name: String,
321 pattern_name: String,
322 ) {
323 self.pattern_alias_map.insert((resource_type, concrete_name), pattern_name);
324 }
325}
326
327impl Default for PatternExpansionService {
328 fn default() -> Self {
329 Self::new()
330 }
331}
332
333#[cfg(test)]
334mod tests {
335 use super::*;
336 use crate::manifest::DetailedDependency;
337
338 #[tokio::test]
342 async fn test_expand_local_pattern() {
343 let dep = ResourceDependency::Detailed(Box::new(DetailedDependency {
346 path: "tests/fixtures/*.md".to_string(),
347 source: None,
348 version: None,
349 branch: None,
350 rev: None,
351 command: None,
352 args: None,
353 target: None,
354 filename: None,
355 dependencies: None,
356 tool: None,
357 flatten: None,
358 install: None,
359 template_vars: Some(serde_json::Value::Object(serde_json::Map::new())),
360 }));
361
362 match expand_local_pattern(&dep, "tests/fixtures/*.md", None).await {
365 Ok(_) => println!("Pattern expansion succeeded"),
366 Err(e) => println!("Pattern expansion failed (expected in test): {}", e),
367 }
368 }
369}