use git2::{FetchOptions, RemoteCallbacks, Repository};
use std::path::Path;
use super::cache::Cache;
use super::manifest::{Dependency, GitVersion};
pub struct Fetcher {
cache: Cache,
}
impl Fetcher {
pub fn new() -> Option<Self> {
Some(Self {
cache: Cache::new()?,
})
}
pub fn with_cache(cache: Cache) -> Self {
Self { cache }
}
pub fn fetch(
&self,
name: &str,
dep: &Dependency,
) -> Result<std::path::PathBuf, FetchError> {
match dep {
Dependency::Git { git, version } => self.fetch_git(name, git, version),
Dependency::Path { path } => {
let path = std::path::PathBuf::from(path);
if !path.exists() {
return Err(FetchError::PathNotFound(path));
}
Ok(path)
}
}
}
fn fetch_git(
&self,
name: &str,
url: &str,
version: &GitVersion,
) -> Result<std::path::PathBuf, FetchError> {
let version_str = self.version_string(version);
let dest = self.cache.dep_path(name, url, &version_str);
if self.cache.is_cached(name, url, &version_str) {
return Ok(dest);
}
if let Some(parent) = dest.parent() {
std::fs::create_dir_all(parent).map_err(|e| FetchError::Io(e.to_string()))?;
}
self.clone_repo(url, &dest, version)?;
Ok(dest)
}
fn clone_repo(
&self,
url: &str,
dest: &Path,
version: &GitVersion,
) -> Result<(), FetchError> {
let mut callbacks = RemoteCallbacks::new();
callbacks.transfer_progress(|_stats| {
true
});
let mut fetch_options = FetchOptions::new();
fetch_options.remote_callbacks(callbacks);
let repo = Repository::clone(url, dest).map_err(|e| FetchError::Git(e.to_string()))?;
self.checkout_version(&repo, version)?;
Ok(())
}
fn checkout_version(&self, repo: &Repository, version: &GitVersion) -> Result<(), FetchError> {
let refspec = if let Some(tag) = &version.tag {
format!("refs/tags/{}", tag)
} else if let Some(branch) = &version.branch {
format!("refs/remotes/origin/{}", branch)
} else if let Some(rev) = &version.rev {
rev.clone()
} else {
return Ok(());
};
let reference = if version.rev.is_some() {
let oid = git2::Oid::from_str(&refspec)
.map_err(|e| FetchError::Git(format!("invalid revision: {}", e)))?;
repo.find_commit(oid)
.map_err(|e| FetchError::Git(format!("commit not found: {}", e)))?;
repo.set_head_detached(oid)
.map_err(|e| FetchError::Git(format!("failed to checkout: {}", e)))?;
return Ok(());
} else {
repo.find_reference(&refspec)
.map_err(|e| FetchError::Git(format!("reference '{}' not found: {}", refspec, e)))?
};
let commit = reference
.peel_to_commit()
.map_err(|e| FetchError::Git(format!("failed to resolve commit: {}", e)))?;
repo.checkout_tree(commit.as_object(), None)
.map_err(|e| FetchError::Git(format!("failed to checkout: {}", e)))?;
repo.set_head(reference.name().unwrap_or(&refspec))
.map_err(|e| FetchError::Git(format!("failed to set HEAD: {}", e)))?;
Ok(())
}
fn version_string(&self, version: &GitVersion) -> String {
if let Some(tag) = &version.tag {
format!("tag-{}", tag)
} else if let Some(branch) = &version.branch {
format!("branch-{}", branch)
} else if let Some(rev) = &version.rev {
format!("rev-{}", rev)
} else {
"default".to_string()
}
}
pub fn cache(&self) -> &Cache {
&self.cache
}
}
#[derive(Debug)]
pub enum FetchError {
Git(String),
Io(String),
PathNotFound(std::path::PathBuf),
}
impl std::fmt::Display for FetchError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FetchError::Git(msg) => write!(f, "git error: {}", msg),
FetchError::Io(msg) => write!(f, "io error: {}", msg),
FetchError::PathNotFound(path) => {
write!(f, "path dependency not found: {}", path.display())
}
}
}
}
impl std::error::Error for FetchError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_version_string() {
let fetcher = Fetcher::with_cache(Cache::with_dir(std::path::PathBuf::from("/tmp")));
assert_eq!(
fetcher.version_string(&GitVersion {
tag: Some("v1.0.0".to_string()),
..Default::default()
}),
"tag-v1.0.0"
);
assert_eq!(
fetcher.version_string(&GitVersion {
branch: Some("main".to_string()),
..Default::default()
}),
"branch-main"
);
assert_eq!(
fetcher.version_string(&GitVersion::default()),
"default"
);
}
}