Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
Revisit `radicle::git::raw`
Merged lorenz opened 6 months ago

Capture all git2 re-exports

A refactoring internal to the radicle crate, with the goal of making dependency on git2 clearer and more controlled. radicle::git::raw is changed from a complete re-export of git2 to a module that selectively re-exports (many) members of git2.

In the future, no more use git2::… statements should be added outside of crates/radicle/src/git/raw.rs. This is in an effort to decrease dependence on git2 in the future.

Introduce trait ErrorExt

This extension trait is more ergonomic than what radicle-git-ext provides.

28 files changed +374 -248 63305904 ea562215
modified crates/radicle-cli/src/commands/init.rs
@@ -13,6 +13,7 @@ use serde_json as json;
use radicle::crypto::ssh;
use radicle::explorer::ExplorerUrl;
use radicle::git::raw;
+
use radicle::git::raw::ErrorExt as _;
use radicle::git::RefString;
use radicle::identity::project::ProjectName;
use radicle::identity::{Doc, RepoId, Visibility};
@@ -189,7 +190,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
    let path = options.path.as_deref().unwrap_or(cwd.as_path());
    let repo = match git::Repository::open(path) {
        Ok(r) => r,
-
        Err(e) if radicle::git::ext::is_not_found_err(&e) => {
+
        Err(e) if e.is_not_found() => {
            anyhow::bail!("a Git repository was not found at the given path")
        }
        Err(e) => return Err(e.into()),
modified crates/radicle-cli/src/commands/patch/checkout.rs
@@ -3,6 +3,7 @@ use anyhow::anyhow;
use git_ref_format::Qualified;
use radicle::cob::patch;
use radicle::cob::patch::RevisionId;
+
use radicle::git::raw::ErrorExt as _;
use radicle::git::RefString;
use radicle::patch::cache::Patches as _;
use radicle::patch::PatchId;
@@ -73,7 +74,7 @@ pub fn run(
                }
                head
            }
-
            Err(e) if radicle::git::is_not_found_err(&e) => {
+
            Err(e) if e.is_not_found() => {
                let commit = find_patch_commit(revision, stored, working)?;
                // Create patch branch and switch to it.
                working.branch(patch_branch.as_str(), &commit, true)?;
@@ -128,7 +129,7 @@ fn find_patch_commit<'a>(

    match working.find_commit(head) {
        Ok(commit) => Ok(commit),
-
        Err(e) if git::ext::is_not_found_err(&e) => {
+
        Err(e) if e.is_not_found() => {
            let output = git::process::fetch_pack(
                Some(working.path()),
                stored,
modified crates/radicle-cli/src/commands/watch.rs
@@ -4,6 +4,7 @@ use std::{thread, time};
use anyhow::{anyhow, Context as _};

use radicle::git;
+
use radicle::git::raw::ErrorExt as _;
use radicle::prelude::{NodeId, RepoId};
use radicle::storage::{ReadRepository, ReadStorage};

@@ -169,7 +170,7 @@ fn reference<R: ReadRepository>(
) -> Result<Option<git::Oid>, git::raw::Error> {
    match repo.reference_oid(nid, qual) {
        Ok(oid) => Ok(Some(oid)),
-
        Err(e) if git::ext::is_not_found_err(&e) => Ok(None),
+
        Err(e) if e.is_not_found() => Ok(None),
        Err(e) => Err(e),
    }
}
modified crates/radicle-fetch/src/git/repository.rs
@@ -1,6 +1,7 @@
pub mod error;

use either::Either;
+
use radicle::git::raw::ErrorExt as _;
use radicle::git::{self, Namespaced, Oid, Qualified};
use radicle::storage::git::Repository;

@@ -53,7 +54,7 @@ fn find_and_peel(repo: &Repository, oid: Oid) -> Result<Oid, error::Ancestry> {
            .map_err(|err| error::Ancestry::Peel { oid, err })?
            .id()
            .into()),
-
        Err(e) if git::is_not_found_err(&e) => Err(error::Ancestry::Missing { oid }),
+
        Err(e) if e.is_not_found() => Err(error::Ancestry::Missing { oid }),
        Err(err) => Err(error::Ancestry::Object { oid, err }),
    }
}
modified crates/radicle/src/cob/identity.rs
@@ -126,7 +126,7 @@ pub enum ApplyError {
    #[error("document does not contain any changes to current identity")]
    DocUnchanged,
    #[error("git: {0}")]
-
    Git(#[from] git2::Error),
+
    Git(#[from] crate::git::raw::Error),
    #[error("git: {0}")]
    GitExt(#[from] git_ext::Error),
    #[error("identity document error: {0}")]
modified crates/radicle/src/cob/op.rs
@@ -21,7 +21,7 @@ pub enum OpEncodingError {
    #[error("encoding failed: {0}")]
    Encoding(#[from] serde_json::Error),
    #[error("git: {0}")]
-
    Git(#[from] git2::Error),
+
    Git(#[from] git::raw::Error),
}

#[derive(Error, Debug)]
modified crates/radicle/src/cob/stream.rs
@@ -8,6 +8,7 @@ use std::marker::PhantomData;

use serde::Deserialize;

+
use crate::git;
use crate::git::Oid;

use super::{ObjectId, Op, TypeName};
@@ -92,7 +93,7 @@ impl HasRoot for CobRange {
///
/// To construct a `Stream`, use [`Stream::new`].
pub struct Stream<'a, A> {
-
    repo: &'a git2::Repository,
+
    repo: &'a git::raw::Repository,
    range: CobRange,
    typename: TypeName,
    marker: PhantomData<A>,
@@ -101,7 +102,7 @@ pub struct Stream<'a, A> {
impl<'a, A> Stream<'a, A> {
    /// Construct a new stream providing the underlying `repo`, a [`CobRange`],
    /// and the [`TypeName`] of the COB that is being streamed.
-
    pub fn new(repo: &'a git2::Repository, range: CobRange, typename: TypeName) -> Self {
+
    pub fn new(repo: &'a git::raw::Repository, range: CobRange, typename: TypeName) -> Self {
        Self {
            repo,
            range,
@@ -185,7 +186,7 @@ mod tests {
        "xyz.radicle.test".parse::<TypeName>().unwrap()
    }

-
    fn gen_ops(repo: &git2::Repository, signer: &MockSigner) -> Vec<cob::Entry> {
+
    fn gen_ops(repo: &git::raw::Repository, signer: &MockSigner) -> Vec<cob::Entry> {
        // Number of ops
        let n = gen::<u8>(1).clamp(1, 10);
        let mut entries = Vec::with_capacity(n.into());
@@ -213,7 +214,7 @@ mod tests {
    }

    fn create_entry(
-
        repo: &git2::Repository,
+
        repo: &git::raw::Repository,
        signer: &MockSigner,
        contents: NonEmpty<Vec<u8>>,
        parent: Option<Oid>,
modified crates/radicle/src/cob/stream/error.rs
@@ -22,7 +22,7 @@ impl Stream {
#[derive(Debug, Error)]
pub enum Ops {
    #[error("failed to get a commit while iterating over stream: {source}")]
-
    Commit { source: git2::Error },
+
    Commit { source: crate::git::raw::Error },
    #[error("failed to load COB operation: {source}")]
    Load { source: op::LoadError },
    #[error("failed to load COB manifest: {source}")]
modified crates/radicle/src/cob/stream/iter.rs
@@ -39,7 +39,7 @@ impl From<PatternString> for Until {
/// from.
pub(super) struct WalkIter<'a> {
    /// Git repository for looking up the commit object during the revwalk.
-
    repo: &'a git2::Repository,
+
    repo: &'a git::raw::Repository,
    /// The root commit that is being walked from.
    ///
    /// N.b. This is required since ranges are non-inclusive in Git, and if the
@@ -47,7 +47,7 @@ pub(super) struct WalkIter<'a> {
    /// error.
    from: Option<Oid>,
    /// The revwalk that is being iterated over.
-
    inner: git2::Revwalk<'a>,
+
    inner: git::raw::Revwalk<'a>,
}

impl From<CobRange> for Walk {
@@ -76,10 +76,10 @@ impl Walk {
    }

    /// Get the iterator for the walk.
-
    pub(super) fn iter(self, repo: &git2::Repository) -> Result<WalkIter<'_>, git2::Error> {
+
    pub(super) fn iter(self, repo: &git::raw::Repository) -> Result<WalkIter<'_>, git::raw::Error> {
        let mut walk = repo.revwalk()?;
        // N.b. ensure that we start from the `self.from` commit.
-
        walk.set_sorting(git2::Sort::TOPOLOGICAL.union(git2::Sort::REVERSE))?;
+
        walk.set_sorting(git::raw::Sort::TOPOLOGICAL.union(git::raw::Sort::REVERSE))?;
        match self.until {
            Until::Tip(tip) => walk.push_range(&format!("{}..{}", self.from, tip))?,
            Until::Glob(glob) => {
@@ -97,7 +97,7 @@ impl Walk {
}

impl<'a> Iterator for WalkIter<'a> {
-
    type Item = Result<git2::Commit<'a>, git2::Error>;
+
    type Item = Result<git::raw::Commit<'a>, git::raw::Error>;

    fn next(&mut self) -> Option<Self::Item> {
        // N.b. ensure that we start using the `from` commit and use the revwalk
modified crates/radicle/src/git.rs
@@ -1,4 +1,5 @@
pub mod canonical;
+
pub mod raw;

use std::io;
use std::path::Path;
@@ -16,11 +17,9 @@ use crate::storage;
use crate::storage::refs::Refs;
use crate::storage::RemoteId;

-
pub use ext::is_not_found_err;
pub use ext::Error;
pub use ext::NotFound;
pub use ext::Oid;
-
pub use git2 as raw;
pub use git_ext::ref_format as fmt;
pub use git_ext::ref_format::{
    component, lit, name, qualified, refname, refspec,
@@ -31,6 +30,8 @@ pub use radicle_git_ext as ext;
pub use storage::git::transport::local::Url;
pub use storage::BranchName;

+
use raw::ErrorExt as _;
+

/// Default port of the `git` transport protocol.
pub const PROTOCOL_PORT: u16 = 9418;
/// Minimum required git version.
@@ -173,13 +174,13 @@ pub enum RefError {
        err: Box<dyn std::error::Error + Send + Sync + 'static>,
    },
    #[error(transparent)]
-
    Other(#[from] git2::Error),
+
    Other(#[from] raw::Error),
}

#[derive(thiserror::Error, Debug)]
pub enum ListRefsError {
    #[error("git error: {0}")]
-
    Git(#[from] git2::Error),
+
    Git(#[from] raw::Error),
    #[error("invalid ref: {0}")]
    InvalidRef(#[from] RefError),
}
@@ -189,7 +190,7 @@ pub mod refs {
    use radicle_cob as cob;

    /// Try to get a qualified reference from a generic reference.
-
    pub fn qualified_from<'a>(r: &'a git2::Reference) -> Result<(Qualified<'a>, Oid), RefError> {
+
    pub fn qualified_from<'a>(r: &'a raw::Reference) -> Result<(Qualified<'a>, Oid), RefError> {
        let name = r.name().ok_or(RefError::InvalidName)?;
        let refstr = RefStr::try_from_str(name)?;
        let target = r.resolve()?.target().ok_or(RefError::NoTarget)?;
@@ -462,9 +463,9 @@ pub mod refs {
pub fn remote_refs(url: &Url) -> Result<RandomMap<RemoteId, Refs>, ListRefsError> {
    let url = url.to_string();
    let mut remotes = RandomMap::default();
-
    let mut remote = git2::Remote::create_detached(url)?;
+
    let mut remote = raw::Remote::create_detached(url)?;

-
    remote.connect(git2::Direction::Fetch)?;
+
    remote.connect(raw::Direction::Fetch)?;

    let refs = remote.list()?;
    for r in refs {
@@ -557,9 +558,9 @@ where

/// Create an initial empty commit.
pub fn initial_commit<'a>(
-
    repo: &'a git2::Repository,
-
    sig: &git2::Signature,
-
) -> Result<git2::Commit<'a>, git2::Error> {
+
    repo: &'a raw::Repository,
+
    sig: &raw::Signature,
+
) -> Result<raw::Commit<'a>, raw::Error> {
    let tree_id = repo.index()?.write_tree()?;
    let tree = repo.find_tree(tree_id)?;
    let oid = repo.commit(None, sig, sig, "Initial commit", &tree, &[])?;
@@ -570,13 +571,13 @@ pub fn initial_commit<'a>(

/// Create a commit and update the given ref to it.
pub fn commit<'a>(
-
    repo: &'a git2::Repository,
-
    parent: &'a git2::Commit,
+
    repo: &'a raw::Repository,
+
    parent: &'a raw::Commit,
    target: &RefStr,
    message: &str,
-
    sig: &git2::Signature,
-
    tree: &git2::Tree,
-
) -> Result<git2::Commit<'a>, git2::Error> {
+
    sig: &raw::Signature,
+
    tree: &raw::Tree,
+
) -> Result<raw::Commit<'a>, raw::Error> {
    let oid = repo.commit(Some(target.as_str()), sig, sig, message, tree, &[parent])?;
    let commit = repo.find_commit(oid)?;

@@ -585,12 +586,12 @@ pub fn commit<'a>(

/// Create an empty commit on top of the parent.
pub fn empty_commit<'a>(
-
    repo: &'a git2::Repository,
-
    parent: &'a git2::Commit,
+
    repo: &'a raw::Repository,
+
    parent: &'a raw::Commit,
    target: &RefStr,
    message: &str,
-
    sig: &git2::Signature,
-
) -> Result<git2::Commit<'a>, git2::Error> {
+
    sig: &raw::Signature,
+
) -> Result<raw::Commit<'a>, raw::Error> {
    let tree = parent.tree()?;
    let oid = repo.commit(Some(target.as_str()), sig, sig, message, &tree, &[parent])?;
    let commit = repo.find_commit(oid)?;
@@ -599,7 +600,7 @@ pub fn empty_commit<'a>(
}

/// Get the repository head.
-
pub fn head(repo: &git2::Repository) -> Result<git2::Commit, git2::Error> {
+
pub fn head(repo: &raw::Repository) -> Result<raw::Commit, raw::Error> {
    let head = repo.head()?.peel_to_commit()?;

    Ok(head)
@@ -609,8 +610,8 @@ pub fn head(repo: &git2::Repository) -> Result<git2::Commit, git2::Error> {
pub fn write_tree<'r>(
    path: &Path,
    bytes: &[u8],
-
    repo: &'r git2::Repository,
-
) -> Result<git2::Tree<'r>, Error> {
+
    repo: &'r raw::Repository,
+
) -> Result<raw::Tree<'r>, Error> {
    let blob_id = repo.blob(bytes)?;
    let mut builder = repo.treebuilder(None)?;
    builder.insert(path, blob_id, 0o100_644)?;
@@ -624,7 +625,7 @@ pub fn write_tree<'r>(
/// Configure a radicle repository.
///
/// * Sets `push.default = upstream`.
-
pub fn configure_repository(repo: &git2::Repository) -> Result<(), git2::Error> {
+
pub fn configure_repository(repo: &raw::Repository) -> Result<(), raw::Error> {
    let mut cfg = repo.config()?;
    cfg.set_str("push.default", "upstream")?;

@@ -652,11 +653,11 @@ pub fn configure_repository(repo: &git2::Repository) -> Result<(), git2::Error>
///  2. `tagOpt = --no-tags` to ensure that tags are not fetched and stored
///     under `refs/tags`, again, because these are fetched by the `rad` remote.
pub fn configure_remote<'r>(
-
    repo: &'r git2::Repository,
+
    repo: &'r raw::Repository,
    name: &str,
    fetch: &Url,
    push: &Url,
-
) -> Result<git2::Remote<'r>, git2::Error> {
+
) -> Result<raw::Remote<'r>, raw::Error> {
    let fetchspec = format!("+refs/heads/*:refs/remotes/{name}/*");
    let remote = repo.remote_with_fetch(name, fetch.to_string().as_str(), &fetchspec)?;

@@ -679,14 +680,14 @@ pub fn configure_remote<'r>(
}

/// Fetch from the given `remote`.
-
pub fn fetch(repo: &git2::Repository, remote: &str) -> Result<(), git2::Error> {
+
pub fn fetch(repo: &raw::Repository, remote: &str) -> Result<(), raw::Error> {
    repo.find_remote(remote)?.fetch::<&str>(
        &[],
        Some(
-
            git2::FetchOptions::new()
+
            raw::FetchOptions::new()
                .update_fetchhead(false)
-
                .prune(git2::FetchPrune::On)
-
                .download_tags(git2::AutotagOption::None),
+
                .prune(raw::FetchPrune::On)
+
                .download_tags(raw::AutotagOption::None),
        ),
        None,
    )
@@ -694,10 +695,10 @@ pub fn fetch(repo: &git2::Repository, remote: &str) -> Result<(), git2::Error> {

/// Push `refspecs` to the given `remote` using the provided `namespace`.
pub fn push<'a>(
-
    repo: &git2::Repository,
+
    repo: &raw::Repository,
    remote: &str,
    refspecs: impl IntoIterator<Item = (&'a Qualified<'a>, &'a Qualified<'a>)>,
-
) -> Result<(), git2::Error> {
+
) -> Result<(), raw::Error> {
    let refspecs = refspecs
        .into_iter()
        .map(|(src, dst)| format!("{src}:{dst}"));
@@ -719,11 +720,11 @@ pub fn push<'a>(
///     merge = refs/heads/main
/// ```
pub fn set_upstream(
-
    repo: &git2::Repository,
+
    repo: &raw::Repository,
    remote: impl AsRef<str>,
    branch: impl AsRef<str>,
    merge: impl AsRef<str>,
-
) -> Result<(), git2::Error> {
+
) -> Result<(), raw::Error> {
    let remote = remote.as_ref();
    let branch = branch.as_ref();
    let merge = merge.as_ref();
@@ -733,14 +734,14 @@ pub fn set_upstream(
    let branch_merge = format!("branch.{branch}.merge");

    config.remove_multivar(&branch_remote, ".*").or_else(|e| {
-
        if ext::is_not_found_err(&e) {
+
        if e.is_not_found() {
            Ok(())
        } else {
            Err(e)
        }
    })?;
    config.remove_multivar(&branch_merge, ".*").or_else(|e| {
-
        if ext::is_not_found_err(&e) {
+
        if e.is_not_found() {
            Ok(())
        } else {
            Err(e)
@@ -752,14 +753,14 @@ pub fn set_upstream(
    Ok(())
}

-
pub fn init_default_branch(repo: &git2::Repository) -> Result<Option<String>, git2::Error> {
+
pub fn init_default_branch(repo: &raw::Repository) -> Result<Option<String>, raw::Error> {
    let config = repo.config().and_then(|mut c| c.snapshot())?;
    let default_branch = config.get_str("init.defaultbranch")?;
-
    let branch = repo.find_branch(default_branch, git2::BranchType::Local)?;
+
    let branch = repo.find_branch(default_branch, raw::BranchType::Local)?;
    Ok(branch.into_reference().shorthand().map(ToOwned::to_owned))
}

-
pub fn head_refname(repo: &git2::Repository) -> Result<Option<String>, git2::Error> {
+
pub fn head_refname(repo: &raw::Repository) -> Result<Option<String>, raw::Error> {
    let head = repo.head()?;
    match head.shorthand() {
        Some("HEAD") => Ok(None),
modified crates/radicle/src/git/canonical/effects.rs
@@ -1,6 +1,7 @@
use std::collections::{BTreeMap, BTreeSet};

use crate::git;
+
use crate::git::raw::ErrorExt as _;
use crate::git::{Oid, Qualified};
use crate::prelude::Did;

@@ -167,7 +168,7 @@ pub struct GraphDescendant {
// `git2` implementations of the above effects
// ===========================================

-
impl FindMergeBase for git2::Repository {
+
impl FindMergeBase for git::raw::Repository {
    fn merge_base(&self, a: Oid, b: Oid) -> Result<MergeBase, MergeBaseError> {
        self.merge_base(*a, *b)
            .map_err(|err| MergeBaseError {
@@ -183,7 +184,7 @@ impl FindMergeBase for git2::Repository {
    }
}

-
impl Ancestry for git2::Repository {
+
impl Ancestry for git::raw::Repository {
    fn graph_ahead_behind(
        &self,
        commit: Oid,
@@ -199,7 +200,7 @@ impl Ancestry for git2::Repository {
    }
}

-
impl FindObjects for git2::Repository {
+
impl FindObjects for git::raw::Repository {
    fn find_objects<'a, 'b, I>(
        &self,
        refname: &Qualified,
@@ -215,7 +216,7 @@ impl FindObjects for git2::Repository {
            let name = &refname.with_namespace(did.as_key().into());
            let reference = match self.find_reference(name.as_str()) {
                Ok(reference) => reference,
-
                Err(e) if git::ext::is_not_found_err(&e) => {
+
                Err(e) if e.is_not_found() => {
                    missing_refs.insert(name.to_owned());
                    continue;
                }
@@ -235,7 +236,7 @@ impl FindObjects for git2::Repository {
                        object.kind().map(|kind| kind.to_string()),
                    )
                }),
-
                Err(err) if git::ext::is_not_found_err(&err) => {
+
                Err(err) if err.is_not_found() => {
                    missing_objects.insert(*did, oid);
                    continue;
                }
modified crates/radicle/src/git/canonical/rules.rs
@@ -780,7 +780,7 @@ mod tests {
        doc.delegates().clone()
    }

-
    fn tag(name: RefString, head: git2::Oid, repo: &git2::Repository) -> git::Oid {
+
    fn tag(name: RefString, head: git::raw::Oid, repo: &git::raw::Repository) -> git::Oid {
        let commit = fixtures::commit(name.as_str(), &[head], repo);
        let target = repo.find_object(*commit, None).unwrap();
        let tagger = repo.signature().unwrap();
added crates/radicle/src/git/raw.rs
@@ -0,0 +1,58 @@
+
//! This module re-exports selected items from the [`git2`] crate and provides
+
//! an extension trait for its [`git2::Error`] type to more conveniently handle
+
//! errors associated with the code [`git2::ErrorCode::NotFound`].
+
//!
+
// Re-exports created by manually scanning the `heartwood` workspace on 2025-10-04.
+

+
// Re-exports that are only used within this crate.
+
pub(crate) use git2::{
+
    message_trailers_strs, AutotagOption, Blob, Config, FetchOptions, FetchPrune, Object,
+
    RemoteCallbacks, Revwalk, Sort,
+
};
+

+
// Re-exports that are used by other crates in the workspace, including this crate.
+
pub use git2::{
+
    Branch, BranchType, Commit, Direction, Error, ErrorClass, ErrorCode, FileMode, ObjectType, Oid,
+
    Reference, Remote, Repository, RepositoryInitOptions, RepositoryOpenFlags, Signature, Time,
+
    Tree,
+
};
+

+
// Re-exports that are used by other crates in the workspace, but *not* this crate.
+
pub use git2::{
+
    AnnotatedCommit, Diff, DiffFindOptions, DiffOptions, DiffStats, MergeAnalysis, MergeOptions,
+
};
+

+
// Re-exports for `radicle-cli`.
+
pub mod build {
+
    pub use git2::build::CheckoutBuilder;
+
}
+

+
pub(crate) mod transport {
+
    pub use git2::transport::{
+
        register, Service, SmartSubtransport, SmartSubtransportStream, Transport,
+
    };
+
}
+

+
/// An extension trait for [`git2::Error`] to more conveniently handle
+
/// errors with the code [`git2::ErrorCode::NotFound`].
+
pub trait ErrorExt {
+
    /// Returns `true` if the error associated with this error is [`git2::ErrorCode::NotFound`].
+
    fn is_not_found(&self) -> bool;
+
}
+

+
impl ErrorExt for git2::Error {
+
    fn is_not_found(&self) -> bool {
+
        self.code() == git2::ErrorCode::NotFound
+
    }
+
}
+

+
impl ErrorExt for git_ext::Error {
+
    fn is_not_found(&self) -> bool {
+
        use git_ext::Error::*;
+
        match self {
+
            Git(e) => e.is_not_found(),
+
            NotFound(_) => true,
+
            _ => false,
+
        }
+
    }
+
}
modified crates/radicle/src/identity/doc.rs
@@ -22,6 +22,7 @@ use crate::crypto;
use crate::crypto::Signature;
use crate::git;
use crate::git::canonical::rules;
+
use crate::git::raw::ErrorExt as _;
use crate::identity::{project::Project, Did};
use crate::node::device::Device;
use crate::storage;
@@ -54,7 +55,7 @@ pub enum DocError {
    #[error("git: {0}")]
    GitExt(#[from] git::Error),
    #[error("git: {0}")]
-
    Git(#[from] git2::Error),
+
    Git(#[from] git::raw::Error),
    #[error("missing identity document")]
    Missing,
}
@@ -71,9 +72,8 @@ impl DocError {
    /// Whether this error is caused by the document not being found.
    pub fn is_not_found(&self) -> bool {
        match self {
-
            Self::GitExt(git::Error::NotFound(_)) => true,
-
            Self::GitExt(git::Error::Git(e)) if git::is_not_found_err(e) => true,
-
            Self::Git(err) if git::is_not_found_err(err) => true,
+
            Self::GitExt(e) => e.is_not_found(),
+
            Self::Git(e) => e.is_not_found(),
            _ => false,
        }
    }
@@ -687,7 +687,7 @@ impl Doc {
    }

    /// Construct a [`Doc`] contained in the provided Git blob.
-
    pub fn from_blob(blob: &git2::Blob) -> Result<Self, DocError> {
+
    pub fn from_blob(blob: &git::raw::Blob) -> Result<Self, DocError> {
        RawDoc::from_json(blob.content())?.verified()
    }

@@ -830,7 +830,7 @@ impl Doc {
    pub(crate) fn blob_at<R: ReadRepository>(
        commit: Oid,
        repo: &R,
-
    ) -> Result<git2::Blob, DocError> {
+
    ) -> Result<git::raw::Blob, DocError> {
        let path = Path::new("embeds").join(*PATH);
        repo.blob_at(commit, path.as_path()).map_err(DocError::from)
    }
@@ -843,7 +843,7 @@ impl Doc {
            serde_json::Serializer::with_formatter(&mut buf, CanonicalFormatter::new());

        self.serialize(&mut serializer)?;
-
        let oid = git2::Oid::hash_object(git2::ObjectType::Blob, &buf)?;
+
        let oid = git::raw::Oid::hash_object(git::raw::ObjectType::Blob, &buf)?;

        Ok((oid.into(), buf))
    }
@@ -1157,10 +1157,13 @@ mod test {
        let remote = arbitrary::gen::<RemoteId>(1);
        let proj = arbitrary::gen::<RepoId>(1);
        let repo = storage.create(proj).unwrap();
-
        let oid = git2::Oid::from_str("2d52a53ce5e4f141148a5f770cfd3ead2d6a45b8").unwrap();
+
        let oid = git::raw::Oid::from_str("2d52a53ce5e4f141148a5f770cfd3ead2d6a45b8").unwrap();

        let err = repo.identity_head_of(&remote).unwrap_err();
-
        matches!(err, git::ext::Error::NotFound(_));
+
        {
+
            use crate::git::raw::ErrorExt as _;
+
            assert!(err.is_not_found());
+
        }

        let err = Doc::load_at(oid.into(), &repo).unwrap_err();
        assert!(err.is_not_found());
modified crates/radicle/src/identity/doc/id.rs
@@ -13,7 +13,7 @@ pub const RAD_PREFIX: &str = "rad:";
#[derive(Error, Debug)]
pub enum IdError {
    #[error("invalid git object id: {0}")]
-
    InvalidOid(#[from] git2::Error),
+
    InvalidOid(#[from] git::raw::Error),
    #[error(transparent)]
    Multibase(#[from] multibase::Error),
}
@@ -102,8 +102,8 @@ impl From<git::Oid> for RepoId {
    }
}

-
impl From<git2::Oid> for RepoId {
-
    fn from(oid: git2::Oid) -> Self {
+
impl From<git::raw::Oid> for RepoId {
+
    fn from(oid: git::raw::Oid) -> Self {
        Self(oid.into())
    }
}
modified crates/radicle/src/profile.rs
@@ -88,7 +88,7 @@ pub mod env {

    /// Get the configured pager program from the environment.
    pub fn pager() -> Option<String> {
-
        if let Ok(cfg) = git2::Config::open_default() {
+
        if let Ok(cfg) = crate::git::raw::Config::open_default() {
            if let Ok(pager) = cfg.get_string("core.pager") {
                return Some(pager);
            }
modified crates/radicle/src/rad.rs
@@ -39,7 +39,7 @@ pub enum InitError {
    #[error("project payload: {0}")]
    ProjectPayload(String),
    #[error("git: {0}")]
-
    Git(#[from] git2::Error),
+
    Git(#[from] git::raw::Error),
    #[error("i/o: {0}")]
    Io(#[from] io::Error),
    #[error("storage: {0}")]
@@ -48,7 +48,7 @@ pub enum InitError {

/// Initialize a new radicle project from a git repository.
pub fn init<G, S>(
-
    repo: &git2::Repository,
+
    repo: &git::raw::Repository,
    name: ProjectName,
    description: &str,
    default_branch: BranchName,
@@ -96,7 +96,7 @@ where
}

fn init_configure<G>(
-
    repo: &git2::Repository,
+
    repo: &git::raw::Repository,
    stored: &Repository,
    default_branch: &BranchName,
    url: &git::Url,
@@ -152,7 +152,7 @@ where
#[derive(Error, Debug)]
pub enum ForkError {
    #[error("git: {0}")]
-
    Git(#[from] git2::Error),
+
    Git(#[from] git::raw::Error),
    #[error("storage: {0}")]
    Storage(#[from] storage::Error),
    #[error("payload: {0}")]
@@ -236,7 +236,7 @@ pub enum CheckoutError {
        stdout: String,
    },
    #[error("git: {0}")]
-
    Git(#[from] git2::Error),
+
    Git(#[from] git::raw::Error),
    #[error("payload: {0}")]
    Payload(#[from] doc::PayloadError),
    #[error("repository `{0}` was not found in storage")]
@@ -253,19 +253,19 @@ pub fn checkout<P: AsRef<Path>, S: storage::ReadStorage>(
    path: P,
    storage: &S,
    bare: bool,
-
) -> Result<git2::Repository, CheckoutError> {
+
) -> Result<git::raw::Repository, CheckoutError> {
    // TODO: Decide on whether we can use `clone_local`
    // TODO: Look into sharing object databases.
    let doc = storage.get(proj)?.ok_or(CheckoutError::NotFound(proj))?;
    let project = doc.project()?;

-
    let mut opts = git2::RepositoryInitOptions::new();
+
    let mut opts = git::raw::RepositoryInitOptions::new();
    opts.no_reinit(true)
        .external_template(false)
        .description(project.description())
        .bare(bare);

-
    let repo = git2::Repository::init_opts(path.as_ref(), &opts)?;
+
    let repo = git::raw::Repository::init_opts(path.as_ref(), &opts)?;
    let url = git::Url::from(proj);

    // Configure repository for radicle.
@@ -335,7 +335,7 @@ pub fn checkout<P: AsRef<Path>, S: storage::ReadStorage>(
#[derive(Error, Debug)]
pub enum RemoteError {
    #[error("git: {0}")]
-
    Git(#[from] git2::Error),
+
    Git(#[from] git::raw::Error),
    #[error("invalid remote url: {0}")]
    Url(#[from] transport::local::UrlError),
    #[error("invalid utf-8 string")]
@@ -347,9 +347,9 @@ pub enum RemoteError {
}

/// Get the radicle ("rad") remote of a repository, and return the associated project id.
-
pub fn remote(repo: &git2::Repository) -> Result<(git2::Remote<'_>, RepoId), RemoteError> {
+
pub fn remote(repo: &git::raw::Repository) -> Result<(git::raw::Remote<'_>, RepoId), RemoteError> {
    let remote = repo.find_remote(&REMOTE_NAME).map_err(|e| {
-
        if e.code() == git2::ErrorCode::NotFound {
+
        if e.code() == git::raw::ErrorCode::NotFound {
            RemoteError::NotFound(REMOTE_NAME.to_string())
        } else {
            RemoteError::from(e)
@@ -362,9 +362,9 @@ pub fn remote(repo: &git2::Repository) -> Result<(git2::Remote<'_>, RepoId), Rem
}

/// Delete the radicle ("rad") remote of a repository.
-
pub fn remove_remote(repo: &git2::Repository) -> Result<(), RemoteError> {
+
pub fn remove_remote(repo: &git::raw::Repository) -> Result<(), RemoteError> {
    repo.remote_delete(&REMOTE_NAME).map_err(|e| {
-
        if e.code() == git2::ErrorCode::NotFound {
+
        if e.code() == git::raw::ErrorCode::NotFound {
            RemoteError::NotFound(REMOTE_NAME.to_string())
        } else {
            RemoteError::from(e)
@@ -380,7 +380,7 @@ pub enum CwdError {

    #[error("Detection failed (git: '{git}', jj: '{jj}')")]
    Detection {
-
        git: git2::Error,
+
        git: git::raw::Error,
        jj: JujutsuGitRootError,
    },
}
@@ -395,7 +395,7 @@ pub enum CwdError {
/// This function should only perform read operations since we do not
/// want to modify the wrong repository in the case that it found a
/// Git repository that is not a Radicle repository.
-
pub fn cwd() -> Result<(git2::Repository, RepoId), CwdError> {
+
pub fn cwd() -> Result<(git::raw::Repository, RepoId), CwdError> {
    let repo =
        repo().or_else(|git| repo_jj_git_root().map_err(|jj| CwdError::Detection { git, jj }))?;

@@ -404,23 +404,23 @@ pub fn cwd() -> Result<(git2::Repository, RepoId), CwdError> {
}

/// Get the repository of project in specified directory
-
pub fn at(path: impl AsRef<Path>) -> Result<(git2::Repository, RepoId), RemoteError> {
-
    let repo = git2::Repository::open(path)?;
+
pub fn at(path: impl AsRef<Path>) -> Result<(git::raw::Repository, RepoId), RemoteError> {
+
    let repo = git::raw::Repository::open(path)?;
    let (_, id) = remote(&repo)?;

    Ok((repo, id))
}

/// Get the current Git repository.
-
pub fn repo() -> Result<git2::Repository, git2::Error> {
-
    let mut flags = git2::RepositoryOpenFlags::empty();
+
pub fn repo() -> Result<git::raw::Repository, git::raw::Error> {
+
    let mut flags = git::raw::RepositoryOpenFlags::empty();
    // Allow to search upwards.
-
    flags.set(git2::RepositoryOpenFlags::NO_SEARCH, false);
+
    flags.set(git::raw::RepositoryOpenFlags::NO_SEARCH, false);
    // Allow to use `GIT_DIR` env.
-
    flags.set(git2::RepositoryOpenFlags::FROM_ENV, true);
+
    flags.set(git::raw::RepositoryOpenFlags::FROM_ENV, true);

    let ceilings: &[&str] = &[];
-
    let repo = git2::Repository::open_ext(Path::new("."), flags, ceilings)?;
+
    let repo = git::raw::Repository::open_ext(Path::new("."), flags, ceilings)?;

    Ok(repo)
}
@@ -428,7 +428,7 @@ pub fn repo() -> Result<git2::Repository, git2::Error> {
#[derive(Error, Debug)]
pub enum JujutsuGitRootError {
    #[error("git: {0}")]
-
    Git(#[from] git2::Error),
+
    Git(#[from] git::raw::Error),

    #[error("i/o: {0}")]
    Io(#[from] io::Error),
@@ -438,7 +438,7 @@ pub enum JujutsuGitRootError {
}

/// Get the Git repo underlying the current Jujutsu repository.
-
pub fn repo_jj_git_root() -> Result<git2::Repository, JujutsuGitRootError> {
+
pub fn repo_jj_git_root() -> Result<git::raw::Repository, JujutsuGitRootError> {
    let output = std::process::Command::new("jj")
        .args(["git", "root"])
        .output()?;
@@ -450,7 +450,7 @@ pub fn repo_jj_git_root() -> Result<git2::Repository, JujutsuGitRootError> {
    }

    let path = std::path::PathBuf::from(String::from_utf8_lossy(&output.stdout).to_string().trim());
-
    Ok(git2::Repository::open(path)?)
+
    Ok(git::raw::Repository::open(path)?)
}

/// Setup patch upstream branch such that `git push` updates the patch.
modified crates/radicle/src/schemars_ext.rs
@@ -94,12 +94,12 @@ pub(crate) mod git {

    /// See [`crate::git::Oid`]
    /// See [`::git_ext::Oid`]
-
    /// See [`::git2::Oid`]
+
    /// See [`::git::raw::Oid`]
    ///
    /// A Git Object Identifier in hexadecimal encoding.
    #[derive(JsonSchema)]
    #[schemars(
-
        remote = "git2::Oid",
+
        remote = "git::raw::Oid",
        description = "A Git Object Identifier (SHA-1 or SHA-256 hash) in hexadecimal encoding."
    )]
    pub(crate) struct Oid(
modified crates/radicle/src/storage.rs
@@ -16,6 +16,7 @@ pub use radicle_git_ext::Oid;

use crate::cob;
use crate::collections::RandomMap;
+
use crate::git::raw::ErrorExt as _;
use crate::git::{canonical, ext as git_ext};
use crate::git::{refspec::Refspec, PatternString, Qualified, RefError, RefStr, RefString};
use crate::identity::{doc, Did, PayloadError};
@@ -133,9 +134,9 @@ pub enum RepositoryError {
impl RepositoryError {
    pub fn is_not_found(&self) -> bool {
        match self {
-
            Self::Storage(e) if e.is_not_found() => true,
-
            Self::Git(e) if git_ext::is_not_found_err(e) => true,
-
            Self::GitExt(git_ext::Error::NotFound(_)) => true,
+
            Self::Storage(e) => e.is_not_found(),
+
            Self::GitExt(e) => e.is_not_found(),
+
            Self::Git(e) => e.is_not_found(),
            _ => false,
        }
    }
@@ -153,7 +154,7 @@ pub enum Error {
    #[error(transparent)]
    Refs(#[from] refs::Error),
    #[error("git: {0}")]
-
    Git(#[from] git2::Error),
+
    Git(#[from] git::raw::Error),
    #[error("git: {0}")]
    Ext(#[from] git::ext::Error),
    #[error("invalid repository identifier {0:?}")]
@@ -167,7 +168,7 @@ impl Error {
    pub fn is_not_found(&self) -> bool {
        match self {
            Self::Io(e) if e.kind() == io::ErrorKind::NotFound => true,
-
            Self::Git(e) if git::ext::is_not_found_err(e) => true,
+
            Self::Git(e) if e.is_not_found() => true,
            Self::Doc(e) if e.is_not_found() => true,
            _ => false,
        }
@@ -179,7 +180,7 @@ impl Error {
#[allow(clippy::large_enum_variant)]
pub enum FetchError {
    #[error("git: {0}")]
-
    Git(#[from] git2::Error),
+
    Git(#[from] git::raw::Error),
    #[error("i/o: {0}")]
    Io(#[from] io::Error),
    #[error(transparent)]
@@ -503,16 +504,20 @@ pub trait ReadRepository: Sized + ValidateRepository {
    fn id(&self) -> RepoId;

    /// Returns `true` if there are no references in the repository.
-
    fn is_empty(&self) -> Result<bool, git2::Error>;
+
    fn is_empty(&self) -> Result<bool, git::raw::Error>;

    /// The [`Path`] to the git repository.
    fn path(&self) -> &Path;

    /// Get a blob in this repository at the given commit and path.
-
    fn blob_at<P: AsRef<Path>>(&self, commit: Oid, path: P) -> Result<git2::Blob, git_ext::Error>;
+
    fn blob_at<P: AsRef<Path>>(
+
        &self,
+
        commit: Oid,
+
        path: P,
+
    ) -> Result<git::raw::Blob, git_ext::Error>;

    /// Get a blob in this repository, given its id.
-
    fn blob(&self, oid: Oid) -> Result<git2::Blob, git_ext::Error>;
+
    fn blob(&self, oid: Oid) -> Result<git::raw::Blob, git_ext::Error>;

    /// Get the head of this repository.
    ///
@@ -572,18 +577,18 @@ pub trait ReadRepository: Sized + ValidateRepository {
        &self,
        remote: &RemoteId,
        reference: &Qualified,
-
    ) -> Result<git2::Reference, git_ext::Error>;
+
    ) -> Result<git::raw::Reference, git_ext::Error>;

-
    /// Get the [`git2::Commit`] found using its `oid`.
+
    /// Get the [`git::raw::Commit`] found using its `oid`.
    ///
    /// Returns `Err` if the commit did not exist.
-
    fn commit(&self, oid: Oid) -> Result<git2::Commit, git::ext::Error>;
+
    fn commit(&self, oid: Oid) -> Result<git::raw::Commit, git::ext::Error>;

    /// Perform a revision walk of a commit history starting from the given head.
-
    fn revwalk(&self, head: Oid) -> Result<git2::Revwalk, git2::Error>;
+
    fn revwalk(&self, head: Oid) -> Result<git::raw::Revwalk, git::raw::Error>;

    /// Check if the underlying ODB contains the given `oid`.
-
    fn contains(&self, oid: Oid) -> Result<bool, git2::Error>;
+
    fn contains(&self, oid: Oid) -> Result<bool, git::raw::Error>;

    /// Check whether the given commit is an ancestor of another commit.
    fn is_ancestor_of(&self, ancestor: Oid, head: Oid) -> Result<bool, git::ext::Error>;
@@ -692,7 +697,7 @@ pub trait WriteRepository: ReadRepository + SignRepository {
    /// Set the user info of the Git repository.
    fn set_user(&self, info: &UserInfo) -> Result<(), Error>;
    /// Get the underlying git repository.
-
    fn raw(&self) -> &git2::Repository;
+
    fn raw(&self) -> &git::raw::Repository;
}

/// Allows signing refs.
modified crates/radicle/src/storage/git.rs
@@ -14,6 +14,7 @@ use std::{fs, io};
use crypto::Verified;

use crate::git::canonical::Quorum;
+
use crate::git::raw::ErrorExt as _;
use crate::identity::crefs::GetCanonicalRefs as _;
use crate::identity::doc::DocError;
use crate::identity::{CanonicalRefs, Doc, DocAt, RepoId};
@@ -56,10 +57,10 @@ pub struct Ref {
    pub namespace: Option<RemoteId>,
}

-
impl TryFrom<git2::Reference<'_>> for Ref {
+
impl TryFrom<git::raw::Reference<'_>> for Ref {
    type Error = RefError;

-
    fn try_from(r: git2::Reference) -> Result<Self, Self::Error> {
+
    fn try_from(r: git::raw::Reference) -> Result<Self, Self::Error> {
        let name = r.name().ok_or(RefError::InvalidName)?;
        let (namespace, name) = match git::parse_ref_namespaced::<RemoteId>(name) {
            Ok((namespace, refname)) => (Some(namespace), refname.to_ref_string()),
@@ -279,7 +280,7 @@ pub struct Repository {
    /// The repository identifier (RID).
    pub id: RepoId,
    /// The backing Git repository.
-
    pub backend: git2::Repository,
+
    pub backend: git::raw::Repository,
}

impl AsRef<Repository> for Repository {
@@ -380,12 +381,12 @@ pub enum Validation {
impl Repository {
    /// Open an existing repository.
    pub fn open<P: AsRef<Path>>(path: P, id: RepoId) -> Result<Self, RepositoryError> {
-
        let backend = git2::Repository::open_ext(
+
        let backend = git::raw::Repository::open_ext(
            path.as_ref(),
-
            git2::RepositoryOpenFlags::empty()
-
                | git2::RepositoryOpenFlags::BARE
-
                | git2::RepositoryOpenFlags::NO_DOTGIT
-
                | git2::RepositoryOpenFlags::NO_SEARCH,
+
            git::raw::RepositoryOpenFlags::empty()
+
                | git::raw::RepositoryOpenFlags::BARE
+
                | git::raw::RepositoryOpenFlags::NO_DOTGIT
+
                | git::raw::RepositoryOpenFlags::NO_SEARCH,
            &[] as &[&std::ffi::OsStr],
        )?;

@@ -394,9 +395,9 @@ impl Repository {

    /// Create a new repository.
    pub fn create<P: AsRef<Path>>(path: P, id: RepoId, info: &UserInfo) -> Result<Self, Error> {
-
        let backend = git2::Repository::init_opts(
+
        let backend = git::raw::Repository::init_opts(
            &path,
-
            git2::RepositoryInitOptions::new()
+
            git::raw::RepositoryInitOptions::new()
                .bare(true)
                .no_reinit(true)
                .external_template(false),
@@ -506,7 +507,7 @@ impl Repository {
    /// Iterate over all references.
    pub fn references(
        &self,
-
    ) -> Result<impl Iterator<Item = Result<Ref, refs::Error>> + '_, git2::Error> {
+
    ) -> Result<impl Iterator<Item = Result<Ref, refs::Error>> + '_, git::raw::Error> {
        let refs = self
            .backend
            .references()?
@@ -539,7 +540,7 @@ impl Repository {

    pub fn remote_ids(
        &self,
-
    ) -> Result<impl Iterator<Item = Result<RemoteId, refs::Error>> + '_, git2::Error> {
+
    ) -> Result<impl Iterator<Item = Result<RemoteId, refs::Error>> + '_, git::raw::Error> {
        let iter = self.backend.references_glob(SIGREFS_GLOB.as_str())?.map(
            |reference| -> Result<RemoteId, refs::Error> {
                let r = reference?;
@@ -556,7 +557,7 @@ impl Repository {
        &self,
    ) -> Result<
        impl Iterator<Item = Result<(RemoteId, Remote<Verified>), refs::Error>> + '_,
-
        git2::Error,
+
        git::raw::Error,
    > {
        let remotes =
            self.backend
@@ -652,7 +653,7 @@ impl ReadRepository for Repository {
        self.id
    }

-
    fn is_empty(&self) -> Result<bool, git2::Error> {
+
    fn is_empty(&self) -> Result<bool, git::raw::Error> {
        Ok(self.remotes()?.next().is_none())
    }

@@ -660,7 +661,7 @@ impl ReadRepository for Repository {
        self.backend.path()
    }

-
    fn blob_at<P: AsRef<Path>>(&self, commit: Oid, path: P) -> Result<git2::Blob, git::Error> {
+
    fn blob_at<P: AsRef<Path>>(&self, commit: Oid, path: P) -> Result<git::raw::Blob, git::Error> {
        let commit = self.backend.find_commit(*commit)?;
        let tree = commit.tree()?;
        let entry = tree.get_path(path.as_ref())?;
@@ -674,7 +675,7 @@ impl ReadRepository for Repository {
        Ok(blob)
    }

-
    fn blob(&self, oid: Oid) -> Result<git2::Blob, git::Error> {
+
    fn blob(&self, oid: Oid) -> Result<git::raw::Blob, git::Error> {
        self.backend.find_blob(oid.into()).map_err(git::Error::from)
    }

@@ -682,7 +683,7 @@ impl ReadRepository for Repository {
        &self,
        remote: &RemoteId,
        name: &git::Qualified,
-
    ) -> Result<git2::Reference, git::Error> {
+
    ) -> Result<git::raw::Reference, git::Error> {
        let name = name.with_namespace(remote.into());
        self.backend.find_reference(&name).map_err(git::Error::from)
    }
@@ -698,13 +699,13 @@ impl ReadRepository for Repository {
        Ok(oid.into())
    }

-
    fn commit(&self, oid: Oid) -> Result<git2::Commit, git::Error> {
+
    fn commit(&self, oid: Oid) -> Result<git::raw::Commit, git::Error> {
        self.backend
            .find_commit(oid.into())
            .map_err(git::Error::from)
    }

-
    fn revwalk(&self, head: Oid) -> Result<git2::Revwalk, git2::Error> {
+
    fn revwalk(&self, head: Oid) -> Result<git::raw::Revwalk, git::raw::Error> {
        let mut revwalk = self.backend.revwalk()?;
        revwalk.push(head.into())?;

@@ -817,7 +818,7 @@ impl ReadRepository for Repository {

        match result {
            Ok(oid) => Ok(oid),
-
            Err(err) if git::ext::is_not_found_err(&err) => self.canonical_identity_head(),
+
            Err(err) if err.is_not_found() => self.canonical_identity_head(),
            Err(err) => Err(err.into()),
        }
    }
@@ -936,7 +937,7 @@ impl WriteRepository for Repository {
        Ok(())
    }

-
    fn raw(&self) -> &git2::Repository {
+
    fn raw(&self) -> &git::raw::Repository {
        &self.backend
    }
}
@@ -986,7 +987,7 @@ pub mod trailers {

    pub fn parse_signatures(msg: &str) -> Result<HashMap<PublicKey, Signature>, Error> {
        let trailers =
-
            git2::message_trailers_strs(msg).map_err(|_| Error::SignatureTrailerFormat)?;
+
            git::raw::message_trailers_strs(msg).map_err(|_| Error::SignatureTrailerFormat)?;
        let mut signatures = HashMap::with_capacity(trailers.len());

        for (key, val) in trailers.iter() {
@@ -1097,7 +1098,7 @@ mod tests {
            fixtures::project(tmp.path().join("project"), &storage, &signer).unwrap();
        let stored = storage.repository(rid).unwrap();
        let sig =
-
            git2::Signature::now(&alice.to_string(), "anonymous@radicle.example.com").unwrap();
+
            git::raw::Signature::now(&alice.to_string(), "anonymous@radicle.example.com").unwrap();
        let head = working.head().unwrap().peel_to_commit().unwrap();

        git::commit(
modified crates/radicle/src/storage/git/cob.rs
@@ -33,7 +33,7 @@ pub enum ObjectsError {
    #[error(transparent)]
    Convert(#[from] cob::object::storage::convert::Error),
    #[error(transparent)]
-
    Git(#[from] git2::Error),
+
    Git(#[from] git::raw::Error),
    #[error(transparent)]
    GitExt(#[from] git_ext::Error),
}
@@ -43,7 +43,7 @@ pub enum TypesError {
    #[error(transparent)]
    Convert(#[from] cob::object::storage::convert::Error),
    #[error(transparent)]
-
    Git(#[from] git2::Error),
+
    Git(#[from] git::raw::Error),
    #[error(transparent)]
    ParseKey(#[from] crypto::Error),
    #[error(transparent)]
@@ -55,12 +55,12 @@ pub enum TypesError {
impl cob::Store for Repository {}

impl change::Storage for Repository {
-
    type StoreError = <git2::Repository as change::Storage>::StoreError;
-
    type LoadError = <git2::Repository as change::Storage>::LoadError;
+
    type StoreError = <git::raw::Repository as change::Storage>::StoreError;
+
    type LoadError = <git::raw::Repository as change::Storage>::LoadError;

-
    type ObjectId = <git2::Repository as change::Storage>::ObjectId;
-
    type Parent = <git2::Repository as change::Storage>::Parent;
-
    type Signatures = <git2::Repository as change::Storage>::Signatures;
+
    type ObjectId = <git::raw::Repository as change::Storage>::ObjectId;
+
    type Parent = <git::raw::Repository as change::Storage>::Parent;
+
    type Signatures = <git::raw::Repository as change::Storage>::Signatures;

    fn store<Signer>(
        &self,
@@ -91,8 +91,8 @@ impl change::Storage for Repository {
impl cob::object::Storage for Repository {
    type ObjectsError = ObjectsError;
    type TypesError = TypesError;
-
    type UpdateError = git2::Error;
-
    type RemoveError = git2::Error;
+
    type UpdateError = git::raw::Error;
+
    type RemoveError = git::raw::Error;

    type Namespace = NodeId;

@@ -200,12 +200,12 @@ impl<'a, R> DraftStore<'a, R> {
impl<R: storage::WriteRepository> cob::Store for DraftStore<'_, R> {}

impl<R: storage::WriteRepository> change::Storage for DraftStore<'_, R> {
-
    type StoreError = <git2::Repository as change::Storage>::StoreError;
-
    type LoadError = <git2::Repository as change::Storage>::LoadError;
+
    type StoreError = <git::raw::Repository as change::Storage>::StoreError;
+
    type LoadError = <git::raw::Repository as change::Storage>::LoadError;

-
    type ObjectId = <git2::Repository as change::Storage>::ObjectId;
-
    type Parent = <git2::Repository as change::Storage>::Parent;
-
    type Signatures = <git2::Repository as change::Storage>::Signatures;
+
    type ObjectId = <git::raw::Repository as change::Storage>::ObjectId;
+
    type Parent = <git::raw::Repository as change::Storage>::Parent;
+
    type Signatures = <git::raw::Repository as change::Storage>::Signatures;

    fn store<Signer>(
        &self,
@@ -274,7 +274,7 @@ impl<R: storage::ReadRepository> ReadRepository for DraftStore<'_, R> {
        self.repo.id()
    }

-
    fn is_empty(&self) -> Result<bool, git2::Error> {
+
    fn is_empty(&self) -> Result<bool, git::raw::Error> {
        self.repo.is_empty()
    }

@@ -290,11 +290,11 @@ impl<R: storage::ReadRepository> ReadRepository for DraftStore<'_, R> {
        self.repo.path()
    }

-
    fn commit(&self, oid: Oid) -> Result<git2::Commit, git_ext::Error> {
+
    fn commit(&self, oid: Oid) -> Result<git::raw::Commit, git_ext::Error> {
        self.repo.commit(oid)
    }

-
    fn revwalk(&self, head: Oid) -> Result<git2::Revwalk, git2::Error> {
+
    fn revwalk(&self, head: Oid) -> Result<git::raw::Revwalk, git::raw::Error> {
        self.repo.revwalk(head)
    }

@@ -310,7 +310,7 @@ impl<R: storage::ReadRepository> ReadRepository for DraftStore<'_, R> {
        &self,
        oid: git_ext::Oid,
        path: P,
-
    ) -> Result<git2::Blob, git_ext::Error> {
+
    ) -> Result<git::raw::Blob, git_ext::Error> {
        self.repo.blob_at(oid, path)
    }

@@ -322,7 +322,7 @@ impl<R: storage::ReadRepository> ReadRepository for DraftStore<'_, R> {
        &self,
        remote: &RemoteId,
        reference: &git::Qualified,
-
    ) -> Result<git2::Reference, git_ext::Error> {
+
    ) -> Result<git::raw::Reference, git_ext::Error> {
        self.repo.reference(remote, reference)
    }

@@ -381,8 +381,8 @@ impl<R: storage::ReadRepository> ReadRepository for DraftStore<'_, R> {
impl<R: storage::WriteRepository> cob::object::Storage for DraftStore<'_, R> {
    type ObjectsError = ObjectsError;
    type TypesError = git::ext::Error;
-
    type UpdateError = git2::Error;
-
    type RemoveError = git2::Error;
+
    type UpdateError = git::raw::Error;
+
    type RemoveError = git::raw::Error;

    type Namespace = NodeId;

modified crates/radicle/src/storage/git/transport/local.rs
@@ -7,6 +7,7 @@ use std::process;
use std::str::FromStr;
use std::sync::Once;

+
use crate::git;
use crate::storage;
use crate::storage::git::Storage;

@@ -25,20 +26,19 @@ struct Local {
    child: RefCell<Option<process::Child>>,
}

-
impl git2::transport::SmartSubtransport for Local {
+
impl crate::git::raw::transport::SmartSubtransport for Local {
    fn action(
        &self,
        url: &str,
-
        service: git2::transport::Service,
-
    ) -> Result<Box<dyn git2::transport::SmartSubtransportStream>, git2::Error> {
-
        let url = Url::from_str(url).map_err(|e| git2::Error::from_str(e.to_string().as_str()))?;
+
        service: git::raw::transport::Service,
+
    ) -> Result<Box<dyn git::raw::transport::SmartSubtransportStream>, git::raw::Error> {
+
        let url =
+
            Url::from_str(url).map_err(|e| git::raw::Error::from_str(e.to_string().as_str()))?;
        let service: &str = match service {
-
            git2::transport::Service::UploadPack | git2::transport::Service::UploadPackLs => {
-
                "upload-pack"
-
            }
-
            git2::transport::Service::ReceivePack | git2::transport::Service::ReceivePackLs => {
-
                "receive-pack"
-
            }
+
            git::raw::transport::Service::UploadPack
+
            | git::raw::transport::Service::UploadPackLs => "upload-pack",
+
            git::raw::transport::Service::ReceivePack
+
            | git::raw::transport::Service::ReceivePackLs => "receive-pack",
        };
        let git_dir = THREAD_STORAGE
            .with(|t| {
@@ -46,7 +46,9 @@ impl git2::transport::SmartSubtransport for Local {
                    .as_ref()
                    .map(|s| storage::git::paths::repository(&s, &url.repo))
            })
-
            .ok_or_else(|| git2::Error::from_str("local transport storage was not registered"))?;
+
            .ok_or_else(|| {
+
                git::raw::Error::from_str("local transport storage was not registered")
+
            })?;

        let mut cmd = process::Command::new("git");

@@ -61,7 +63,7 @@ impl git2::transport::SmartSubtransport for Local {
            .stdout(process::Stdio::piped())
            .stderr(process::Stdio::inherit())
            .spawn()
-
            .map_err(|e| git2::Error::from_str(e.to_string().as_str()))?;
+
            .map_err(|e| git::raw::Error::from_str(e.to_string().as_str()))?;

        let stdin = child.stdin.take().expect("taking stdin is safe");
        let stdout = child.stdout.take().expect("taking stdout is safe");
@@ -71,19 +73,19 @@ impl git2::transport::SmartSubtransport for Local {
        Ok(Box::new(ChildStream { stdout, stdin }))
    }

-
    fn close(&self) -> Result<(), git2::Error> {
+
    fn close(&self) -> Result<(), git::raw::Error> {
        if let Some(mut child) = self.child.take() {
            let result = child
                .wait()
-
                .map_err(|e| git2::Error::from_str(e.to_string().as_str()))?;
+
                .map_err(|e| git::raw::Error::from_str(e.to_string().as_str()))?;

            if !result.success() {
                return if let Some(code) = result.code() {
-
                    Err(git2::Error::from_str(
+
                    Err(git::raw::Error::from_str(
                        format!("transport: child process exited with error code {code}").as_str(),
                    ))
                } else {
-
                    Err(git2::Error::from_str(
+
                    Err(git::raw::Error::from_str(
                        "transport: child process exited with unknown error",
                    ))
                };
@@ -103,8 +105,8 @@ pub fn register(storage: Storage) {
    });

    REGISTER.call_once(|| unsafe {
-
        git2::transport::register(Url::SCHEME, move |remote| {
-
            git2::transport::Transport::smart(remote, false, Local::default())
+
        git::raw::transport::register(Url::SCHEME, move |remote| {
+
            git::raw::transport::Transport::smart(remote, false, Local::default())
        })
        .expect("local transport registration");
    });
modified crates/radicle/src/storage/git/transport/remote/mock.rs
@@ -8,6 +8,7 @@ use std::thread::ThreadId;
use std::{process, thread};

use super::Url;
+
use crate::git;
use crate::storage::git::transport::ChildStream;
use crate::storage::RemoteId;

@@ -19,19 +20,21 @@ static NODES: LazyLock<Mutex<HashMap<(ThreadId, RemoteId), PathBuf>>> =
#[derive(Default)]
struct MockTransport;

-
impl git2::transport::SmartSubtransport for MockTransport {
+
impl git::raw::transport::SmartSubtransport for MockTransport {
    fn action(
        &self,
        url: &str,
-
        service: git2::transport::Service,
-
    ) -> Result<Box<dyn git2::transport::SmartSubtransportStream>, git2::Error> {
-
        let url = Url::from_str(url).map_err(|e| git2::Error::from_str(e.to_string().as_str()))?;
+
        service: git::raw::transport::Service,
+
    ) -> Result<Box<dyn git::raw::transport::SmartSubtransportStream>, git::raw::Error> {
+
        let url =
+
            Url::from_str(url).map_err(|e| git::raw::Error::from_str(e.to_string().as_str()))?;
        let id = thread::current().id();
        let nodes = NODES.lock().expect("lock cannot be poisoned");
        let storage = if let Some(storage) = nodes.get(&(id, url.node)) {
            match service {
-
                git2::transport::Service::ReceivePack | git2::transport::Service::ReceivePackLs => {
-
                    return Err(git2::Error::from_str(
+
                git::raw::transport::Service::ReceivePack
+
                | git::raw::transport::Service::ReceivePackLs => {
+
                    return Err(git::raw::Error::from_str(
                        "git-receive-pack is not supported with the mock transport",
                    ));
                }
@@ -39,7 +42,7 @@ impl git2::transport::SmartSubtransport for MockTransport {
            }
            storage
        } else {
-
            return Err(git2::Error::from_str(&format!(
+
            return Err(git::raw::Error::from_str(&format!(
                "node {} was not registered with the mock transport",
                url.node
            )));
@@ -76,7 +79,7 @@ impl git2::transport::SmartSubtransport for MockTransport {
        Ok(Box::new(ChildStream { stdout, stdin }))
    }

-
    fn close(&self) -> Result<(), git2::Error> {
+
    fn close(&self) -> Result<(), git::raw::Error> {
        Ok(())
    }
}
@@ -86,8 +89,8 @@ pub fn register(node: &RemoteId, path: &Path) {
    static REGISTER: Once = Once::new();

    REGISTER.call_once(|| unsafe {
-
        git2::transport::register(Url::SCHEME, move |remote| {
-
            git2::transport::Transport::smart(remote, false, MockTransport)
+
        git::raw::transport::register(Url::SCHEME, move |remote| {
+
            git::raw::transport::Transport::smart(remote, false, MockTransport)
        })
        .expect("transport registration is successful");
    });
modified crates/radicle/src/storage/refs.rs
@@ -14,6 +14,7 @@ use thiserror::Error;

use crate::git;
use crate::git::ext as git_ext;
+
use crate::git::raw::ErrorExt as _;
use crate::git::Oid;
use crate::node::device::Device;
use crate::profile::env;
@@ -55,7 +56,7 @@ pub enum Error {
    #[error("invalid reference: {0}")]
    Ref(#[from] git::RefError),
    #[error(transparent)]
-
    Git(#[from] git2::Error),
+
    Git(#[from] git::raw::Error),
    #[error(transparent)]
    GitExt(#[from] git_ext::Error),
}
@@ -64,9 +65,8 @@ impl Error {
    /// Whether this error is caused by a reference not being found.
    pub fn is_not_found(&self) -> bool {
        match self {
-
            Self::GitExt(git::Error::NotFound(_)) => true,
-
            Self::GitExt(git::Error::Git(e)) if git::is_not_found_err(e) => true,
-
            Self::Git(e) if git::is_not_found_err(e) => true,
+
            Self::GitExt(e) => e.is_not_found(),
+
            Self::Git(e) => e.is_not_found(),
            _ => false,
        }
    }
@@ -322,8 +322,8 @@ impl SignedRefs<Verified> {
                    env::GIT_COMMITTER_DATE
                );
            };
-
            let time = git2::Time::new(timestamp, 0);
-
            git2::Signature::new("radicle", remote.to_string().as_str(), &time)?
+
            let time = git::raw::Time::new(timestamp, 0);
+
            git::raw::Signature::new("radicle", remote.to_string().as_str(), &time)?
        } else {
            raw.signature()?
        };
@@ -334,13 +334,13 @@ impl SignedRefs<Verified> {
            &author,
            "Update signed refs\n",
            &tree,
-
            &parent.iter().collect::<Vec<&git2::Commit>>(),
+
            &parent.iter().collect::<Vec<&git::raw::Commit>>(),
        );

        match commit {
            Ok(oid) => Ok(Updated::Updated { oid: oid.into() }),
            Err(e) => match (e.class(), e.code()) {
-
                (git2::ErrorClass::Object, git2::ErrorCode::Modified) => {
+
                (git::raw::ErrorClass::Object, git::raw::ErrorCode::Modified) => {
                    log::warn!("Concurrent modification of refs: {e:?}");

                    Err(Error::Git(e))
@@ -430,7 +430,7 @@ impl SignedRefsAt {
    {
        let at = match RefsAt::new(repo, remote) {
            Ok(RefsAt { at, .. }) => at,
-
            Err(e) if git::is_not_found_err(&e) => return Ok(None),
+
            Err(e) if e.is_not_found() => return Ok(None),
            Err(e) => return Err(e.into()),
        };
        Self::load_at(at, remote, repo).map(Some)
@@ -471,7 +471,7 @@ pub mod canonical {
        #[error(transparent)]
        Io(#[from] io::Error),
        #[error(transparent)]
-
        Git(#[from] git2::Error),
+
        Git(#[from] git::raw::Error),
    }
}

@@ -581,7 +581,7 @@ mod tests {
            .unwrap();

            let paris_head = bob_working.find_commit(paris_head).unwrap();
-
            let bob_sig = git2::Signature::now("bob", "bob@example.com").unwrap();
+
            let bob_sig = git::raw::Signature::now("bob", "bob@example.com").unwrap();
            let bob_head = git::empty_commit(
                &bob_working,
                &paris_head,
modified crates/radicle/src/test.rs
@@ -6,6 +6,7 @@ pub mod storage;

use super::storage::{Namespaces, RefUpdate};

+
use crate::git;
use crate::prelude::NodeId;
use crate::storage::WriteRepository;

@@ -22,13 +23,13 @@ pub fn fetch<W: WriteRepository>(
        Namespaces::Followed(followed) => followed.into_iter().next(),
    };
    let mut updates = Vec::new();
-
    let mut callbacks = git2::RemoteCallbacks::new();
-
    let mut opts = git2::FetchOptions::default();
+
    let mut callbacks = git::raw::RemoteCallbacks::new();
+
    let mut opts = git::raw::FetchOptions::default();
    let refspec = if let Some(namespace) = namespace {
-
        opts.prune(git2::FetchPrune::On);
+
        opts.prune(git::raw::FetchPrune::On);
        format!("refs/namespaces/{namespace}/refs/*:refs/namespaces/{namespace}/refs/*")
    } else {
-
        opts.prune(git2::FetchPrune::Off);
+
        opts.prune(git::raw::FetchPrune::Off);
        "refs/namespaces/*:refs/namespaces/*".to_owned()
    };

@@ -295,22 +296,22 @@ pub mod setup {
    }

    pub fn commit<S: AsRef<Path>, T: AsRef<[u8]>>(
-
        repo: &git2::Repository,
+
        repo: &git::raw::Repository,
        refname: &git::Qualified,
        blobs: impl IntoIterator<Item = (S, T)>,
-
        parents: &[&git2::Commit<'_>],
+
        parents: &[&git::raw::Commit<'_>],
    ) -> git::Oid {
        let tree = {
            let mut tb = repo.treebuilder(None).unwrap();
            for (name, blob) in blobs.into_iter() {
                let oid = repo.blob(blob.as_ref()).unwrap();
-
                tb.insert(name.as_ref(), oid, git2::FileMode::Blob.into())
+
                tb.insert(name.as_ref(), oid, git::raw::FileMode::Blob.into())
                    .unwrap();
            }
            tb.write().unwrap()
        };
        let tree = repo.find_tree(tree).unwrap();
-
        let author = git2::Signature::now("anonymous", "anonymous@example.com").unwrap();
+
        let author = git::raw::Signature::now("anonymous", "anonymous@example.com").unwrap();

        repo.commit(
            Some(refname.as_str()),
modified crates/radicle/src/test/fixtures.rs
@@ -72,7 +72,15 @@ pub fn project<P, G>(
    path: P,
    storage: &Storage,
    signer: &Device<G>,
-
) -> Result<(RepoId, SignedRefs<Verified>, git2::Repository, git2::Oid), rad::InitError>
+
) -> Result<
+
    (
+
        RepoId,
+
        SignedRefs<Verified>,
+
        git::raw::Repository,
+
        git::raw::Oid,
+
    ),
+
    rad::InitError,
+
>
where
    P: AsRef<Path>,
    G: crypto::signature::Signer<crypto::Signature>,
@@ -94,19 +102,19 @@ where
}

/// Creates a regular repository at the given path with a couple of commits.
-
pub fn repository<P: AsRef<Path>>(path: P) -> (git2::Repository, git2::Oid) {
+
pub fn repository<P: AsRef<Path>>(path: P) -> (git::raw::Repository, git::raw::Oid) {
    let (repo, oid) = repository_with(
        path,
-
        git2::RepositoryInitOptions::new().external_template(false),
+
        git::raw::RepositoryInitOptions::new().external_template(false),
    );
    repo.checkout_head(None).unwrap();
    (repo, oid)
}

-
pub fn bare_repository<P: AsRef<Path>>(path: P) -> (git2::Repository, git2::Oid) {
+
pub fn bare_repository<P: AsRef<Path>>(path: P) -> (git::raw::Repository, git::raw::Oid) {
    repository_with(
        path,
-
        git2::RepositoryInitOptions::new()
+
        git::raw::RepositoryInitOptions::new()
            .external_template(false)
            .bare(true),
    )
@@ -114,17 +122,21 @@ pub fn bare_repository<P: AsRef<Path>>(path: P) -> (git2::Repository, git2::Oid)

fn repository_with<P: AsRef<Path>>(
    path: P,
-
    opts: &mut git2::RepositoryInitOptions,
-
) -> (git2::Repository, git2::Oid) {
-
    let repo = git2::Repository::init_opts(path, opts).unwrap();
+
    opts: &mut git::raw::RepositoryInitOptions,
+
) -> (git::raw::Repository, git::raw::Oid) {
+
    let repo = git::raw::Repository::init_opts(path, opts).unwrap();

    {
        let mut config = repo.config().unwrap();
        config.set_str("user.name", USER_NAME).unwrap();
        config.set_str("user.email", USER_EMAIL).unwrap();
    }
-
    let sig =
-
        git2::Signature::new(USER_NAME, USER_EMAIL, &git2::Time::new(RADICLE_EPOCH, 0)).unwrap();
+
    let sig = git::raw::Signature::new(
+
        USER_NAME,
+
        USER_EMAIL,
+
        &git::raw::Time::new(RADICLE_EPOCH, 0),
+
    )
+
    .unwrap();
    let head = git::initial_commit(&repo, &sig).unwrap();
    let tree = git::write_tree(Path::new("README"), "Hello World!\n".as_bytes(), &repo).unwrap();
    let oid = {
@@ -149,10 +161,14 @@ fn repository_with<P: AsRef<Path>>(
}

/// Create an empty commit on the current branch.
-
pub fn commit(msg: &str, parents: &[git2::Oid], repo: &git2::Repository) -> git::Oid {
+
pub fn commit(msg: &str, parents: &[git::raw::Oid], repo: &git::raw::Repository) -> git::Oid {
    let head = repo.head().unwrap();
-
    let sig =
-
        git2::Signature::new(USER_NAME, USER_EMAIL, &git2::Time::new(RADICLE_EPOCH, 0)).unwrap();
+
    let sig = git::raw::Signature::new(
+
        USER_NAME,
+
        USER_EMAIL,
+
        &git::raw::Time::new(RADICLE_EPOCH, 0),
+
    )
+
    .unwrap();
    let tree = head.peel_to_commit().unwrap().tree().unwrap();
    let parents = parents
        .iter()
@@ -166,19 +182,28 @@ pub fn commit(msg: &str, parents: &[git2::Oid], repo: &git2::Repository) -> git:
}

/// Create an (annotated) tag of the given commit.
-
pub fn tag(name: &str, message: &str, commit: git2::Oid, repo: &git2::Repository) -> git::Oid {
+
pub fn tag(
+
    name: &str,
+
    message: &str,
+
    commit: git::raw::Oid,
+
    repo: &git::raw::Repository,
+
) -> git::Oid {
    let target = repo
-
        .find_object(commit, Some(git2::ObjectType::Commit))
+
        .find_object(commit, Some(git::raw::ObjectType::Commit))
        .unwrap();
-
    let tagger =
-
        git2::Signature::new(USER_NAME, USER_EMAIL, &git2::Time::new(RADICLE_EPOCH, 0)).unwrap();
+
    let tagger = git::raw::Signature::new(
+
        USER_NAME,
+
        USER_EMAIL,
+
        &git::raw::Time::new(RADICLE_EPOCH, 0),
+
    )
+
    .unwrap();
    repo.tag(name, &target, &tagger, message, false)
        .unwrap()
        .into()
}

/// Populate a repository with commits, branches and blobs.
-
pub fn populate(repo: &git2::Repository, scale: usize) -> Vec<git::Qualified> {
+
pub fn populate(repo: &git::raw::Repository, scale: usize) -> Vec<git::Qualified> {
    assert!(
        scale <= 8,
        "Scale parameter must be less than or equal to 8"
@@ -198,7 +223,7 @@ pub fn populate(repo: &git2::Repository, scale: usize) -> Vec<git::Qualified> {
            .to_lowercase();
        let name =
            git::refname!("feature").join(git::RefString::try_from(random.as_str()).unwrap());
-
        let signature = git2::Signature::now("Radicle", "radicle@radicle.xyz").unwrap();
+
        let signature = git::raw::Signature::now("Radicle", "radicle@radicle.xyz").unwrap();

        rng.fill(&mut buffer);

@@ -241,13 +266,13 @@ pub mod gen {
    }

    /// Creates a regular repository at the given path with a couple of commits.
-
    pub fn repository<P: AsRef<Path>>(path: P) -> (git2::Repository, git2::Oid) {
-
        let repo = git2::Repository::init_opts(
+
    pub fn repository<P: AsRef<Path>>(path: P) -> (git::raw::Repository, git::raw::Oid) {
+
        let repo = git::raw::Repository::init_opts(
            path,
-
            git2::RepositoryInitOptions::new().external_template(false),
+
            git::raw::RepositoryInitOptions::new().external_template(false),
        )
        .unwrap();
-
        let sig = git2::Signature::now(string(6).as_str(), email().as_str()).unwrap();
+
        let sig = git::raw::Signature::now(string(6).as_str(), email().as_str()).unwrap();
        let head = git::initial_commit(&repo, &sig).unwrap();
        let tree =
            git::write_tree(Path::new("README"), "Hello World!\n".as_bytes(), &repo).unwrap();
modified crates/radicle/src/test/storage.rs
@@ -199,7 +199,7 @@ impl ReadRepository for MockRepository {
        self.id
    }

-
    fn is_empty(&self) -> Result<bool, git2::Error> {
+
    fn is_empty(&self) -> Result<bool, git::raw::Error> {
        Ok(self.remotes.is_empty())
    }

@@ -215,17 +215,17 @@ impl ReadRepository for MockRepository {
        todo!()
    }

-
    fn commit(&self, oid: Oid) -> Result<git2::Commit, git_ext::Error> {
+
    fn commit(&self, oid: Oid) -> Result<git::raw::Commit, git_ext::Error> {
        Err(git_ext::Error::NotFound(git_ext::NotFound::NoSuchObject(
            *oid,
        )))
    }

-
    fn revwalk(&self, _head: Oid) -> Result<git2::Revwalk, git2::Error> {
+
    fn revwalk(&self, _head: Oid) -> Result<git::raw::Revwalk, git::raw::Error> {
        todo!()
    }

-
    fn contains(&self, oid: Oid) -> Result<bool, git2::Error> {
+
    fn contains(&self, oid: Oid) -> Result<bool, git::raw::Error> {
        Ok(self
            .remotes
            .values()
@@ -236,7 +236,7 @@ impl ReadRepository for MockRepository {
        Ok(true)
    }

-
    fn blob(&self, _oid: Oid) -> Result<git2::Blob, git_ext::Error> {
+
    fn blob(&self, _oid: Oid) -> Result<git::raw::Blob, git_ext::Error> {
        todo!()
    }

@@ -244,7 +244,7 @@ impl ReadRepository for MockRepository {
        &self,
        _oid: git_ext::Oid,
        _path: P,
-
    ) -> Result<git2::Blob, git_ext::Error> {
+
    ) -> Result<git::raw::Blob, git_ext::Error> {
        todo!()
    }

@@ -252,7 +252,7 @@ impl ReadRepository for MockRepository {
        &self,
        _remote: &RemoteId,
        _reference: &git::Qualified,
-
    ) -> Result<git2::Reference, git_ext::Error> {
+
    ) -> Result<git::raw::Reference, git_ext::Error> {
        todo!()
    }

@@ -322,7 +322,7 @@ impl ReadRepository for MockRepository {
}

impl WriteRepository for MockRepository {
-
    fn raw(&self) -> &git2::Repository {
+
    fn raw(&self) -> &git::raw::Repository {
        todo!()
    }

modified flake.nix
@@ -176,25 +176,46 @@
        // {
          pre-commit-check = let
            grep = rec {
-
              words = ["radicle.xyz" "radicle.zulipchat.com"];
-
              after = map id words;
+
              generators = [
+
                {
+
                  word = "radicle.xyz";
+
                  files = "\\.rs$";
+
                  excludes = [];
+
                }
+
                {
+
                  word = "radicle.zulipchat.com";
+
                  files = "\\.rs$";
+
                  excludes = [];
+
                }
+
                {
+
                  word = "git2::";
+
                  files = "^crates/radicle/.*\\.rs$";
+
                  excludes = ["crates/radicle/src/git/raw.rs"];
+
                }
+
              ];
+
              after = map id generators;
              prefix = "grep-";
-
              id = word: prefix + word;
-
              hooks = builtins.listToAttrs (map (word: {
+
              id = {word, ...}: prefix + word;
+
              hooks = builtins.listToAttrs (map (generator: {
                  # "," is problematic, as this is used to split
                  # lists of hook names, when skipping, see:
                  # https://pre-commit.com/#temporarily-disabling-hooks
-
                  name = assert !lib.hasInfix "," word; id word;
-
                  value = hook word;
+
                  name = assert !lib.hasInfix "," generator.word; id generator;
+
                  value = hook generator;
                })
-
                words);
-
              hook = word: {
+
                generators);
+
              hook = {
+
                word,
+
                files,
+
                excludes,
+
              }: {
+
                inherit files excludes;
                enable = true;
                entry = builtins.toString (pkgs.writeShellScript
                  "grep-${word}"
                  "! ${lib.getExe pkgs.ripgrep} --context=3 --fixed-strings '${word}' $@");
-
                name = "Avoid '${word}' in Rust code";
-
                files = "\\.rs$";
+
                name = "Avoid '${word}' in '${files}'";
+
                stages = ["pre-commit" "pre-push"];
                pass_filenames = true;
              };
            };