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
This commit is contained in:
Fintan Halpenny 2025-03-28 16:31:00 +00:00 committed by Lorenz Leutgeb
parent 433483e07a
commit 8fd044833e
No known key found for this signature in database
2 changed files with 64 additions and 13 deletions

View File

@ -1,7 +1,7 @@
#![allow(clippy::too_many_arguments)] #![allow(clippy::too_many_arguments)]
use std::collections::HashMap; use std::collections::HashMap;
use std::io::IsTerminal; use std::io::IsTerminal;
use std::path::Path; use std::path::{Path, PathBuf};
use std::str::FromStr; use std::str::FromStr;
use std::{assert_eq, io}; use std::{assert_eq, io};
@ -30,6 +30,10 @@ use crate::{hint, read_line, warn, Options};
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum 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. /// Public key doesn't match the remote namespace we're pushing to.
#[error("cannot push to remote namespace owned by {0}")] #[error("cannot push to remote namespace owned by {0}")]
KeyMismatch(Did), KeyMismatch(Did),
@ -777,12 +781,32 @@ fn push_ref(
working: &git::raw::Repository, working: &git::raw::Repository,
stored: &git::raw::Repository, stored: &git::raw::Repository,
) -> Result<(), Error> { ) -> Result<(), Error> {
let mut remote = working.remote_anonymous(&git::url::File::new(stored.path()).to_string())?; let url = git::url::File::new(stored.path()).to_string();
let refspec = git::Refspec { src, dst, force };
// Nb. The *force* indicator (`+`) is processed by Git tooling before we even reach this code. // Nb. The *force* indicator (`+`) is processed by Git tooling before we even reach this code.
// This happens during the `list for-push` phase. // 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(()) Ok(())
} }

View File

@ -1,6 +1,6 @@
#![allow(clippy::let_unit_value)] #![allow(clippy::let_unit_value)]
use std::io; use std::io;
use std::path::Path; use std::path::{Path, PathBuf};
use std::str::FromStr; use std::str::FromStr;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
@ -29,6 +29,10 @@ pub static PATCHES_REFNAME: Lazy<git::RefString> = Lazy::new(|| git::refname!("r
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum InitError { pub enum InitError {
#[error(
"the Git repository found at {path:?} is a bare repository, expected a working directory"
)]
BareRepository { path: PathBuf },
#[error("doc: {0}")] #[error("doc: {0}")]
Doc(#[from] DocError), Doc(#[from] DocError),
#[error("repository: {0}")] #[error("repository: {0}")]
@ -103,13 +107,36 @@ where
git::configure_repository(repo)?; git::configure_repository(repo)?;
git::configure_remote(repo, &REMOTE_NAME, url, &url.clone().with_namespace(*pk))?; git::configure_remote(repo, &REMOTE_NAME, url, &url.clone().with_namespace(*pk))?;
git::push( let branch = git::Qualified::from(git::fmt::lit::refs_heads(default_branch));
repo, // Pushes to default branch to the namespace of the `signer`
&REMOTE_NAME, let pushspec = git::Refspec {
[( src: branch.clone(),
&git::fmt::lit::refs_heads(default_branch).into(), dst: branch.with_namespace(git::Component::from(pk)),
&git::fmt::lit::refs_heads(default_branch).into(), 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_remote_identity_root_to(pk, identity)?;
stored.set_identity_head_to(identity)?; stored.set_identity_head_to(identity)?;