use crate::errors::GitError;
use crate::progress::ProgressReporter;
use anyhow::{anyhow, Context, Result};
use git2::{Cred, FetchOptions, RemoteCallbacks, Repository, ResetType};
use std::sync::Arc;
pub(super) fn create_remote_callbacks(
progress: Option<Arc<dyn ProgressReporter>>,
) -> RemoteCallbacks<'static> {
let mut callbacks = RemoteCallbacks::new();
callbacks.credentials(|_url, username_from_url, _allowed_types| {
let username = username_from_url.unwrap_or("git");
log::debug!("Attempting SSH authentication for user: {}", username);
if let Ok(cred) = Cred::ssh_key_from_agent(username) {
log::debug!("Authenticated via SSH agent");
return Ok(cred);
}
if let Ok(cred) = Cred::ssh_key(
username,
None,
std::env::var("HOME")
.or_else(|_| std::env::var("USERPROFILE"))
.map(std::path::PathBuf::from)
.ok()
.as_deref()
.unwrap_or_else(|| std::path::Path::new(""))
.join(".ssh")
.join("id_rsa")
.as_path(),
None,
) {
log::debug!("Authenticated via default SSH key path");
return Ok(cred);
}
log::warn!("SSH authentication failed: No agent or default keys found.");
Err(git2::Error::from_str(
"Authentication failed: could not connect with SSH agent or default keys",
))
});
if let Some(p) = progress {
callbacks.transfer_progress(move |stats| {
if stats.received_objects() == stats.total_objects() {
p.set_length(stats.total_deltas() as u64);
p.set_position(stats.indexed_deltas() as u64);
p.set_message("Resolving deltas...".to_string());
} else if stats.total_objects() > 0 {
p.set_length(stats.total_objects() as u64);
p.set_position(stats.received_objects() as u64);
p.set_message("Receiving objects...".to_string());
}
true
});
}
callbacks
}
pub(super) fn create_fetch_options(
depth: Option<u32>,
progress: Option<Arc<dyn ProgressReporter>>,
) -> FetchOptions<'static> {
let mut fetch_options = FetchOptions::new();
fetch_options.remote_callbacks(create_remote_callbacks(progress));
fetch_options.prune(git2::FetchPrune::On);
fetch_options.download_tags(git2::AutotagOption::All);
if let Some(depth) = depth {
fetch_options.depth(depth as i32);
log::debug!("Set shallow clone depth to: {}", depth);
}
fetch_options
}
pub(super) fn find_remote_commit<'a>(
repo: &'a Repository,
branch: &Option<String>,
) -> Result<git2::Commit<'a>, GitError> {
if let Some(ref_name) = branch {
log::debug!("Using user-specified ref: {}", ref_name);
let branch_ref_name = format!("refs/remotes/origin/{}", ref_name);
if let Ok(reference) = repo.find_reference(&branch_ref_name) {
log::debug!("Resolved '{}' as a remote branch.", ref_name);
return repo
.find_commit(
reference
.target()
.context("Remote branch reference has no target commit")?,
)
.context("Failed to find commit for branch reference")
.map_err(GitError::Generic);
}
let tag_ref_name = format!("refs/tags/{}", ref_name);
if let Ok(reference) = repo.find_reference(&tag_ref_name) {
log::debug!("Resolved '{}' as a tag.", ref_name);
let object = reference
.peel(git2::ObjectType::Commit)
.map_err(|e| GitError::Generic(anyhow!(e)))?;
return object.into_commit().map_err(|_| {
GitError::Generic(anyhow!("Tag '{}' does not point to a commit", ref_name))
});
}
return Err(GitError::RefNotFound {
name: ref_name.clone(),
});
}
log::debug!("Resolving remote's default branch via origin/HEAD");
let remote_head = repo.find_reference("refs/remotes/origin/HEAD")
.context("Could not find remote's HEAD. The repository might not have a default branch set, or it may be empty. Please specify a branch with --git-branch.")
.map_err(|e| GitError::DefaultBranchResolution(e.to_string()))?;
let remote_branch_ref_name = remote_head
.symbolic_target()
.context("Remote HEAD is not a symbolic reference; cannot determine default branch.")
.map_err(|e| GitError::DefaultBranchResolution(e.to_string()))?
.to_string();
log::debug!("Targeting remote reference: {}", remote_branch_ref_name);
let fetch_head = repo
.find_reference(&remote_branch_ref_name)
.with_context(|| {
format!(
"Could not find remote branch reference '{}' after fetch. Does this branch exist on the remote?",
remote_branch_ref_name
)
})
.map_err(GitError::Generic)?;
repo.find_commit(
fetch_head
.target()
.context("Remote branch reference has no target commit")?,
)
.context("Failed to find commit for default branch reference")
.map_err(GitError::Generic)
}
pub fn update_repo(
repo: &Repository,
branch: &Option<String>,
depth: Option<u32>,
progress: Option<Arc<dyn ProgressReporter>>,
) -> Result<(), GitError> {
log::info!("Updating cached repository...");
let mut remote = repo
.find_remote("origin")
.map_err(|e| GitError::Generic(anyhow!(e)))?;
let mut fetch_options = create_fetch_options(depth, progress);
remote
.fetch(&[] as &[&str], Some(&mut fetch_options), None)
.map_err(|e| GitError::FetchFailed {
remote: "origin".to_string(),
source: e,
})?;
let target_commit = find_remote_commit(repo, branch)?;
repo.set_head_detached(target_commit.id())
.context("Failed to detach HEAD in cached repository")
.map_err(|e| GitError::UpdateFailed(e.to_string()))?;
repo.reset(
target_commit.as_object(),
ResetType::Hard,
None, )
.context("Failed to perform hard reset on cached repository")
.map_err(|e| GitError::UpdateFailed(e.to_string()))?;
log::info!("Cached repository updated successfully.");
Ok(())
}