use std::collections::HashMap;
use std::path::Path;
use crate::config::ModuleRegistryEntry;
use crate::errors::{ModuleError, Result};
use super::LoadedModule;
use super::git::{
GitSource, clone_repo, fetch_existing_repo, fetch_git_source, get_head_commit_sha,
git_cache_dir, is_git_source, open_repo, parse_git_source,
};
use super::loader::load_module;
use super::lockfile::hash_module_contents;
pub fn is_registry_ref(name: &str) -> bool {
name.contains('/') && !is_git_source(name)
}
pub struct RegistryRef {
pub registry: String,
pub module: String,
pub tag: Option<String>,
}
pub fn parse_registry_ref(input: &str) -> Option<RegistryRef> {
let (registry, remainder) = input.split_once('/')?;
if registry.is_empty() || remainder.is_empty() {
return None;
}
let (module, tag) = match remainder.split_once('@') {
Some((m, t)) if !m.is_empty() && !t.is_empty() => (m.to_string(), Some(t.to_string())),
Some((_, _)) => return None, None => (remainder.to_string(), None),
};
Some(RegistryRef {
registry: registry.to_string(),
module,
tag,
})
}
pub fn resolve_profile_module_name(profile_ref: &str) -> &str {
if is_registry_ref(profile_ref) {
profile_ref
.split_once('/')
.map(|(_, m)| m)
.unwrap_or(profile_ref)
} else {
profile_ref
}
}
#[derive(Debug, Clone)]
pub struct FetchedRemoteModule {
pub module: LoadedModule,
pub commit: String,
pub integrity: String,
}
pub fn fetch_remote_module(
url: &str,
cache_base: &Path,
printer: &crate::output::Printer,
) -> Result<FetchedRemoteModule> {
let git_src = parse_git_source(url)?;
if git_src.git_ref.is_some() {
return Err(ModuleError::UnpinnedRemoteModule {
name: url.to_string(),
}
.into());
}
if git_src.tag.is_none() {
return Err(ModuleError::UnpinnedRemoteModule {
name: url.to_string(),
}
.into());
}
let local_path = fetch_git_source(&git_src, cache_base, "remote", printer)?;
let repo_dir = git_cache_dir(cache_base, &git_src.repo_url);
let commit = get_head_commit_sha(&repo_dir)?;
let module = load_module(&local_path)?;
let integrity = hash_module_contents(&local_path)?;
Ok(FetchedRemoteModule {
module,
commit,
integrity,
})
}
#[derive(Debug, Clone)]
pub struct RegistryModule {
pub name: String,
pub description: String,
pub registry: String,
pub tags: Vec<String>,
}
pub fn extract_registry_name(url: &str) -> Option<String> {
if let Some(rest) = url
.strip_prefix("https://github.com/")
.or_else(|| url.strip_prefix("http://github.com/"))
{
return rest.split('/').next().map(|s| s.to_string());
}
if let Some(rest) = url.strip_prefix("git@github.com:") {
return rest.split('/').next().map(|s| s.to_string());
}
None
}
pub fn fetch_registry_modules(
registry: &ModuleRegistryEntry,
cache_base: &Path,
printer: &crate::output::Printer,
) -> Result<Vec<RegistryModule>> {
let git_src = GitSource {
repo_url: registry.url.clone(),
tag: None,
git_ref: None,
subdir: None,
};
let cache_dir = git_cache_dir(cache_base, ®istry.url);
if cache_dir.join(".git").exists() || cache_dir.join("HEAD").exists() {
fetch_existing_repo(&cache_dir, &git_src, ®istry.name, printer)?;
} else {
clone_repo(&cache_dir, &git_src, ®istry.name, printer)?;
}
let modules_dir = cache_dir.join("modules");
if !modules_dir.is_dir() {
return Err(ModuleError::SourceFetchFailed {
url: registry.url.clone(),
message: "registry repo has no modules/ directory".into(),
}
.into());
}
let module_tags = list_module_tags(&cache_dir, ®istry.name)?;
let mut found = Vec::new();
let entries = std::fs::read_dir(&modules_dir)?;
for entry in entries {
let entry = entry?;
let path = entry.path();
if !path.is_dir() {
continue;
}
let module_yaml = path.join("module.yaml");
if !module_yaml.exists() {
continue;
}
let mod_name = match path.file_name().and_then(|n| n.to_str()) {
Some(n) => n.to_string(),
None => continue,
};
let description = std::fs::read_to_string(&module_yaml)
.ok()
.and_then(|c| crate::config::parse_module(&c).ok())
.and_then(|doc| doc.metadata.description.clone())
.unwrap_or_default();
let tags = module_tags.get(&mod_name).cloned().unwrap_or_default();
found.push(RegistryModule {
name: mod_name,
description,
registry: registry.name.clone(),
tags,
});
}
found.sort_by(|a, b| a.name.cmp(&b.name));
Ok(found)
}
fn list_module_tags(repo_path: &Path, source_name: &str) -> Result<HashMap<String, Vec<String>>> {
let repo = open_repo(repo_path, source_name, "")?;
let tag_names = repo
.tag_names(None)
.map_err(|e| ModuleError::GitFetchFailed {
module: source_name.to_string(),
url: String::new(),
message: format!("cannot list tags: {e}"),
})?;
Ok(group_module_tags(tag_names.iter().flatten()))
}
pub(super) fn group_module_tags<'a, I>(tag_names: I) -> HashMap<String, Vec<String>>
where
I: IntoIterator<Item = &'a str>,
{
let mut result: HashMap<String, Vec<String>> = HashMap::new();
for tag_name in tag_names {
if let Some((module, version)) = tag_name.split_once('/') {
result
.entry(module.to_string())
.or_default()
.push(version.to_string());
}
}
for tags in result.values_mut() {
tags.sort_by(|a, b| {
let av = crate::parse_loose_version(a);
let bv = crate::parse_loose_version(b);
match (av, bv) {
(Some(av), Some(bv)) => av.cmp(&bv),
_ => a.cmp(b),
}
});
}
result
}
pub fn latest_module_version(
registry: &ModuleRegistryEntry,
module_name: &str,
cache_base: &Path,
) -> Result<Option<String>> {
let cache_dir = git_cache_dir(cache_base, ®istry.url);
let tags = list_module_tags(&cache_dir, ®istry.name)?;
Ok(tags.get(module_name).and_then(|t| t.last()).cloned())
}