use anyhow::{anyhow, bail, Context, Result};
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use super::git_source::{resolve_reference, GitReference, GitSource, GitSourceStore};
use super::lockfile::CargoLockPackage;
use super::manifest::CargoToml;
use super::manifest_models::{TomlDependency, TomlDetailedDep};
use super::registry_config::RegistryConfig;
use super::resolver::ManifestDirectDep;
use crate::core::cache_root::GIT_DIR;
#[derive(Debug, Default)]
pub struct GitResolution {
pub packages: Vec<CargoLockPackage>,
pub registry_deps: Vec<ManifestDirectDep>,
}
pub struct GitResolver {
store: GitSourceStore,
config: RegistryConfig,
seen: HashSet<(String, String)>,
}
impl GitResolver {
pub fn for_cache_dir(cache_dir: Option<&Path>, config: RegistryConfig) -> Self {
let root = cache_dir
.map(|c| c.join(GIT_DIR))
.unwrap_or_else(GitSourceStore::default_root);
Self {
store: GitSourceStore::new(root),
config,
seen: HashSet::new(),
}
}
pub fn resolve(
&mut self,
dep_name: &str,
dep: &TomlDetailedDep,
offline: bool,
out: &mut GitResolution,
) -> Result<String> {
let url = dep
.git
.as_deref()
.ok_or_else(|| anyhow!("{dep_name}: not a git dependency"))?;
let reference = match (&dep.tag, &dep.branch, &dep.rev) {
(Some(t), _, _) => GitReference::Tag(t.clone()),
(_, Some(b), _) => GitReference::Branch(b.clone()),
(_, _, Some(r)) => GitReference::Rev(r.clone()),
_ => GitReference::Default,
};
let precise = resolve_reference(&self.store, url, &reference, offline)?;
let src = GitSource {
url: url.to_string(),
reference,
precise,
};
let checkout = self.store.ensure_checkout(&src, offline)?;
let pkg_name = dep
.package_name
.clone()
.unwrap_or_else(|| dep_name.to_string());
let pkg_dir = find_package_dir_by_name(&checkout, &pkg_name).ok_or_else(|| {
anyhow!(
"git dependency {}: commit {} has no package named {pkg_name}",
src.url,
src.precise
)
})?;
self.collect(&src, &checkout, &pkg_dir, offline, out)
}
fn collect(
&mut self,
src: &GitSource,
checkout: &Path,
pkg_dir: &Path,
offline: bool,
out: &mut GitResolution,
) -> Result<String> {
let manifest_path = pkg_dir.join("Cargo.toml");
let mut toml = CargoToml::from_file(&manifest_path)
.with_context(|| format!("git dependency {}: {:?}", src.url, manifest_path))?;
toml.resolve_inherited_workspace_fields(&manifest_path);
let package = toml
.package
.as_ref()
.ok_or_else(|| anyhow!("{:?} has no [package]", manifest_path))?;
let name = package.name.clone();
let version = package
.version
.clone()
.unwrap_or_else(|| "0.0.0".to_string());
let source = src.lock_source();
if !self.seen.insert((source.clone(), name.clone())) {
return Ok(name);
}
let mut dep_names = Vec::new();
let tables = [&toml.dependencies, &toml.build_dependencies]
.into_iter()
.chain(
toml.target
.values()
.flat_map(|t| [&t.dependencies, &t.build_dependencies]),
);
for (dname, dval) in tables.flatten() {
let listed = match dval {
TomlDependency::Simple(req) => {
out.registry_deps
.push(ManifestDirectDep::crates_io(dname.clone(), req.clone()));
dname.clone()
}
TomlDependency::Detailed(d) if d.path.is_some() => {
let rel = d.path.as_deref().unwrap_or(".");
let dir = normalize(&pkg_dir.join(rel));
if !dir.starts_with(normalize(checkout)) {
bail!(
"git dependency {}: {dname} points outside the repository ({rel})",
src.url
);
}
self.collect(src, checkout, &dir, offline, out)?
}
TomlDependency::Detailed(d) if d.git.is_some() => {
self.resolve(dname, d, offline, out)?
}
TomlDependency::Detailed(d) => {
let pkg = d.package_name.clone().unwrap_or_else(|| dname.clone());
out.registry_deps.push(ManifestDirectDep {
name: pkg.clone(),
req: d.version.clone().unwrap_or_else(|| "*".to_string()),
source: self
.config
.lock_source_for_registry(d.registry.as_deref())?,
features: d.features.clone().unwrap_or_default(),
default_features: d.default_features.unwrap_or(true),
});
pkg
}
};
dep_names.push(listed);
}
dep_names.sort();
dep_names.dedup();
out.packages.push(CargoLockPackage {
name: name.clone(),
version,
source: Some(source),
checksum: None,
dependencies: dep_names,
});
Ok(name)
}
}
fn find_package_dir_by_name(checkout: &Path, name: &str) -> Option<PathBuf> {
let root_manifest = checkout.join("Cargo.toml");
let root = CargoToml::from_file(&root_manifest).ok();
if let Some(ref toml) = root {
if toml.package.as_ref().is_some_and(|p| p.name == name) {
return Some(checkout.to_path_buf());
}
if let Ok(members) = toml.discover_workspace_members(checkout) {
if let Some((_, dir)) = members.iter().find(|(m, _)| m == name) {
return Some(dir.clone());
}
}
}
let mut candidates = Vec::new();
for sub in std::fs::read_dir(checkout).ok()?.flatten() {
let p = sub.path();
if !p.is_dir() || sub.file_name().to_string_lossy().starts_with('.') {
continue;
}
candidates.push(p.clone());
if sub.file_name() == "crates" {
candidates.extend(
std::fs::read_dir(&p)
.into_iter()
.flatten()
.flatten()
.map(|e| e.path()),
);
}
}
candidates.into_iter().find(|dir| {
CargoToml::from_file(&dir.join("Cargo.toml"))
.ok()
.and_then(|t| t.package)
.is_some_and(|p| p.name == name)
})
}
fn normalize(path: &Path) -> PathBuf {
let mut out = PathBuf::new();
for comp in path.components() {
match comp {
std::path::Component::ParentDir => {
out.pop();
}
std::path::Component::CurDir => {}
other => out.push(other.as_os_str()),
}
}
out
}