use crate::db::MirrorDb; use crate::discovery::DiscoveredRepo; use crate::health::GraspServer; use anyhow::{Context, Result}; use git2::RemoteCallbacks; use std::path::Path; pub struct GitMirror { mirror_dir: std::path::PathBuf, } impl GitMirror { pub fn new(mirror_dir: &Path) -> Self { Self { mirror_dir: mirror_dir.to_path_buf(), } } fn repo_path(&self, pubkey: &str, identifier: &str) -> std::path::PathBuf { self.mirror_dir .join(pubkey) .join(format!("{}.git", identifier)) } pub async fn mirror_repo_to_servers( &self, db: &MirrorDb, repo: &DiscoveredRepo, target_servers: &[GraspServer], ) -> Result<()> { if target_servers.is_empty() { tracing::debug!( identifier = %repo.identifier, "no missing servers to mirror to" ); return Ok(()); } let pk_hex = repo.pubkey.to_hex(); let repo_path = self.repo_path(&pk_hex, &repo.identifier); if !repo_path.exists() { self.clone_bare(&repo_path, &repo.clone_urls)?; } for server in target_servers { let target_url = server.clone_url(&pk_hex, &repo.identifier); tracing::info!( identifier = %repo.identifier, server = %server.domain, target = %target_url, "mirroring git data" ); let repo_id = db.get_all_repos().await.ok().and_then(|repos| { repos .iter() .find(|r| r.pubkey == pk_hex && r.identifier == repo.identifier) .map(|r| r.id) }); match self.push_mirror(&repo_path, &target_url) { Ok(()) => { tracing::info!( identifier = %repo.identifier, server = %server.domain, "git mirror succeeded" ); if let Some(id) = repo_id { let _ = db.mark_git_synced(id, &server.domain).await; } } Err(e) => { tracing::error!( identifier = %repo.identifier, server = %server.domain, error = %e, "git mirror failed" ); if let Some(id) = repo_id { let _ = db.mark_sync_error(id, &server.domain, &e.to_string()).await; } } } } Ok(()) } fn clone_bare(&self, repo_path: &Path, clone_urls: &[String]) -> Result<()> { if let Some(parent) = repo_path.parent() { std::fs::create_dir_all(parent) .with_context(|| format!("failed to create {:?}", parent))?; } let mut last_error = None; for url in clone_urls { if url.is_empty() { continue; } tracing::info!(url = %url, path = ?repo_path, "cloning bare repo"); let mut callbacks = RemoteCallbacks::new(); callbacks.credentials(|_url, _username, _allowed| git2::Cred::default()); let mut fetch_opts = git2::FetchOptions::new(); fetch_opts.remote_callbacks(callbacks); let mut builder = git2::build::RepoBuilder::new(); builder.bare(true).fetch_options(fetch_opts); match builder.clone(url, repo_path) { Ok(_) => { tracing::info!(url = %url, "bare clone succeeded"); return Ok(()); } Err(e) => { tracing::warn!(url = %url, error = %e, "clone failed, trying next URL"); last_error = Some(e); if repo_path.exists() { let _ = std::fs::remove_dir_all(repo_path); } } } } let err = last_error.unwrap_or_else(|| git2::Error::from_str("no clone URLs available")); Err(err).with_context(|| format!("all clone attempts failed for {:?}", repo_path)) } fn push_mirror(&self, repo_path: &Path, target_url: &str) -> Result<()> { let repo = git2::Repository::open(repo_path) .with_context(|| format!("failed to open bare repo at {:?}", repo_path))?; let mut remote = repo.remote("push_target", target_url)?; let mut callbacks = RemoteCallbacks::new(); callbacks.credentials(|_url, _username, _allowed| git2::Cred::default()); callbacks.push_update_reference(|_refname, status| { if let Some(s) = status { tracing::warn!(status = %s, "push rejected"); } Ok(()) }); let mut push_opts = git2::PushOptions::new(); push_opts.remote_callbacks(callbacks); let refspecs = ["+refs/*:refs/*"]; remote .push(&refspecs, Some(&mut push_opts)) .with_context(|| format!("failed to push mirror to {}", target_url))?; Ok(()) } }