use crate::git::GitRepo;
use crate::manifest::{DetailedDependency, ResourceDependency};
use crate::pattern::PatternResolver;
use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
use tracing::debug;
pub async fn expand_pattern_to_concrete_deps(
dep: &ResourceDependency,
resource_type: crate::core::ResourceType,
source_manager: &crate::source::SourceManager,
cache: &crate::cache::Cache,
manifest_dir: Option<&Path>,
) -> Result<Vec<(String, ResourceDependency)>> {
let pattern = dep.get_path();
if dep.is_local() {
expand_local_pattern(dep, pattern, manifest_dir).await
} else {
expand_remote_pattern(dep, pattern, resource_type, source_manager, cache).await
}
}
async fn expand_local_pattern(
dep: &ResourceDependency,
pattern: &str,
manifest_dir: Option<&Path>,
) -> Result<Vec<(String, ResourceDependency)>> {
let pattern_path = Path::new(pattern);
let (base_path, search_pattern) = if pattern_path.is_absolute() {
let components: Vec<_> = pattern_path.components().collect();
let glob_idx = components.iter().position(|c| {
let s = c.as_os_str().to_string_lossy();
s.contains('*') || s.contains('?') || s.contains('[')
});
if let Some(idx) = glob_idx {
let base_components = &components[..idx];
let pattern_components = &components[idx..];
let base: PathBuf = base_components.iter().collect();
let pattern: String = pattern_components
.iter()
.map(|c| c.as_os_str().to_string_lossy())
.collect::<Vec<_>>()
.join("/");
(base, pattern)
} else {
(PathBuf::from("."), pattern.to_string())
}
} else {
let base = manifest_dir.map(|p| p.to_path_buf()).unwrap_or_else(|| PathBuf::from("."));
(base, pattern.to_string())
};
let pattern_resolver = PatternResolver::new();
let matches = pattern_resolver.resolve(&search_pattern, &base_path)?;
debug!("Pattern '{}' matched {} files", pattern, matches.len());
let (tool, target, flatten) = match dep {
ResourceDependency::Detailed(d) => (d.tool.clone(), d.target.clone(), d.flatten),
_ => (None, None, None),
};
let mut concrete_deps = Vec::new();
for matched_path in matches {
let absolute_path = base_path.join(&matched_path);
let concrete_path = absolute_path.to_string_lossy().to_string();
let source_context = if let Some(manifest_dir) = manifest_dir {
crate::resolver::source_context::SourceContext::local(manifest_dir)
} else {
crate::resolver::source_context::SourceContext::local(&base_path)
};
let dep_name = generate_dependency_name(&concrete_path, &source_context);
let concrete_dep = ResourceDependency::Detailed(Box::new(DetailedDependency {
path: concrete_path,
source: None,
version: None,
branch: None,
rev: None,
command: None,
args: None,
target: target.clone(),
filename: None,
dependencies: None,
tool: tool.clone(),
flatten,
install: None,
template_vars: Some(serde_json::Value::Object(serde_json::Map::new())),
}));
concrete_deps.push((dep_name, concrete_dep));
}
Ok(concrete_deps)
}
async fn expand_remote_pattern(
dep: &ResourceDependency,
pattern: &str,
_resource_type: crate::core::ResourceType,
source_manager: &crate::source::SourceManager,
cache: &crate::cache::Cache,
) -> Result<Vec<(String, ResourceDependency)>> {
let source_name = dep
.get_source()
.ok_or_else(|| anyhow::anyhow!("Remote pattern dependency missing source: {}", pattern))?;
let source_url = source_manager
.get_source_url(source_name)
.with_context(|| format!("Source not found: {}", source_name))?;
let repo_path = cache
.get_or_clone_source(source_name, &source_url, dep.get_version())
.await
.with_context(|| format!("Failed to access source repository: {}", source_name))?;
let repo = GitRepo::new(&repo_path);
let version = dep.get_version().unwrap_or("HEAD");
let commit_sha = repo.resolve_to_sha(Some(version)).await.with_context(|| {
format!("Failed to resolve version '{}' for source {}", version, source_name)
})?;
let worktree_path = cache
.get_or_create_worktree_for_sha(source_name, &source_url, &commit_sha, Some(version))
.await
.with_context(|| format!("Failed to create worktree for {}@{}", source_name, version))?;
let pattern_resolver = PatternResolver::new();
let matches = pattern_resolver.resolve(pattern, &worktree_path)?;
debug!("Remote pattern '{}' in {} matched {} files", pattern, source_name, matches.len());
let (tool, target, flatten) = match dep {
ResourceDependency::Detailed(d) => (d.tool.clone(), d.target.clone(), d.flatten),
_ => (None, None, None),
};
let mut concrete_deps = Vec::new();
for matched_path in matches {
let source_context = crate::resolver::source_context::SourceContext::git(&worktree_path);
let dep_name = generate_dependency_name(&matched_path.to_string_lossy(), &source_context);
let concrete_dep = ResourceDependency::Detailed(Box::new(DetailedDependency {
path: matched_path.to_string_lossy().to_string(),
source: Some(source_name.to_string()),
version: Some(commit_sha.clone()),
branch: None,
rev: None,
command: None,
args: None,
target: target.clone(),
filename: None,
dependencies: None,
tool: tool.clone(),
flatten,
install: None,
template_vars: Some(serde_json::Value::Object(serde_json::Map::new())),
}));
concrete_deps.push((dep_name, concrete_dep));
}
Ok(concrete_deps)
}
pub fn generate_dependency_name(
path: &str,
source_context: &crate::resolver::source_context::SourceContext,
) -> String {
crate::resolver::source_context::compute_canonical_name(path, source_context)
}
use crate::core::ResourceType;
use std::collections::HashMap;
use super::types::ResolutionCore;
use super::version_resolver::VersionResolutionService;
pub struct PatternExpansionService {
pattern_alias_map: HashMap<(ResourceType, String), String>,
}
impl PatternExpansionService {
pub fn new() -> Self {
Self {
pattern_alias_map: HashMap::new(),
}
}
pub async fn expand_pattern(
&mut self,
core: &ResolutionCore,
dep: &ResourceDependency,
resource_type: ResourceType,
_version_service: &VersionResolutionService,
) -> Result<Vec<(String, ResourceDependency)>> {
expand_pattern_to_concrete_deps(
dep,
resource_type,
&core.source_manager,
&core.cache,
None, )
.await
}
pub fn get_pattern_alias(&self, resource_type: ResourceType, name: &str) -> Option<&String> {
self.pattern_alias_map.get(&(resource_type, name.to_string()))
}
pub fn add_pattern_alias(
&mut self,
resource_type: ResourceType,
concrete_name: String,
pattern_name: String,
) {
self.pattern_alias_map.insert((resource_type, concrete_name), pattern_name);
}
}
impl Default for PatternExpansionService {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::manifest::DetailedDependency;
#[tokio::test]
async fn test_expand_local_pattern() {
let dep = ResourceDependency::Detailed(Box::new(DetailedDependency {
path: "tests/fixtures/*.md".to_string(),
source: None,
version: None,
branch: None,
rev: None,
command: None,
args: None,
target: None,
filename: None,
dependencies: None,
tool: None,
flatten: None,
install: None,
template_vars: Some(serde_json::Value::Object(serde_json::Map::new())),
}));
match expand_local_pattern(&dep, "tests/fixtures/*.md", None).await {
Ok(_) => println!("Pattern expansion succeeded"),
Err(e) => println!("Pattern expansion failed (expected in test): {}", e),
}
}
}