diff --git a/Cargo.lock b/Cargo.lock index 9de4d27..5aea0ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -60,6 +60,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "anyhow" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" + [[package]] name = "autocfg" version = "1.1.0" @@ -428,6 +434,7 @@ checksum = "8bccbff07d5ed689c4087d20d7307a52ab6141edeedf487c3876a55b86cf63df" name = "protofetch" version = "0.0.28" dependencies = [ + "anyhow", "clap", "derive-new", "env_logger", diff --git a/Cargo.toml b/Cargo.toml index a0e93ae..6523cf1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ vendored-openssl = ["git2/vendored-openssl"] vendored-libgit2 = ["git2/vendored-libgit2"] [dependencies] +anyhow = "1.0.75" clap = { version = "4.3.2", features = ["derive"] } derive-new = "0.5.9" env_logger = "0.10.0" diff --git a/src/cache.rs b/src/cache.rs index 17c2687..903b5e4 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -12,7 +12,9 @@ use crate::{ use crate::proto_repository::ProtoRepository; pub trait RepositoryCache { - fn clone_or_update(&self, entry: &Coordinate) -> Result, CacheError>; + type Repository: ProtoRepository; + + fn clone_or_update(&self, entry: &Coordinate) -> Result; } pub struct ProtofetchGitCache { @@ -32,7 +34,9 @@ pub enum CacheError { } impl RepositoryCache for ProtofetchGitCache { - fn clone_or_update(&self, entry: &Coordinate) -> Result, CacheError> { + type Repository = ProtoGitRepository; + + fn clone_or_update(&self, entry: &Coordinate) -> Result { let repo = match self.get_entry(entry) { None => self.clone_repo(entry)?, Some(path) => { @@ -44,7 +48,7 @@ impl RepositoryCache for ProtofetchGitCache { } }; - Ok(Box::new(ProtoGitRepository::new(repo))) + Ok(ProtoGitRepository::new(repo)) } } diff --git a/src/fetch.rs b/src/fetch.rs index 2b19bcf..c51ed2b 100644 --- a/src/fetch.rs +++ b/src/fetch.rs @@ -10,6 +10,8 @@ use crate::{ lock::{LockFile, LockedDependency}, Dependency, DependencyName, Descriptor, RevisionSpecification, }, + proto_repository::ProtoRepository, + resolver::ModuleResolver, }; use log::{debug, error, info}; use thiserror::Error; @@ -30,11 +32,16 @@ pub enum FetchError { ProtoRepoError(#[from] crate::proto_repository::ProtoRepoError), #[error("IO error: {0}")] IO(#[from] std::io::Error), + #[error(transparent)] + Resolver(anyhow::Error), } -pub fn lock(descriptor: &Descriptor, cache: &impl RepositoryCache) -> Result { +pub fn lock( + descriptor: &Descriptor, + resolver: &impl ModuleResolver, +) -> Result { fn go( - cache: &impl RepositoryCache, + resolver: &impl ModuleResolver, resolved: &mut BTreeMap, dependencies: &[Dependency], ) -> Result<(), FetchError> { @@ -43,11 +50,15 @@ pub fn lock(descriptor: &Descriptor, cache: &impl RepositoryCache) -> Result { log::info!("Resolving {:?}", dependency.coordinate); - let repository = cache.clone_or_update(&dependency.coordinate)?; - let commit_hash = repository.resolve_commit_hash(&dependency.specification)?; - let mut descriptor = - repository.extract_descriptor(&dependency.name, &commit_hash)?; - let dependencies = descriptor + let mut resolved_module = resolver + .resolve( + &dependency.coordinate, + &dependency.specification, + &dependency.name, + ) + .map_err(FetchError::Resolver)?; + let dependencies = resolved_module + .descriptor .dependencies .iter() .map(|dep| dep.name.clone()) @@ -55,7 +66,7 @@ pub fn lock(descriptor: &Descriptor, cache: &impl RepositoryCache) -> Result Result { if resolved.coordinate != dependency.coordinate { @@ -88,7 +99,7 @@ pub fn lock(descriptor: &Descriptor, cache: &impl RepositoryCache) -> Result Result( #[cfg(test)] mod tests { - use std::rc::Rc; + use anyhow::anyhow; use crate::{ model::protofetch::{Coordinate, Protocol, Revision, RevisionSpecification, Rules}, - proto_repository::{ProtoRepoError, ProtoRepository}, + resolver::ResolvedModule, }; use super::*; use pretty_assertions::assert_eq; - struct FakeRepositoryCache { - entries: BTreeMap>, + #[derive(Default)] + struct FakeModuleResolver { + entries: BTreeMap>, } - #[derive(Clone, Default)] - struct FakeRepository { - specifications: BTreeMap, - descriptors: BTreeMap, - } - - impl FakeRepository { - fn push(&mut self, revision: &str, commit_hash: &str, descriptor: Descriptor) { - self.specifications.insert( + impl FakeModuleResolver { + fn push(&mut self, name: &str, revision: &str, commit_hash: &str, descriptor: Descriptor) { + self.entries.entry(coordinate(name)).or_default().insert( RevisionSpecification { revision: Revision::pinned(revision), branch: None, }, - commit_hash.to_string(), + ResolvedModule { + commit_hash: commit_hash.to_string(), + descriptor, + }, ); - self.descriptors.insert(commit_hash.to_string(), descriptor); } } - impl RepositoryCache for FakeRepositoryCache { - fn clone_or_update( - &self, - entry: &Coordinate, - ) -> Result, CacheError> { - Ok(Box::new(self.entries.get(entry).unwrap().clone())) - } - } - - impl ProtoRepository for Rc { - fn extract_descriptor( - &self, - _: &DependencyName, - commit_hash: &str, - ) -> Result { - Ok(self.descriptors.get(commit_hash).unwrap().clone()) - } - - fn create_worktrees( - &self, - _: &str, - _: &DependencyName, - _: &str, - _: &Path, - ) -> Result<(), ProtoRepoError> { - Ok(()) - } - - fn resolve_commit_hash( + impl ModuleResolver for FakeModuleResolver { + fn resolve( &self, + coordinate: &Coordinate, specification: &RevisionSpecification, - ) -> Result { - Ok(self.specifications.get(specification).unwrap().clone()) + _: &DependencyName, + ) -> anyhow::Result { + Ok(self + .entries + .get(coordinate) + .ok_or_else(|| anyhow!("Coordinate not found: {}", coordinate))? + .get(specification) + .ok_or_else(|| anyhow!("Specification not found: {}", specification))? + .clone()) } } @@ -259,8 +248,9 @@ mod tests { #[test] fn resolve_transitive() { - let mut foo = FakeRepository::default(); - foo.push( + let mut resolver = FakeModuleResolver::default(); + resolver.push( + "foo", "1.0.0", "c1", Descriptor { @@ -271,8 +261,8 @@ mod tests { }, ); - let mut bar = FakeRepository::default(); - bar.push( + resolver.push( + "bar", "2.0.0", "c2", Descriptor { @@ -283,13 +273,6 @@ mod tests { }, ); - let cache = FakeRepositoryCache { - entries: BTreeMap::from([ - (coordinate("foo"), Rc::new(foo)), - (coordinate("bar"), Rc::new(bar)), - ]), - }; - let lock_file = lock( &Descriptor { name: "root".to_owned(), @@ -297,7 +280,7 @@ mod tests { proto_out_dir: None, dependencies: vec![dependency("foo", "1.0.0")], }, - &cache, + &resolver, ) .unwrap(); @@ -315,8 +298,9 @@ mod tests { #[test] fn resolve_transitive_root_priority() { - let mut foo = FakeRepository::default(); - foo.push( + let mut resolver = FakeModuleResolver::default(); + resolver.push( + "foo", "1.0.0", "c1", Descriptor { @@ -327,8 +311,8 @@ mod tests { }, ); - let mut bar = FakeRepository::default(); - bar.push( + resolver.push( + "bar", "1.0.0", "c3", Descriptor { @@ -338,7 +322,8 @@ mod tests { dependencies: Vec::new(), }, ); - bar.push( + resolver.push( + "bar", "2.0.0", "c2", Descriptor { @@ -349,13 +334,6 @@ mod tests { }, ); - let cache = FakeRepositoryCache { - entries: BTreeMap::from([ - (coordinate("foo"), Rc::new(foo)), - (coordinate("bar"), Rc::new(bar)), - ]), - }; - let lock_file = lock( &Descriptor { name: "root".to_owned(), @@ -363,7 +341,7 @@ mod tests { proto_out_dir: None, dependencies: vec![dependency("foo", "1.0.0"), dependency("bar", "1.0.0")], }, - &cache, + &resolver, ) .unwrap(); diff --git a/src/lib.rs b/src/lib.rs index d1f79fb..e156ddd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,5 +5,6 @@ mod fetch; mod model; mod proto; mod proto_repository; +mod resolver; pub use api::{Protofetch, ProtofetchBuilder}; diff --git a/src/proto_repository.rs b/src/proto_repository.rs index 49389e0..47e0029 100644 --- a/src/proto_repository.rs +++ b/src/proto_repository.rs @@ -43,11 +43,6 @@ pub struct ProtoGitRepository { } pub trait ProtoRepository { - fn extract_descriptor( - &self, - dep_name: &DependencyName, - commit_hash: &str, - ) -> Result; fn create_worktrees( &self, module_name: &str, @@ -55,10 +50,6 @@ pub trait ProtoRepository { commit_hash: &str, out_dir: &Path, ) -> Result<(), ProtoRepoError>; - fn resolve_commit_hash( - &self, - specification: &RevisionSpecification, - ) -> Result; } impl ProtoGitRepository { @@ -66,18 +57,7 @@ impl ProtoGitRepository { ProtoGitRepository { git_repo } } - fn commit_hash_for_obj_str(&self, str: &str) -> Result { - Ok(self.git_repo.revparse_single(str)?.peel_to_commit()?.id()) - } - - // Check if `a` is an ancestor of `b` - fn is_ancestor(&self, a: Oid, b: Oid) -> Result { - Ok(self.git_repo.merge_base(a, b)? == a) - } -} - -impl ProtoRepository for ProtoGitRepository { - fn extract_descriptor( + pub fn extract_descriptor( &self, dep_name: &DependencyName, commit_hash: &str, @@ -120,6 +100,50 @@ impl ProtoRepository for ProtoGitRepository { } } + pub fn resolve_commit_hash( + &self, + specification: &RevisionSpecification, + ) -> Result { + let RevisionSpecification { branch, revision } = specification; + let oid = match (branch, revision) { + (None, Revision::Arbitrary) => self.commit_hash_for_obj_str("HEAD")?, + (None, Revision::Pinned { revision }) => self.commit_hash_for_obj_str(revision)?, + (Some(branch), Revision::Arbitrary) => self + .commit_hash_for_obj_str(&format!("origin/{branch}")) + .map_err(|_| ProtoRepoError::BranchNotFound { + branch: branch.to_owned(), + })?, + (Some(branch), Revision::Pinned { revision }) => { + let branch_commit = self + .commit_hash_for_obj_str(&format!("origin/{branch}")) + .map_err(|_| ProtoRepoError::BranchNotFound { + branch: branch.to_owned(), + })?; + let revision_commit = self.commit_hash_for_obj_str(revision)?; + if self.is_ancestor(revision_commit, branch_commit)? { + revision_commit + } else { + return Err(ProtoRepoError::RevisionNotOnBranch { + revision: revision.to_owned(), + branch: branch.to_owned(), + }); + } + } + }; + Ok(oid.to_string()) + } + + fn commit_hash_for_obj_str(&self, str: &str) -> Result { + Ok(self.git_repo.revparse_single(str)?.peel_to_commit()?.id()) + } + + // Check if `a` is an ancestor of `b` + fn is_ancestor(&self, a: Oid, b: Oid) -> Result { + Ok(self.git_repo.merge_base(a, b)? == a) + } +} + +impl ProtoRepository for ProtoGitRepository { fn create_worktrees( &self, module_name: &str, @@ -193,37 +217,4 @@ impl ProtoRepository for ProtoGitRepository { Ok(()) } - - fn resolve_commit_hash( - &self, - specification: &RevisionSpecification, - ) -> Result { - let RevisionSpecification { branch, revision } = specification; - let oid = match (branch, revision) { - (None, Revision::Arbitrary) => self.commit_hash_for_obj_str("HEAD")?, - (None, Revision::Pinned { revision }) => self.commit_hash_for_obj_str(revision)?, - (Some(branch), Revision::Arbitrary) => self - .commit_hash_for_obj_str(&format!("origin/{branch}")) - .map_err(|_| ProtoRepoError::BranchNotFound { - branch: branch.to_owned(), - })?, - (Some(branch), Revision::Pinned { revision }) => { - let branch_commit = self - .commit_hash_for_obj_str(&format!("origin/{branch}")) - .map_err(|_| ProtoRepoError::BranchNotFound { - branch: branch.to_owned(), - })?; - let revision_commit = self.commit_hash_for_obj_str(revision)?; - if self.is_ancestor(revision_commit, branch_commit)? { - revision_commit - } else { - return Err(ProtoRepoError::RevisionNotOnBranch { - revision: revision.to_owned(), - branch: branch.to_owned(), - }); - } - } - }; - Ok(oid.to_string()) - } } diff --git a/src/resolver/git.rs b/src/resolver/git.rs new file mode 100644 index 0000000..9ea7a12 --- /dev/null +++ b/src/resolver/git.rs @@ -0,0 +1,23 @@ +use crate::{ + cache::{ProtofetchGitCache, RepositoryCache}, + model::protofetch::{Coordinate, DependencyName, RevisionSpecification}, +}; + +use super::{ModuleResolver, ResolvedModule}; + +impl ModuleResolver for ProtofetchGitCache { + fn resolve( + &self, + coordinate: &Coordinate, + specification: &RevisionSpecification, + name: &DependencyName, + ) -> anyhow::Result { + let repository = self.clone_or_update(coordinate)?; + let commit_hash = repository.resolve_commit_hash(specification)?; + let descriptor = repository.extract_descriptor(name, &commit_hash)?; + Ok(ResolvedModule { + commit_hash, + descriptor, + }) + } +} diff --git a/src/resolver/mod.rs b/src/resolver/mod.rs new file mode 100644 index 0000000..8efa061 --- /dev/null +++ b/src/resolver/mod.rs @@ -0,0 +1,18 @@ +mod git; + +use crate::model::protofetch::{Coordinate, DependencyName, Descriptor, RevisionSpecification}; + +pub trait ModuleResolver { + fn resolve( + &self, + coordinate: &Coordinate, + specification: &RevisionSpecification, + name: &DependencyName, + ) -> anyhow::Result; +} + +#[derive(Clone)] +pub struct ResolvedModule { + pub commit_hash: String, + pub descriptor: Descriptor, +}