use git2::{ErrorCode, FetchOptions, Oid, Repository};
use std::path::Path;
use std::sync::Arc;
use super::auth::CredentialProvider;
use super::error::{GitError, Result};
pub struct BareRepository {
repo: Repository,
url: String,
credential_provider: Option<Arc<CredentialProvider>>,
}
impl BareRepository {
fn resolve_object_to_commit_oid(&self, oid: Oid) -> Result<Oid> {
let object = match self.repo.find_object(oid, None) {
Ok(object) => object,
Err(error) if error.code() == ErrorCode::NotFound => {
self.fetch_objects(oid)?;
self.repo.find_object(oid, None)?
}
Err(error) => return Err(GitError::Repository(error)),
};
let commit = object.peel_to_commit()?;
let commit_oid = commit.id();
if commit_oid != oid {
tracing::debug!("Resolved object {} to commit {}", oid, commit_oid);
}
Ok(commit_oid)
}
fn resolve_reference_to_commit_oid(&self, reference_name: &str) -> Result<Option<Oid>> {
let Ok(reference) = self.repo.find_reference(reference_name) else {
return Ok(None);
};
if let Some(oid) = reference.target() {
return self.resolve_object_to_commit_oid(oid).map(Some);
}
Ok(None)
}
pub fn get_or_create(url: &str, path: &Path, credential_provider: Option<Arc<CredentialProvider>>) -> Result<Self> {
let repo = if path.exists() {
tracing::debug!("Reusing cached bare repository for {} at {}", url, path.display());
Repository::open(path)?
} else {
tracing::debug!("Creating bare repository cache for {} at {}", url, path.display());
Self::clone_bare(url, path, credential_provider.as_deref())?
};
Ok(Self {
repo,
url: url.to_string(),
credential_provider,
})
}
fn clone_bare(url: &str, path: &Path, credential_provider: Option<&CredentialProvider>) -> Result<Repository> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
tracing::trace!("Starting bare clone for {} into {}", url, path.display());
tracing::debug!("Initializing bare Git repository for {} at {}", url, path.display());
let repo = Repository::init_bare(path).map_err(|e| GitError::CloneFailed {
url: url.to_string(),
source: e,
})?;
repo.remote("origin", url).map_err(|e| GitError::CloneFailed {
url: url.to_string(),
source: e,
})?;
tracing::debug!("Fetching initial refs for {}", url);
Self::fetch_refs_with_auth(&repo, url, credential_provider)?;
tracing::debug!("Initial ref fetch complete for {}", url);
tracing::trace!("Bare clone completed successfully for {}", url);
Ok(repo)
}
fn fetch_refs_with_auth(
repo: &Repository,
url: &str,
credential_provider: Option<&CredentialProvider>,
) -> Result<()> {
tracing::trace!(
"Fetching refs for {} (credential_provider={})",
url,
if credential_provider.is_some() {
"present"
} else {
"absent"
}
);
let callbacks = if let Some(provider) = credential_provider {
provider.build_callbacks(url)
} else {
Self::build_default_callbacks()
};
let mut fetch_options = FetchOptions::new();
fetch_options.remote_callbacks(callbacks);
let mut remote = repo.find_remote("origin").map_err(GitError::Repository)?;
remote
.fetch(
&[
"+refs/heads/*:refs/heads/*",
"+refs/tags/*:refs/tags/*",
"+HEAD:refs/remotes/origin/HEAD",
],
Some(&mut fetch_options),
None,
)
.map_err(|e| GitError::Command(format!("git fetch failed: {}", e)))?;
tracing::trace!("Fetch refs completed for {}", url);
Ok(())
}
fn build_default_callbacks<'a>() -> git2::RemoteCallbacks<'a> {
let mut callbacks = git2::RemoteCallbacks::new();
callbacks.credentials(|_url, username_from_url, allowed_types| {
if allowed_types.contains(git2::CredentialType::SSH_KEY) {
let username = username_from_url.unwrap_or("git");
git2::Cred::ssh_key_from_agent(username)
} else {
Err(git2::Error::from_str("No credentials available"))
}
});
callbacks
}
pub fn fetch_refs(&self) -> Result<()> {
tracing::debug!("Refreshing remote refs for {}", self.url);
Self::fetch_refs_with_auth(&self.repo, &self.url, self.credential_provider.as_deref())
}
pub fn resolve_ref(&self, ref_name: &str) -> Result<Oid> {
if let Some(oid) = self.resolve_reference_to_commit_oid(ref_name)? {
return Ok(oid);
}
let branch_ref = format!("refs/heads/{}", ref_name);
if let Some(oid) = self.resolve_reference_to_commit_oid(&branch_ref)? {
return Ok(oid);
}
let tag_ref = format!("refs/tags/{}", ref_name);
if let Some(oid) = self.resolve_reference_to_commit_oid(&tag_ref)? {
return Ok(oid);
}
if let Ok(oid) = Oid::from_str(ref_name) {
if let Ok(commit_oid) = self.resolve_object_to_commit_oid(oid) {
return Ok(commit_oid);
}
}
if ref_name == "HEAD" {
if let Some(oid) = self.resolve_reference_to_commit_oid("HEAD")? {
return Ok(oid);
}
if let Some(oid) = self.resolve_reference_to_commit_oid("refs/remotes/origin/HEAD")? {
return Ok(oid);
}
}
Err(GitError::RefNotFound {
ref_name: ref_name.to_string(),
})
}
pub fn has_object(&self, oid: Oid) -> bool {
self.repo.find_object(oid, None).is_ok()
}
pub fn fetch_objects(&self, oid: Oid) -> Result<()> {
let oid_str = oid.to_string();
tracing::debug!("Fetching commit objects for {} at {}", self.url, oid_str);
let callbacks = if let Some(provider) = &self.credential_provider {
provider.build_callbacks(&self.url)
} else {
Self::build_default_callbacks()
};
let mut fetch_options = FetchOptions::new();
fetch_options.remote_callbacks(callbacks);
let mut remote = self.repo.find_remote("origin").map_err(GitError::Repository)?;
remote
.fetch(&[&oid_str], Some(&mut fetch_options), None)
.map_err(|e| GitError::Command(format!("git fetch {} failed: {}", oid_str, e)))?;
tracing::debug!("Fetched commit objects for {} at {}", self.url, oid_str);
Ok(())
}
pub fn path(&self) -> &Path {
self.repo.path()
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_bare_repository_creation() {
let temp_dir = TempDir::new().unwrap();
let repo_path = temp_dir.path().join("test.git");
let source_dir = TempDir::new().unwrap();
let source_repo = Repository::init(source_dir.path()).unwrap();
let sig = git2::Signature::now("Test", "test@example.com").unwrap();
let tree_id = {
let mut index = source_repo.index().unwrap();
index.write_tree().unwrap()
};
let tree = source_repo.find_tree(tree_id).unwrap();
source_repo
.commit(Some("HEAD"), &sig, &sig, "Initial commit", &tree, &[])
.unwrap();
assert!(!repo_path.exists());
}
}