From 8fd044833e40440e7b9d7a2c08281f404e5f5ed7 Mon Sep 17 00:00:00 2001 From: Fintan Halpenny Date: Fri, 28 Mar 2025 16:31:00 +0000 Subject: [PATCH] radicle: use `git push` to avoid `libgit2` push In issue #[2836] of `libgit2`, the speed of `libgit2`'s `file://` protocol for git operations is found to be very slow. This is further corrobarated by `rad init` taking 13 hours to initialise the hardenedbsd [ports] repository. There are two areas where the `heartwood` project uses `libgit2` to push using the file protocol. The first is when via the `trasnport::local` smart transport registration and the second is the final push to storage in the `git-remote-rad` binary. When both these `push` operations are changed to use the `git` binary instead, the [ports] repository can be initialised in less than 10 minutes (nearly 100x speed up). This change is clearly required if `heartwood` wishes to support larger repositories. [2836]: https://github.com/libgit2/libgit2/issues/2836 [ports]: https://git.hardenedbsd.org/hardenedbsd/ports --- radicle-remote-helper/src/push.rs | 34 ++++++++++++++++++++---- radicle/src/rad.rs | 43 +++++++++++++++++++++++++------ 2 files changed, 64 insertions(+), 13 deletions(-) diff --git a/radicle-remote-helper/src/push.rs b/radicle-remote-helper/src/push.rs index 32e98318..ef36dca1 100644 --- a/radicle-remote-helper/src/push.rs +++ b/radicle-remote-helper/src/push.rs @@ -1,7 +1,7 @@ #![allow(clippy::too_many_arguments)] use std::collections::HashMap; use std::io::IsTerminal; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::str::FromStr; use std::{assert_eq, io}; @@ -30,6 +30,10 @@ use crate::{hint, read_line, warn, Options}; #[derive(Debug, Error)] pub enum Error { + #[error( + "the Git repository found at {path:?} is a bare repository, expected a working directory" + )] + BareRepository { path: PathBuf }, /// Public key doesn't match the remote namespace we're pushing to. #[error("cannot push to remote namespace owned by {0}")] KeyMismatch(Did), @@ -777,12 +781,32 @@ fn push_ref( working: &git::raw::Repository, stored: &git::raw::Repository, ) -> Result<(), Error> { - let mut remote = working.remote_anonymous(&git::url::File::new(stored.path()).to_string())?; - let refspec = git::Refspec { src, dst, force }; - + let url = git::url::File::new(stored.path()).to_string(); // Nb. The *force* indicator (`+`) is processed by Git tooling before we even reach this code. // This happens during the `list for-push` phase. - remote.push(&[refspec.to_string().as_str()], None)?; + let refspec = git::Refspec { src, dst, force }; + let repo = working.workdir().ok_or(Error::BareRepository { + path: working.path().to_path_buf(), + })?; + + radicle::git::run::<_, _, &str, &str>( + repo, + [ + "push", + url.to_string().as_str(), + refspec.to_string().as_str(), + ], + [], + ) + .map_err(|err| { + Error::Io(std::io::Error::new( + std::io::ErrorKind::Other, + format!( + "failed to run `git push {url} {refspec}` in {:?}: {err}", + working.path() + ), + )) + })?; Ok(()) } diff --git a/radicle/src/rad.rs b/radicle/src/rad.rs index 5b08364a..d94665d1 100644 --- a/radicle/src/rad.rs +++ b/radicle/src/rad.rs @@ -1,6 +1,6 @@ #![allow(clippy::let_unit_value)] use std::io; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::str::FromStr; use once_cell::sync::Lazy; @@ -29,6 +29,10 @@ pub static PATCHES_REFNAME: Lazy = Lazy::new(|| git::refname!("r #[derive(Error, Debug)] pub enum InitError { + #[error( + "the Git repository found at {path:?} is a bare repository, expected a working directory" + )] + BareRepository { path: PathBuf }, #[error("doc: {0}")] Doc(#[from] DocError), #[error("repository: {0}")] @@ -103,13 +107,36 @@ where git::configure_repository(repo)?; git::configure_remote(repo, &REMOTE_NAME, url, &url.clone().with_namespace(*pk))?; - git::push( - repo, - &REMOTE_NAME, - [( - &git::fmt::lit::refs_heads(default_branch).into(), - &git::fmt::lit::refs_heads(default_branch).into(), - )], + let branch = git::Qualified::from(git::fmt::lit::refs_heads(default_branch)); + // Pushes to default branch to the namespace of the `signer` + let pushspec = git::Refspec { + src: branch.clone(), + dst: branch.with_namespace(git::Component::from(pk)), + force: false, + }; + git::run::<_, _, &str, &str>( + repo.workdir().ok_or(InitError::BareRepository { + path: repo.path().to_path_buf(), + })?, + [ + "push", + &format!("{}", stored.path().canonicalize()?.display()), + &pushspec.to_string(), + ], + [], + )?; + // N.b. we need to create the remote branch for the default branch + let rad_remote = + git::Qualified::from(git::lit::refs_remotes(&*REMOTE_COMPONENT)).join(default_branch); + let oid = repo.refname_to_id(branch.as_str())?; + repo.reference( + rad_remote.as_str(), + oid, + false, + &format!( + "radicle: remote branch {}/{}", + *REMOTE_COMPONENT, default_branch + ), )?; stored.set_remote_identity_root_to(pk, identity)?; stored.set_identity_head_to(identity)?;