1use std::collections::HashMap;
5use std::path::Path;
6
7use crate::config::ModuleRegistryEntry;
8use crate::errors::{ModuleError, Result};
9
10use super::LoadedModule;
11use super::git::{
12 GitSource, clone_repo, fetch_existing_repo, fetch_git_source, get_head_commit_sha,
13 git_cache_dir, is_git_source, open_repo, parse_git_source,
14};
15use super::loader::load_module;
16use super::lockfile::hash_module_contents;
17
18pub fn is_registry_ref(name: &str) -> bool {
21 name.contains('/') && !is_git_source(name)
22}
23
24pub struct RegistryRef {
26 pub registry: String,
27 pub module: String,
28 pub tag: Option<String>,
29}
30
31pub fn parse_registry_ref(input: &str) -> Option<RegistryRef> {
34 let (registry, remainder) = input.split_once('/')?;
36 if registry.is_empty() || remainder.is_empty() {
37 return None;
38 }
39
40 let (module, tag) = match remainder.split_once('@') {
42 Some((m, t)) if !m.is_empty() && !t.is_empty() => (m.to_string(), Some(t.to_string())),
43 Some((_, _)) => return None, None => (remainder.to_string(), None),
45 };
46
47 Some(RegistryRef {
48 registry: registry.to_string(),
49 module,
50 tag,
51 })
52}
53
54pub fn resolve_profile_module_name(profile_ref: &str) -> &str {
62 if is_registry_ref(profile_ref) {
63 profile_ref
64 .split_once('/')
65 .map(|(_, m)| m)
66 .unwrap_or(profile_ref)
67 } else {
68 profile_ref
69 }
70}
71
72#[derive(Debug, Clone)]
74pub struct FetchedRemoteModule {
75 pub module: LoadedModule,
76 pub commit: String,
77 pub integrity: String,
78}
79
80pub fn fetch_remote_module(
85 url: &str,
86 cache_base: &Path,
87 printer: &crate::output::Printer,
88) -> Result<FetchedRemoteModule> {
89 let git_src = parse_git_source(url)?;
90
91 if git_src.git_ref.is_some() {
95 return Err(ModuleError::UnpinnedRemoteModule {
96 name: url.to_string(),
97 }
98 .into());
99 }
100 if git_src.tag.is_none() {
101 return Err(ModuleError::UnpinnedRemoteModule {
102 name: url.to_string(),
103 }
104 .into());
105 }
106
107 let local_path = fetch_git_source(&git_src, cache_base, "remote", printer)?;
108
109 let repo_dir = git_cache_dir(cache_base, &git_src.repo_url);
111 let commit = get_head_commit_sha(&repo_dir)?;
112
113 let module = load_module(&local_path)?;
115
116 let integrity = hash_module_contents(&local_path)?;
118
119 Ok(FetchedRemoteModule {
120 module,
121 commit,
122 integrity,
123 })
124}
125
126#[derive(Debug, Clone)]
132pub struct RegistryModule {
133 pub name: String,
135 pub description: String,
137 pub registry: String,
139 pub tags: Vec<String>,
141}
142
143pub fn extract_registry_name(url: &str) -> Option<String> {
146 if let Some(rest) = url
148 .strip_prefix("https://github.com/")
149 .or_else(|| url.strip_prefix("http://github.com/"))
150 {
151 return rest.split('/').next().map(|s| s.to_string());
152 }
153 if let Some(rest) = url.strip_prefix("git@github.com:") {
155 return rest.split('/').next().map(|s| s.to_string());
156 }
157 None
158}
159
160pub fn fetch_registry_modules(
165 registry: &ModuleRegistryEntry,
166 cache_base: &Path,
167 printer: &crate::output::Printer,
168) -> Result<Vec<RegistryModule>> {
169 let git_src = GitSource {
170 repo_url: registry.url.clone(),
171 tag: None,
172 git_ref: None,
173 subdir: None,
174 };
175
176 let cache_dir = git_cache_dir(cache_base, ®istry.url);
177
178 if cache_dir.join(".git").exists() || cache_dir.join("HEAD").exists() {
180 fetch_existing_repo(&cache_dir, &git_src, ®istry.name, printer)?;
181 } else {
182 clone_repo(&cache_dir, &git_src, ®istry.name, printer)?;
183 }
184
185 let modules_dir = cache_dir.join("modules");
186 if !modules_dir.is_dir() {
187 return Err(ModuleError::SourceFetchFailed {
188 url: registry.url.clone(),
189 message: "registry repo has no modules/ directory".into(),
190 }
191 .into());
192 }
193
194 let module_tags = list_module_tags(&cache_dir, ®istry.name)?;
196
197 let mut found = Vec::new();
199 let entries = std::fs::read_dir(&modules_dir)?;
200 for entry in entries {
201 let entry = entry?;
202 let path = entry.path();
203 if !path.is_dir() {
204 continue;
205 }
206 let module_yaml = path.join("module.yaml");
207 if !module_yaml.exists() {
208 continue;
209 }
210 let mod_name = match path.file_name().and_then(|n| n.to_str()) {
211 Some(n) => n.to_string(),
212 None => continue,
213 };
214
215 let description = std::fs::read_to_string(&module_yaml)
217 .ok()
218 .and_then(|c| crate::config::parse_module(&c).ok())
219 .and_then(|doc| doc.metadata.description.clone())
220 .unwrap_or_default();
221
222 let tags = module_tags.get(&mod_name).cloned().unwrap_or_default();
224
225 found.push(RegistryModule {
226 name: mod_name,
227 description,
228 registry: registry.name.clone(),
229 tags,
230 });
231 }
232
233 found.sort_by(|a, b| a.name.cmp(&b.name));
234 Ok(found)
235}
236
237fn list_module_tags(repo_path: &Path, source_name: &str) -> Result<HashMap<String, Vec<String>>> {
241 let repo = open_repo(repo_path, source_name, "")?;
242 let tag_names = repo
243 .tag_names(None)
244 .map_err(|e| ModuleError::GitFetchFailed {
245 module: source_name.to_string(),
246 url: String::new(),
247 message: format!("cannot list tags: {e}"),
248 })?;
249 Ok(group_module_tags(tag_names.iter().flatten()))
250}
251
252pub(super) fn group_module_tags<'a, I>(tag_names: I) -> HashMap<String, Vec<String>>
262where
263 I: IntoIterator<Item = &'a str>,
264{
265 let mut result: HashMap<String, Vec<String>> = HashMap::new();
266 for tag_name in tag_names {
267 if let Some((module, version)) = tag_name.split_once('/') {
268 result
269 .entry(module.to_string())
270 .or_default()
271 .push(version.to_string());
272 }
273 }
274 for tags in result.values_mut() {
275 tags.sort_by(|a, b| {
276 let av = crate::parse_loose_version(a);
279 let bv = crate::parse_loose_version(b);
280 match (av, bv) {
281 (Some(av), Some(bv)) => av.cmp(&bv),
282 _ => a.cmp(b),
283 }
284 });
285 }
286 result
287}
288
289pub fn latest_module_version(
292 registry: &ModuleRegistryEntry,
293 module_name: &str,
294 cache_base: &Path,
295) -> Result<Option<String>> {
296 let cache_dir = git_cache_dir(cache_base, ®istry.url);
297 let tags = list_module_tags(&cache_dir, ®istry.name)?;
298 Ok(tags.get(module_name).and_then(|t| t.last()).cloned())
299}