207e4d03
Use system git for fetch/push in sync, fixing SSH auth
a73x 2026-03-20 19:07
libgit2's SSH transport cannot use ssh-agent, causing auth failures. Sync now shells out to `git fetch` and `git push` for network operations while keeping git2 for local ref manipulation and DAG reconciliation. Simplified reconcile_refs to not need remote_name since sync refs are now fetched to a flat refs/collab/sync/ namespace. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
diff --git a/src/error.rs b/src/error.rs index 82a949c..0f65c88 100644 --- a/src/error.rs +++ b/src/error.rs @@ -7,4 +7,7 @@ pub enum Error { #[error(transparent)] Json(#[from] serde_json::Error), #[error("{0}")] Cmd(String), } diff --git a/src/sync.rs b/src/sync.rs index 3c6e0f6..dda19ef 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -1,17 +1,19 @@ use git2::{Direction, Oid, Repository}; use std::process::Command; use git2::Repository; use crate::dag; use crate::error::Error; use crate::identity::get_author; /// Add collab refspecs to all remotes. pub fn init(repo: &Repository) -> Result<(), crate::error::Error> { pub fn init(repo: &Repository) -> Result<(), Error> { let remotes = repo.remotes()?; if remotes.is_empty() { println!("No remotes configured."); return Ok(()); } for remote_name in remotes.iter().flatten() { // Add fetch refspec for collab refs let fetch_spec = format!("+refs/collab/*:refs/collab/sync/{}/*", remote_name); repo.remote_add_fetch(remote_name, &fetch_spec)?; println!("Configured remote '{}'", remote_name); @@ -21,66 +23,73 @@ pub fn init(repo: &Repository) -> Result<(), crate::error::Error> { } /// Sync with a specific remote: fetch, reconcile, push. pub fn sync(repo: &Repository, remote_name: &str) -> Result<(), crate::error::Error> { pub fn sync(repo: &Repository, remote_name: &str) -> Result<(), Error> { let author = get_author(repo)?; let workdir = repo.path().parent().unwrap_or(repo.path()).to_path_buf(); // Step 1: Connect to remote and discover collab refs // Step 1: Fetch collab refs using system git (handles SSH agent, credentials, etc.) println!("Fetching from '{}'...", remote_name); let mut remote = repo.find_remote(remote_name)?; // Fetch using configured refspecs (empty slice = use config refspecs) let empty: &[&str] = &[]; remote.fetch(empty, None, None)?; drop(remote); // After fetch, check if the refspec-mapped sync refs exist. // If git2's fetch didn't map them, manually discover and create them. // We do this by connecting and listing remote refs. let remote_collab_refs = list_remote_collab_refs(repo, remote_name)?; for (remote_ref_name, oid) in &remote_collab_refs { // Map refs/collab/X to refs/collab/sync/{remote}/X let suffix = remote_ref_name .strip_prefix("refs/collab/") .unwrap_or(remote_ref_name); let sync_ref = format!("refs/collab/sync/{}/{}", remote_name, suffix); // Ensure the object exists locally (it should after fetch) if repo.find_commit(*oid).is_ok() { repo.reference(&sync_ref, *oid, true, "sync: map remote ref")?; } let fetch_status = Command::new("git") .args([ "fetch", remote_name, "+refs/collab/issues/*:refs/collab/sync/issues/*", "+refs/collab/patches/*:refs/collab/sync/patches/*", ]) .current_dir(&workdir) .status() .map_err(|e| Error::Cmd(format!("failed to run git fetch: {}", e)))?; if !fetch_status.success() { return Err(Error::Cmd(format!( "git fetch exited with status {}", fetch_status ))); } // Step 2: Reconcile issues reconcile_refs(repo, remote_name, "issues", &author)?; // Step 3: Reconcile patches reconcile_refs(repo, remote_name, "patches", &author)?; // Step 2: Reconcile // Re-open repo to see the fetched refs (git2 caches ref state) let repo = Repository::open(repo.path())?; reconcile_refs(&repo, "issues", &author)?; reconcile_refs(&repo, "patches", &author)?; // Step 4: Push — enumerate concrete refs (git2 push doesn't support globs) // Step 3: Push collab refs using system git println!("Pushing to '{}'...", remote_name); let mut push_specs: Vec<String> = Vec::new(); let mut push_args = vec!["push", remote_name]; let mut refspecs: Vec<String> = Vec::new(); for pattern in &["refs/collab/issues/*", "refs/collab/patches/*"] { let refs = repo.references_glob(pattern)?; for r in refs { let r = r?; if let Some(name) = r.name() { push_specs.push(format!("+{}:{}", name, name)); refspecs.push(format!("+{}:{}", name, name)); } } } if !push_specs.is_empty() { let mut remote = repo.find_remote(remote_name)?; let specs: Vec<&str> = push_specs.iter().map(|s| s.as_str()).collect(); remote.push(&specs, None)?; if refspecs.is_empty() { println!("Nothing to push."); } else { let refspec_strs: Vec<&str> = refspecs.iter().map(|s| s.as_str()).collect(); push_args.extend(refspec_strs); let push_status = Command::new("git") .args(&push_args) .current_dir(&workdir) .status() .map_err(|e| Error::Cmd(format!("failed to run git push: {}", e)))?; if !push_status.success() { return Err(Error::Cmd(format!( "git push exited with status {}", push_status ))); } } // Step 5: Clean up sync refs for prefix in &[ format!("refs/collab/sync/{}/issues/", remote_name), format!("refs/collab/sync/{}/patches/", remote_name), ] { // Step 4: Clean up sync refs for prefix in &["refs/collab/sync/issues/", "refs/collab/sync/patches/"] { let refs: Vec<String> = repo .references_glob(&format!("{}*", prefix))? .filter_map(|r| r.ok()?.name().map(|n| n.to_string())) @@ -95,36 +104,13 @@ pub fn sync(repo: &Repository, remote_name: &str) -> Result<(), crate::error::Er Ok(()) } /// Connect to a remote and list all refs under refs/collab/ (issues + patches only). fn list_remote_collab_refs( repo: &Repository, remote_name: &str, ) -> Result<Vec<(String, Oid)>, crate::error::Error> { let mut remote = repo.find_remote(remote_name)?; remote.connect(Direction::Fetch)?; let refs: Vec<(String, Oid)> = remote .list()? .iter() .filter(|head| { let name = head.name(); name.starts_with("refs/collab/issues/") || name.starts_with("refs/collab/patches/") }) .map(|head| (head.name().to_string(), head.oid())) .collect(); remote.disconnect()?; Ok(refs) } /// Reconcile all refs of a given kind (issues or patches) from sync refs. fn reconcile_refs( repo: &Repository, remote_name: &str, kind: &str, author: &crate::event::Author, ) -> Result<(), crate::error::Error> { let sync_prefix = format!("refs/collab/sync/{}/{}/", remote_name, kind); ) -> Result<(), Error> { let sync_prefix = format!("refs/collab/sync/{}/", kind); let sync_refs: Vec<(String, String)> = { let refs = repo.references_glob(&format!("{}*", sync_prefix))?; refs.filter_map(|r| {