Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
A Little Less `git2`
Merged lorenz opened 6 months ago

(I should write up a lengthy description…)

137 files changed +3208 -1298 292befdb efe10f95
modified Cargo.lock
@@ -241,12 +241,6 @@ checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa"

[[package]]
name = "base64"
-
version = "0.13.1"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
-

-
[[package]]
-
name = "base64"
version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
@@ -1119,9 +1113,9 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"

[[package]]
name = "git-ref-format"
-
version = "0.3.1"
+
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "7428e0d6e549a9a613d6f019b839a0f5142c331295b79e119ca8f4faac145da1"
+
checksum = "76314f6eb43910ebc5eb89a1f0728724a6b9144dabd18bca4e7cc7c01c3804e3"
dependencies = [
 "git-ref-format-core",
 "git-ref-format-macro",
@@ -1129,9 +1123,9 @@ dependencies = [

[[package]]
name = "git-ref-format-core"
-
version = "0.3.1"
+
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "bbaeb9672a55e9e32cb6d3ef781e7526b25ab97d499fae71615649340b143424"
+
checksum = "ae6d9ee666ca7d4ad49cbf7174f785f299a18b37565694f665e8c7df24999cdd"
dependencies = [
 "bstr",
 "serde",
@@ -1140,12 +1134,12 @@ dependencies = [

[[package]]
name = "git-ref-format-macro"
-
version = "0.3.1"
+
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "3b6ca5353accc201f6324dff744ba4660099546d4daf187ba868f07562e36ca4"
+
checksum = "2dc2ded12a6ea2b6e63afaf09415e4c15bf4baa74e530acd9daed9ff47e7dd41"
dependencies = [
 "git-ref-format-core",
-
 "proc-macro-error",
+
 "proc-macro-error2",
 "quote",
 "syn 2.0.89",
]
@@ -2240,12 +2234,6 @@ dependencies = [

[[package]]
name = "nonempty"
-
version = "0.5.0"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "9ff7ac1e5ea23db6d61ad103e91864675049644bf47c35912336352fa4e9c109"
-

-
[[package]]
-
name = "nonempty"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "995defdca0a589acfdd1bd2e8e3b896b4d4f7675a31fd14c32611440c7f608e6"
@@ -2643,27 +2631,25 @@ dependencies = [
]

[[package]]
-
name = "proc-macro-error"
-
version = "1.0.4"
+
name = "proc-macro-error-attr2"
+
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+
checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
dependencies = [
-
 "proc-macro-error-attr",
 "proc-macro2",
 "quote",
-
 "syn 1.0.109",
-
 "version_check",
]

[[package]]
-
name = "proc-macro-error-attr"
-
version = "1.0.4"
+
name = "proc-macro-error2"
+
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+
checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
dependencies = [
+
 "proc-macro-error-attr2",
 "proc-macro2",
 "quote",
-
 "version_check",
+
 "syn 2.0.89",
]

[[package]]
@@ -2751,13 +2737,15 @@ dependencies = [
 "localtime",
 "log",
 "multibase",
-
 "nonempty 0.9.0",
+
 "nonempty",
 "pretty_assertions",
 "qcheck",
 "qcheck-macros",
 "radicle-cob",
 "radicle-crypto",
-
 "radicle-git-ext",
+
 "radicle-git-metadata",
+
 "radicle-git-ref-format",
+
 "radicle-oid",
 "radicle-ssh",
 "schemars",
 "serde",
@@ -2779,19 +2767,18 @@ dependencies = [
 "chrono",
 "clap",
 "dunce",
-
 "git-ref-format",
 "human-panic",
 "itertools",
 "lexopt",
 "localtime",
 "log",
-
 "nonempty 0.9.0",
+
 "nonempty",
 "pretty_assertions",
 "radicle",
 "radicle-cli-test",
 "radicle-cob",
 "radicle-crypto",
-
 "radicle-git-ext",
+
 "radicle-git-ref-format",
 "radicle-node",
 "radicle-surf",
 "radicle-term",
@@ -2837,14 +2824,17 @@ name = "radicle-cob"
version = "0.17.0"
dependencies = [
 "fastrand",
+
 "git-ref-format-core",
 "git2",
 "log",
-
 "nonempty 0.9.0",
+
 "nonempty",
 "qcheck",
 "qcheck-macros",
 "radicle-crypto",
 "radicle-dag",
-
 "radicle-git-ext",
+
 "radicle-git-metadata",
+
 "radicle-git-ref-format",
+
 "radicle-oid",
 "serde",
 "serde_json",
 "signature 2.2.0",
@@ -2894,17 +2884,18 @@ dependencies = [
 "gix-protocol",
 "gix-transport",
 "log",
-
 "nonempty 0.9.0",
+
 "nonempty",
 "radicle",
-
 "radicle-git-ext",
+
 "radicle-git-ref-format",
+
 "radicle-oid",
 "thiserror 1.0.69",
]

[[package]]
name = "radicle-git-ext"
-
version = "0.8.1"
+
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "4b78c26e67d1712ad5a0c602ae3b236609461372ac04e200bda359fe4a1c6650"
+
checksum = "fb3de6999a8ff570e0dd92a04ba3132853dd08a3bcfc2f7faf56de7b1bd36053"
dependencies = [
 "git-ref-format",
 "git2",
@@ -2915,6 +2906,20 @@ dependencies = [
]

[[package]]
+
name = "radicle-git-metadata"
+
version = "0.1.0"
+
dependencies = [
+
 "thiserror 1.0.69",
+
]
+

+
[[package]]
+
name = "radicle-git-ref-format"
+
version = "0.1.0"
+
dependencies = [
+
 "git-ref-format-core",
+
]
+

+
[[package]]
name = "radicle-node"
version = "0.16.0"
dependencies = [
@@ -2930,13 +2935,12 @@ dependencies = [
 "localtime",
 "log",
 "mio 1.0.4",
-
 "nonempty 0.9.0",
+
 "nonempty",
 "qcheck",
 "qcheck-macros",
 "radicle",
 "radicle-crypto",
 "radicle-fetch",
-
 "radicle-git-ext",
 "radicle-protocol",
 "radicle-signals",
 "radicle-systemd",
@@ -2954,6 +2958,19 @@ dependencies = [
]

[[package]]
+
name = "radicle-oid"
+
version = "0.1.0"
+
dependencies = [
+
 "git2",
+
 "gix-hash",
+
 "qcheck",
+
 "qcheck-macros",
+
 "radicle-git-ref-format",
+
 "schemars",
+
 "serde",
+
]
+

+
[[package]]
name = "radicle-protocol"
version = "0.4.0"
dependencies = [
@@ -2964,14 +2981,13 @@ dependencies = [
 "fastrand",
 "localtime",
 "log",
-
 "nonempty 0.9.0",
+
 "nonempty",
 "paste",
 "qcheck",
 "qcheck-macros",
 "radicle",
 "radicle-crypto",
 "radicle-fetch",
-
 "radicle-git-ext",
 "scrypt",
 "serde",
 "serde_json",
@@ -2988,7 +3004,6 @@ dependencies = [
 "radicle",
 "radicle-cli",
 "radicle-crypto",
-
 "radicle-git-ext",
 "thiserror 1.0.69",
]

@@ -3022,22 +3037,22 @@ dependencies = [

[[package]]
name = "radicle-std-ext"
-
version = "0.1.0"
+
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "db20136bbc9ae63f3fec8e5a6c369f4902fac2244501b5dfc6d668e43475aaa4"
+
checksum = "5310e7a04506b6ce92dc9c47b26bd24c1c680937a3dcd13cd20847f89dbda32a"

[[package]]
name = "radicle-surf"
-
version = "0.22.0"
+
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "fb308c3989087f71e43d8c7a2737273fdc7fbcd3e6628af81a42f601ae64f314"
+
checksum = "2f08adc954d4a49287d86bd1ce14ca7e34a11a3acd904044db30551ca8f4e46a"
dependencies = [
 "anyhow",
-
 "base64 0.13.1",
+
 "base64 0.21.7",
 "flate2",
 "git2",
 "log",
-
 "nonempty 0.5.0",
+
 "nonempty",
 "radicle-git-ext",
 "radicle-std-ext",
 "tar",
modified Cargo.toml
@@ -28,8 +28,7 @@ crossbeam-channel = "0.5.6"
cyphernet = "0.5.2"
dunce = "1.0.5"
fastrand = { version = "2.0.0", default-features = false }
-
git-ref-format-core = { version = "0.3.0", default-features = false }
-
git2 = { version = "0.19.0", default-features = false }
+
git2 = { version = "0.19.0", default-features = false, features = ["vendored-libgit2"] }
human-panic = "2"
itertools = "0.14"
lexopt = "0.3.0"
@@ -48,15 +47,17 @@ radicle-cob = { version = "0.17", path = "crates/radicle-cob" }
radicle-crypto = { version = "0.14", path = "crates/radicle-crypto" }
radicle-dag = { version = "0.10", path = "crates/radicle-dag" }
radicle-fetch = { version = "0.16", path = "crates/radicle-fetch" }
-
radicle-git-ext = { version = "0.8", default-features = false }
+
radicle-git-metadata = { version = "0.1.0", path = "crates/radicle-git-metadata", default-features = false }
+
radicle-git-ref-format = { version = "0.1.0", path = "crates/radicle-git-ref-format", default-features = false }
radicle-node = { version = "0.16", path = "crates/radicle-node" }
+
radicle-oid = { version = "0.1.0", path = "crates/radicle-oid", default-features = false }
radicle-protocol = { version = "0.4", path = "crates/radicle-protocol" }
radicle-signals = { version = "0.11", path = "crates/radicle-signals" }
radicle-ssh = { version = "0.10", path = "crates/radicle-ssh", default-features = false }
radicle-systemd = { version = "0.11", path = "crates/radicle-systemd" }
radicle-term = { version = "0.16", path = "crates/radicle-term" }
-
schemars = { version = "1.0.4" }
-
serde = "1.0"
+
schemars = { version = "1.0.4", default-features = false }
+
serde = { version = "1.0", default-features = false }
serde_json = "1.0"
shlex = "1.1.0"
signature = "2.2"
@@ -67,6 +68,13 @@ thiserror = "1.0"
winpipe = "0.1.1"
zeroize = "1.5.7"

+
# Crates from the "radicle-git" workspace. These should be synced manually.
+
# When updating, start from `radicle-surf`:
+
# `radicle-surf` → `radicle-git-ext` → `git-ref-format` → `git-ref-format-core`
+
# Also note that `radicle-surf → git2` so try to also sync with `git2`.
+
git-ref-format-core = { version = "0.5.0", default-features = false }
+
radicle-surf = "0.25.0"
+

[workspace.lints]
clippy.type_complexity = "allow"
clippy.enum_variant_names = "allow"
modified crates/radicle-cli/Cargo.toml
@@ -18,7 +18,6 @@ anyhow = "1"
chrono = { workspace = true, features = ["clock", "std"] }
clap = { version = "4.5.44", features = ["derive"] }
dunce = { workspace = true }
-
git-ref-format = { version = "0.3.0", features = ["macro"] }
human-panic.workspace = true
itertools.workspace = true
lexopt = { workspace = true }
@@ -28,10 +27,8 @@ nonempty = { workspace = true }
radicle = { workspace = true, features = ["logger", "schemars"] }
radicle-cob = { workspace = true }
radicle-crypto = { workspace = true }
-
# N.b. this is required to use macros, even though it's re-exported
-
# through radicle
-
radicle-git-ext = { workspace = true, features = ["serde"] }
-
radicle-surf = "0.22.0"
+
radicle-git-ref-format = { workspace = true, features = ["macro"] }
+
radicle-surf = { workspace = true }
radicle-term = { workspace = true }
schemars = { workspace = true }
serde = { workspace = true }
modified crates/radicle-cli/src/commands/checkout.rs
@@ -156,9 +156,9 @@ pub fn setup_remotes(
pub fn setup_remote(
    setup: &project::SetupRemote,
    remote_id: &NodeId,
-
    remote_name: Option<git::RefString>,
+
    remote_name: Option<git::fmt::RefString>,
    aliases: &impl AliasStore,
-
) -> anyhow::Result<git::RefString> {
+
) -> anyhow::Result<git::fmt::RefString> {
    let remote_name = if let Some(name) = remote_name {
        name
    } else {
@@ -167,7 +167,7 @@ pub fn setup_remote(
        } else {
            remote_id.to_human()
        };
-
        git::RefString::try_from(name.as_str())
+
        git::fmt::RefString::try_from(name.as_str())
            .map_err(|_| anyhow!("invalid remote name: '{name}'"))?
    };
    let (remote, branch) = setup.run(&remote_name, *remote_id)?;
modified crates/radicle-cli/src/commands/inbox.rs
@@ -4,16 +4,17 @@ use std::process;

use anyhow::anyhow;

-
use git_ref_format::Qualified;
use localtime::LocalTime;
use radicle::cob::TypedId;
+
use radicle::git::fmt::Qualified;
+
use radicle::git::BranchName;
use radicle::identity::Identity;
use radicle::issue::cache::Issues as _;
use radicle::node::notifications;
use radicle::node::notifications::*;
use radicle::patch::cache::Patches as _;
use radicle::prelude::{NodeId, Profile, RepoId};
-
use radicle::storage::{BranchName, ReadRepository, ReadStorage};
+
use radicle::storage::{ReadRepository, ReadStorage};
use radicle::{cob, git, Storage};

use term::Element as _;
modified crates/radicle-cli/src/commands/init.rs
@@ -16,9 +16,9 @@ use serde_json as json;

use radicle::crypto::ssh;
use radicle::explorer::ExplorerUrl;
+
use radicle::git::fmt::RefString;
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};
use radicle::node::events::UploadPack;
modified crates/radicle-cli/src/commands/patch.rs
@@ -24,7 +24,7 @@ use anyhow::anyhow;

use radicle::cob::patch::PatchId;
use radicle::cob::{patch, Label, Reaction};
-
use radicle::git::RefString;
+
use radicle::git::fmt::RefString;
use radicle::patch::cache::Patches as _;
use radicle::storage::git::transport;
use radicle::{prelude::*, Node};
modified crates/radicle-cli/src/commands/patch/checkout.rs
@@ -1,10 +1,10 @@
use anyhow::anyhow;

-
use git_ref_format::Qualified;
use radicle::cob::patch;
use radicle::cob::patch::RevisionId;
+
use radicle::git::fmt::Qualified;
+
use radicle::git::fmt::RefString;
use radicle::git::raw::ErrorExt as _;
-
use radicle::git::RefString;
use radicle::patch::cache::Patches as _;
use radicle::patch::PatchId;
use radicle::storage::git::Repository;
@@ -25,7 +25,7 @@ impl Options {
            Some(refname) => Ok(Qualified::from_refstr(refname)
                .map_or_else(|| refname.clone(), |q| q.to_ref_string())),
            // SAFETY: Patch IDs are valid refstrings.
-
            None => Ok(git::refname!("patch")
+
            None => Ok(git::fmt::refname!("patch")
                .join(RefString::try_from(term::format::cob(id).item).unwrap())),
        }
    }
@@ -57,34 +57,33 @@ pub fn run(
    let mut spinner = term::spinner("Performing checkout...");
    let patch_branch = opts.branch(patch_id)?;

-
    let commit =
-
        match working.find_branch(patch_branch.as_str(), radicle::git::raw::BranchType::Local) {
-
            Ok(branch) if opts.force => {
-
                let commit = find_patch_commit(revision, stored, working)?;
-
                let mut r = branch.into_reference();
-
                r.set_target(commit.id(), &format!("force update '{patch_branch}'"))?;
-
                commit
-
            }
-
            Ok(branch) => {
-
                let head = branch.get().peel_to_commit()?;
-
                if head.id() != *revision.head() {
-
                    anyhow::bail!(
-
                        "branch '{patch_branch}' already exists (use `--force` to overwrite)"
-
                    );
-
                }
-
                head
-
            }
-
            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)?;
-
                commit
+
    let commit = match working.find_branch(patch_branch.as_str(), git::raw::BranchType::Local) {
+
        Ok(branch) if opts.force => {
+
            let commit = find_patch_commit(revision, stored, working)?;
+
            let mut r = branch.into_reference();
+
            r.set_target(commit.id(), &format!("force update '{patch_branch}'"))?;
+
            commit
+
        }
+
        Ok(branch) => {
+
            let head = branch.get().peel_to_commit()?;
+
            if revision.head() != head.id() {
+
                anyhow::bail!(
+
                    "branch '{patch_branch}' already exists (use `--force` to overwrite)"
+
                );
            }
-
            Err(e) => return Err(e.into()),
-
        };
+
            head
+
        }
+
        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)?;
+
            commit
+
        }
+
        Err(e) => return Err(e.into()),
+
    };

    if opts.force {
-
        let mut builder = radicle::git::raw::build::CheckoutBuilder::new();
+
        let mut builder = git::raw::build::CheckoutBuilder::new();
        builder.force();
        working.checkout_tree(commit.as_object(), Some(&mut builder))?;
    } else {
@@ -125,7 +124,7 @@ fn find_patch_commit<'a>(
    stored: &Repository,
    working: &'a git::raw::Repository,
) -> anyhow::Result<git::raw::Commit<'a>> {
-
    let head = *revision.head();
+
    let head = revision.head().into();

    match working.find_commit(head) {
        Ok(commit) => Ok(commit),
modified crates/radicle-cli/src/commands/patch/review/builder.rs
@@ -23,10 +23,10 @@ use radicle::cob::patch::{PatchId, Revision, Verdict};
use radicle::cob::{CodeLocation, CodeRange};
use radicle::crypto;
use radicle::git;
+
use radicle::git::Oid;
use radicle::node::device::Device;
use radicle::prelude::*;
use radicle::storage::git::{cob::DraftStore, Repository};
-
use radicle_git_ext::Oid;
use radicle_surf::diff::*;
use radicle_term::{Element, VStack};

@@ -196,25 +196,28 @@ impl ReviewItem {

    fn paths(&self) -> (Option<(&Path, Oid)>, Option<(&Path, Oid)>) {
        match self {
-
            Self::FileAdded { path, new, .. } => (None, Some((path, new.oid))),
-
            Self::FileDeleted { path, old, .. } => (Some((path, old.oid)), None),
+
            Self::FileAdded { path, new, .. } => (None, Some((path, Oid::from(*new.oid)))),
+
            Self::FileDeleted { path, old, .. } => (Some((path, Oid::from(*old.oid))), None),
            Self::FileMoved { moved } => (
-
                Some((&moved.old_path, moved.old.oid)),
-
                Some((&moved.new_path, moved.new.oid)),
+
                Some((&moved.old_path, Oid::from(*moved.old.oid))),
+
                Some((&moved.new_path, Oid::from(*moved.new.oid))),
            ),
            Self::FileCopied { copied } => (
-
                Some((&copied.old_path, copied.old.oid)),
-
                Some((&copied.new_path, copied.new.oid)),
+
                Some((&copied.old_path, Oid::from(*copied.old.oid))),
+
                Some((&copied.new_path, Oid::from(*copied.new.oid))),
+
            ),
+
            Self::FileModified { path, old, new, .. } => (
+
                Some((path, Oid::from(*old.oid))),
+
                Some((path, Oid::from(*new.oid))),
+
            ),
+
            Self::FileEofChanged { path, old, new, .. } => (
+
                Some((path, Oid::from(*old.oid))),
+
                Some((path, Oid::from(*new.oid))),
+
            ),
+
            Self::FileModeChanged { path, old, new, .. } => (
+
                Some((path, Oid::from(*old.oid))),
+
                Some((path, Oid::from(*new.oid))),
            ),
-
            Self::FileModified { path, old, new, .. } => {
-
                (Some((path, old.oid)), Some((path, new.oid)))
-
            }
-
            Self::FileEofChanged { path, old, new, .. } => {
-
                (Some((path, old.oid)), Some((path, new.oid)))
-
            }
-
            Self::FileModeChanged { path, old, new, .. } => {
-
                (Some((path, old.oid)), Some((path, new.oid)))
-
            }
        }
    }

@@ -479,7 +482,7 @@ impl FileReviewBuilder {
/// of changes introduced by a patch.
pub struct Brain<'a> {
    /// Where the review draft is being stored.
-
    refname: git::Namespaced<'a>,
+
    refname: git::fmt::Namespaced<'a>,
    /// The commit pointed to by the ref.
    head: git::raw::Commit<'a>,
    /// The tree of accepted changes pointed to by the head commit.
@@ -565,7 +568,7 @@ impl<'a> Brain<'a> {
    }

    /// Get the brain's refname given the patch and remote.
-
    fn refname(patch: &PatchId, remote: &NodeId) -> git::Namespaced<'a> {
+
    fn refname(patch: &PatchId, remote: &NodeId) -> git::fmt::Namespaced<'a> {
        git::refs::storage::draft::review(remote, patch)
    }
}
modified crates/radicle-cli/src/commands/patch/update.rs
@@ -1,5 +1,6 @@
use radicle::cob::patch;
use radicle::git;
+
use radicle::git::Oid;
use radicle::prelude::*;
use radicle::storage::git::Repository;

@@ -9,7 +10,7 @@ use crate::terminal::patch::*;
/// Run patch update.
pub fn run(
    patch_id: patch::PatchId,
-
    base_id: Option<git::raw::Oid>,
+
    base_id: Option<Oid>,
    message: term::patch::Message,
    profile: &Profile,
    repository: &Repository,
@@ -27,22 +28,25 @@ pub fn run(
    let head_oid = branch_oid(&head_branch)?;
    let base_oid = match base_id {
        Some(oid) => oid,
-
        None => repository.backend.merge_base(*target_oid, *head_oid)?,
+
        None => repository
+
            .backend
+
            .merge_base(target_oid.into(), head_oid.into())?
+
            .into(),
    };

    // N.b. we don't update if both the head and base are the same as
    // any previous revision
    if patch
        .revisions()
-
        .any(|(_, revision)| revision.head() == head_oid && **revision.base() == base_oid)
+
        .any(|(_, revision)| revision.head() == head_oid && *revision.base() == base_oid)
    {
        return Ok(());
    }

    let (_, revision) = patch.latest();
-
    let message = term::patch::get_update_message(message, workdir, revision, &head_oid)?;
+
    let message = term::patch::get_update_message(message, workdir, revision, &head_oid.into())?;
    let signer = term::signer(profile)?;
-
    let revision = patch.update(message, base_oid, *head_oid, &signer)?;
+
    let revision = patch.update(message, base_oid, head_oid, &signer)?;

    term::print(revision);

modified crates/radicle-cli/src/commands/remote.rs
@@ -8,7 +8,7 @@ use std::ffi::OsString;

use anyhow::anyhow;

-
use radicle::git::RefString;
+
use radicle::git::fmt::RefString;
use radicle::prelude::NodeId;
use radicle::storage::ReadStorage;

modified crates/radicle-cli/src/commands/remote/add.rs
@@ -1,7 +1,7 @@
use std::str::FromStr;

use radicle::git;
-
use radicle::git::RefString;
+
use radicle::git::fmt::RefString;
use radicle::prelude::*;
use radicle::Profile;
use radicle_crypto::PublicKey;
modified crates/radicle-cli/src/commands/stats.rs
@@ -71,7 +71,7 @@ pub fn run(_args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
            let remote = remote?;
            let sigrefs = repo.reference_oid(&remote, &git::refs::storage::SIGREFS_BRANCH)?;
            let mut walk = repo.raw().revwalk()?;
-
            walk.push(*sigrefs)?;
+
            walk.push(sigrefs.into())?;

            stats.local.pushes += walk.count();
            stats.local.forks += 1;
modified crates/radicle-cli/src/commands/watch.rs
@@ -41,7 +41,7 @@ Options

pub struct Options {
    rid: Option<RepoId>,
-
    refstr: git::RefString,
+
    refstr: git::fmt::RefString,
    target: Option<git::Oid>,
    nid: Option<NodeId>,
    interval: time::Duration,
@@ -56,7 +56,7 @@ impl Args for Options {
        let mut rid = None;
        let mut nid: Option<NodeId> = None;
        let mut target: Option<git::Oid> = None;
-
        let mut refstr: Option<git::RefString> = None;
+
        let mut refstr: Option<git::fmt::RefString> = None;
        let mut interval: Option<time::Duration> = None;
        let mut timeout: time::Duration = time::Duration::MAX;

@@ -166,7 +166,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
fn reference<R: ReadRepository>(
    repo: &R,
    nid: &NodeId,
-
    qual: &git::Qualified,
+
    qual: &git::fmt::Qualified,
) -> Result<Option<git::Oid>, git::raw::Error> {
    match repo.reference_oid(nid, qual) {
        Ok(oid) => Ok(Some(oid)),
modified crates/radicle-cli/src/git.rs
@@ -20,14 +20,15 @@ use thiserror::Error;

use radicle::crypto::ssh;
use radicle::git;
-
use radicle::git::raw as git2;
use radicle::git::{Version, VERSION_REQUIRED};
use radicle::prelude::{NodeId, RepoId};
use radicle::storage::git::transport;

+
pub use radicle::git::Oid;
+

pub use radicle::git::raw::{
-
    build::CheckoutBuilder, AnnotatedCommit, Commit, Direction, ErrorCode, MergeAnalysis,
-
    MergeOptions, Oid, Reference, Repository, Signature,
+
    build::CheckoutBuilder, AnnotatedCommit, Commit, Direction, ErrorCode, ErrorExt as _,
+
    MergeAnalysis, MergeOptions, Reference, Repository, Signature,
};

pub const CONFIG_COMMIT_GPG_SIGN: &str = "commit.gpgsign";
@@ -46,10 +47,10 @@ impl Rev {
        &self.0
    }

-
    /// Resolve the revision to an [`From<git2::Oid>`].
-
    pub fn resolve<T>(&self, repo: &git2::Repository) -> Result<T, git2::Error>
+
    /// Resolve the revision to an [`From<git::raw::Oid>`].
+
    pub fn resolve<T>(&self, repo: &Repository) -> Result<T, git::raw::Error>
    where
-
        T: From<git2::Oid>,
+
        T: From<git::raw::Oid>,
    {
        let object = repo.revparse_single(self.as_str())?;
        Ok(object.id().into())
@@ -84,13 +85,13 @@ pub struct Remote<'a> {
    pub url: radicle::git::Url,
    pub pushurl: Option<radicle::git::Url>,

-
    inner: git2::Remote<'a>,
+
    inner: git::raw::Remote<'a>,
}

-
impl<'a> TryFrom<git2::Remote<'a>> for Remote<'a> {
+
impl<'a> TryFrom<git::raw::Remote<'a>> for Remote<'a> {
    type Error = RemoteError;

-
    fn try_from(value: git2::Remote<'a>) -> Result<Self, Self::Error> {
+
    fn try_from(value: git::raw::Remote<'a>) -> Result<Self, Self::Error> {
        let url = value.url().map_or(Err(RemoteError::MissingUrl), |url| {
            Ok(radicle::git::Url::from_str(url)?)
        })?;
@@ -110,7 +111,7 @@ impl<'a> TryFrom<git2::Remote<'a>> for Remote<'a> {
}

impl<'a> Deref for Remote<'a> {
-
    type Target = git2::Remote<'a>;
+
    type Target = git::raw::Remote<'a>;

    fn deref(&self) -> &Self::Target {
        &self.inner
@@ -250,7 +251,7 @@ pub fn is_signing_configured(repo: &Path) -> Result<bool, anyhow::Error> {
}

/// Return the list of radicle remotes for the given repository.
-
pub fn rad_remotes(repo: &git2::Repository) -> anyhow::Result<Vec<Remote>> {
+
pub fn rad_remotes(repo: &Repository) -> anyhow::Result<Vec<Remote>> {
    let remotes: Vec<_> = repo
        .remotes()?
        .iter()
@@ -263,16 +264,16 @@ pub fn rad_remotes(repo: &git2::Repository) -> anyhow::Result<Vec<Remote>> {
}

/// Check if the git remote is configured for the `Repository`.
-
pub fn is_remote(repo: &git2::Repository, alias: &str) -> anyhow::Result<bool> {
+
pub fn is_remote(repo: &Repository, alias: &str) -> anyhow::Result<bool> {
    match repo.find_remote(alias) {
        Ok(_) => Ok(true),
-
        Err(err) if err.code() == git2::ErrorCode::NotFound => Ok(false),
+
        Err(err) if err.is_not_found() => Ok(false),
        Err(err) => Err(err.into()),
    }
}

/// Get the repository's "rad" remote.
-
pub fn rad_remote(repo: &Repository) -> anyhow::Result<(git2::Remote, RepoId)> {
+
pub fn rad_remote(repo: &Repository) -> anyhow::Result<(git::raw::Remote, RepoId)> {
    match radicle::rad::remote(repo) {
        Ok((remote, id)) => Ok((remote, id)),
        Err(radicle::rad::RemoteError::NotFound(_)) => Err(anyhow!(
@@ -352,12 +353,12 @@ pub fn parse_remote(refspec: &str) -> Option<(NodeId, &str)> {
}

pub fn add_tag(
-
    repo: &git2::Repository,
+
    repo: &Repository,
    message: &str,
    patch_tag_name: &str,
-
) -> anyhow::Result<git2::Oid> {
+
) -> anyhow::Result<git::raw::Oid> {
    let head = repo.head()?;
-
    let commit = head.peel(git2::ObjectType::Commit).unwrap();
+
    let commit = head.peel(git::raw::ObjectType::Commit).unwrap();
    let oid = repo.tag(patch_tag_name, &commit, &repo.signature()?, message, false)?;

    Ok(oid)
modified crates/radicle-cli/src/git/pretty_diff.rs
@@ -2,7 +2,7 @@ use std::fs;
use std::path::{Path, PathBuf};

use radicle::git;
-
use radicle_git_ext::Oid;
+
use radicle::git::Oid;
use radicle_surf::diff;
use radicle_surf::diff::{Added, Copied, Deleted, FileStats, Hunks, Modified, Moved};
use radicle_surf::diff::{Diff, DiffContent, FileDiff, Hunk, Modification};
@@ -33,7 +33,7 @@ pub trait Repo {

impl Repo for git::raw::Repository {
    fn blob(&self, oid: git::Oid) -> Result<Blob, git::raw::Error> {
-
        let blob = self.find_blob(*oid)?;
+
        let blob = self.find_blob(oid.into())?;

        if blob.is_binary() {
            Ok(Blob::Binary)
@@ -338,7 +338,7 @@ impl ToPretty for Added {
        repo: &R,
    ) -> Self::Output {
        let old = None;
-
        let new = Some((self.path.as_path(), self.new.oid));
+
        let new = Some((self.path.as_path(), Oid::from(*self.new.oid)));

        pretty_modification(header, &self.diff, old, new, repo, hi)
    }
@@ -354,7 +354,7 @@ impl ToPretty for Deleted {
        header: &Self::Context,
        repo: &R,
    ) -> Self::Output {
-
        let old = Some((self.path.as_path(), self.old.oid));
+
        let old = Some((self.path.as_path(), Oid::from(*self.old.oid)));
        let new = None;

        pretty_modification(header, &self.diff, old, new, repo, hi)
@@ -371,8 +371,8 @@ impl ToPretty for Modified {
        header: &Self::Context,
        repo: &R,
    ) -> Self::Output {
-
        let old = Some((self.path.as_path(), self.old.oid));
-
        let new = Some((self.path.as_path(), self.new.oid));
+
        let old = Some((self.path.as_path(), Oid::from(*self.old.oid)));
+
        let new = Some((self.path.as_path(), Oid::from(*self.new.oid)));

        pretty_modification(header, &self.diff, old, new, repo, hi)
    }
@@ -595,8 +595,8 @@ mod test {
    use term::Element;

    use super::*;
-
    use radicle::git::raw::RepositoryOpenFlags;
-
    use radicle::git::raw::{Oid, Repository};
+
    use git::raw::RepositoryOpenFlags;
+
    use git::raw::{Oid, Repository};

    #[test]
    #[ignore]
modified crates/radicle-cli/src/git/unified_diff.rs
@@ -7,7 +7,6 @@ use radicle_surf::diff::FileStats;
use thiserror::Error;

use radicle::git;
-
use radicle::git::raw::Oid;
use radicle_surf::diff;
use radicle_surf::diff::{Diff, DiffContent, DiffFile, FileDiff, Hunk, Hunks, Line, Modification};

@@ -307,8 +306,8 @@ impl Encode for FileHeader {
                if old.mode == new.mode {
                    w.meta(format!(
                        "index {}..{} {:o}",
-
                        term::format::oid(old.oid),
-
                        term::format::oid(new.oid),
+
                        term::format::oid(*old.oid),
+
                        term::format::oid(*new.oid),
                        u32::from(old.mode.clone()),
                    ))?;
                } else {
@@ -316,8 +315,8 @@ impl Encode for FileHeader {
                    w.meta(format!("new mode {:o}", u32::from(new.mode.clone())))?;
                    w.meta(format!(
                        "index {}..{}",
-
                        term::format::oid(old.oid),
-
                        term::format::oid(new.oid)
+
                        term::format::oid(*old.oid),
+
                        term::format::oid(*new.oid)
                    ))?;
                }

@@ -334,8 +333,8 @@ impl Encode for FileHeader {
                w.meta(format!("new file mode {:o}", u32::from(new.mode.clone())))?;
                w.meta(format!(
                    "index {}..{}",
-
                    term::format::oid(Oid::zero()),
-
                    term::format::oid(new.oid),
+
                    term::format::oid(git::Oid::sha1_zero()),
+
                    term::format::oid(*new.oid),
                ))?;

                w.meta("--- /dev/null")?;
@@ -355,8 +354,8 @@ impl Encode for FileHeader {
                ))?;
                w.meta(format!(
                    "index {}..{}",
-
                    term::format::oid(old.oid),
-
                    term::format::oid(Oid::zero())
+
                    term::format::oid(*old.oid),
+
                    term::format::oid(git::Oid::sha1_zero())
                ))?;

                w.meta(format!("--- a/{}", path.display()))?;
modified crates/radicle-cli/src/project.rs
@@ -1,7 +1,7 @@
use radicle::prelude::*;

use crate::git;
-
use radicle::git::RefStr;
+
use radicle::git::fmt::RefStr;
use radicle::node::NodeId;

/// Setup a repository remote and tracking branch.
modified crates/radicle-cli/src/terminal/args.rs
@@ -7,7 +7,7 @@ use anyhow::anyhow;

use radicle::cob::{self, issue, patch};
use radicle::crypto;
-
use radicle::git::{Oid, RefString};
+
use radicle::git::{fmt::RefString, Oid};
use radicle::node::{Address, Alias};
use radicle::prelude::{Did, NodeId, RepoId};

modified crates/radicle-cli/src/terminal/patch.rs
@@ -188,8 +188,8 @@ fn message_from_commits(name: &str, commits: Vec<git::raw::Commit>) -> Result<St
/// Return commits between the merge base and a head.
pub fn patch_commits<'a>(
    repo: &'a git::raw::Repository,
-
    base: &git::Oid,
-
    head: &git::Oid,
+
    base: &git::raw::Oid,
+
    head: &git::raw::Oid,
) -> Result<Vec<git::raw::Commit<'a>>, git::raw::Error> {
    let mut commits = Vec::new();
    let mut revwalk = repo.revwalk()?;
@@ -205,8 +205,8 @@ pub fn patch_commits<'a>(
/// The message shown in the editor when creating a `Patch`.
fn create_display_message(
    repo: &git::raw::Repository,
-
    base: &git::Oid,
-
    head: &git::Oid,
+
    base: &git::raw::Oid,
+
    head: &git::raw::Oid,
) -> Result<String, Error> {
    let commits = patch_commits(repo, base, head)?;
    if commits.is_empty() {
@@ -226,8 +226,8 @@ fn create_display_message(
pub fn get_create_message(
    message: term::patch::Message,
    repo: &git::raw::Repository,
-
    base: &git::Oid,
-
    head: &git::Oid,
+
    base: &git::raw::Oid,
+
    head: &git::raw::Oid,
) -> Result<(Title, String), Error> {
    let display_msg = create_display_message(repo, base, head)?;
    let message = message.get(&display_msg)?;
@@ -279,10 +279,10 @@ pub fn get_edit_message(
/// The message shown in the editor when updating a `Patch`.
fn update_display_message(
    repo: &git::raw::Repository,
-
    last_rev_head: &git::Oid,
-
    head: &git::Oid,
+
    last_rev_head: &git::raw::Oid,
+
    head: &git::raw::Oid,
) -> Result<String, Error> {
-
    if !repo.graph_descendant_of(**head, **last_rev_head)? {
+
    if !repo.graph_descendant_of(*head, *last_rev_head)? {
        return Ok(REVISION_MSG.trim_start().to_string());
    }

@@ -302,9 +302,9 @@ pub fn get_update_message(
    message: term::patch::Message,
    repo: &git::raw::Repository,
    latest: &patch::Revision,
-
    head: &git::Oid,
+
    head: &git::raw::Oid,
) -> Result<String, Error> {
-
    let display_msg = update_display_message(repo, &latest.head(), head)?;
+
    let display_msg = update_display_message(repo, &latest.head().into(), head)?;
    let message = message.get(&display_msg)?;
    let message = message.trim();

@@ -366,11 +366,8 @@ pub fn show(
    } else {
        vec![]
    };
-
    let ahead_behind = common::ahead_behind(
-
        stored.raw(),
-
        *revision.head(),
-
        *patch.target().head(stored)?,
-
    )?;
+
    let ahead_behind =
+
        common::ahead_behind(stored.raw(), revision.head(), patch.target().head(stored)?)?;
    let author = patch.author();
    let author = term::format::Author::new(author.id(), profile, verbose);
    let labels = patch.labels().map(|l| l.to_string()).collect::<Vec<_>>();
@@ -468,7 +465,7 @@ fn patch_commit_lines(
    let (from, to) = patch.range()?;
    let mut lines = Vec::new();

-
    for commit in patch_commits(stored.raw(), &from, &to)? {
+
    for commit in patch_commits(stored.raw(), &from.into(), &to.into())? {
        lines.push(term::Line::spaced([
            term::label(term::format::secondary::<String>(
                term::format::oid(commit.id()).into(),
@@ -484,37 +481,36 @@ fn patch_commit_lines(
#[cfg(test)]
mod test {
    use super::*;
-
    use radicle::git::refname;
+
    use radicle::git::fmt::refname;
    use radicle::test::fixtures;
    use std::path;

    fn commit(
        repo: &git::raw::Repository,
-
        branch: &git::RefStr,
-
        parent: &git::Oid,
+
        branch: &git::fmt::RefStr,
+
        parent: &git::raw::Oid,
        msg: &str,
-
    ) -> git::Oid {
+
    ) -> git::raw::Oid {
        let sig = git::raw::Signature::new(
            "anonymous",
            "anonymous@radicle.example.com",
            &git::raw::Time::new(0, 0),
        )
        .unwrap();
-
        let head = repo.find_commit(**parent).unwrap();
+
        let head = repo.find_commit(*parent).unwrap();
        let tree =
            git::write_tree(path::Path::new("README"), "Hello World!\n".as_bytes(), repo).unwrap();

        let branch = git::refs::branch(branch);
        let commit = git::commit(repo, &head, &branch, msg, &sig, &tree).unwrap();

-
        commit.id().into()
+
        commit.id()
    }

    #[test]
    fn test_create_display_message() {
        let tmpdir = tempfile::tempdir().unwrap();
        let (repo, commit_0) = fixtures::repository(&tmpdir);
-
        let commit_0 = commit_0.into();
        let commit_1 = commit(
            &repo,
            &refname!("feature"),
@@ -625,7 +621,6 @@ mod test {
    fn test_update_display_message() {
        let tmpdir = tempfile::tempdir().unwrap();
        let (repo, commit_0) = fixtures::repository(&tmpdir);
-
        let commit_0 = commit_0.into();

        let commit_1 = commit(&repo, &refname!("feature"), &commit_0, "commit 1\n");
        let commit_2 = commit(&repo, &refname!("feature"), &commit_1, "commit 2\n");
modified crates/radicle-cli/src/terminal/patch/common.rs
@@ -1,7 +1,7 @@
use anyhow::anyhow;

use radicle::git;
-
use radicle::git::raw::Oid;
+
use radicle::git::Oid;
use radicle::prelude::*;
use radicle::storage::git::Repository;

@@ -9,7 +9,7 @@ use crate::terminal as term;

/// Give the oid of the branch or an appropriate error.
#[inline]
-
pub fn branch_oid(branch: &git::raw::Branch) -> anyhow::Result<git::Oid> {
+
pub fn branch_oid(branch: &git::raw::Branch) -> anyhow::Result<Oid> {
    let oid = branch
        .get()
        .target()
@@ -18,7 +18,7 @@ pub fn branch_oid(branch: &git::raw::Branch) -> anyhow::Result<git::Oid> {
}

#[inline]
-
fn get_branch(git_ref: git::Qualified) -> git::RefString {
+
fn get_branch(git_ref: git::fmt::Qualified) -> git::fmt::RefString {
    let (_, _, head, tail) = git_ref.non_empty_components();
    std::iter::once(head).chain(tail).collect()
}
@@ -28,16 +28,18 @@ fn get_branch(git_ref: git::Qualified) -> git::RefString {
pub fn get_merge_target(
    storage: &Repository,
    head_branch: &git::raw::Branch,
-
) -> anyhow::Result<(git::RefString, git::Oid)> {
+
) -> anyhow::Result<(git::fmt::RefString, git::Oid)> {
    let (qualified_ref, target_oid) = storage.canonical_head()?;
    let head_oid = branch_oid(head_branch)?;
-
    let merge_base = storage.raw().merge_base(*head_oid, *target_oid)?;
+
    let merge_base = storage
+
        .raw()
+
        .merge_base(head_oid.into(), target_oid.into())?;

-
    if head_oid == merge_base.into() {
+
    if head_oid == merge_base {
        anyhow::bail!("commits are already included in the target branch; nothing to do");
    }

-
    Ok((get_branch(qualified_ref), (*target_oid).into()))
+
    Ok((get_branch(qualified_ref), (target_oid)))
}

/// Get the diff stats between two commits.
@@ -47,8 +49,8 @@ pub fn diff_stats(
    old: &Oid,
    new: &Oid,
) -> Result<git::raw::DiffStats, git::raw::Error> {
-
    let old = repo.find_commit(*old)?;
-
    let new = repo.find_commit(*new)?;
+
    let old = repo.find_commit(old.into())?;
+
    let new = repo.find_commit(new.into())?;
    let old_tree = old.tree()?;
    let new_tree = new.tree()?;
    let mut diff = repo.diff_tree_to_tree(Some(&old_tree), Some(&new_tree), None)?;
@@ -64,7 +66,7 @@ pub fn ahead_behind(
    revision_oid: Oid,
    head_oid: Oid,
) -> anyhow::Result<term::Line> {
-
    let (a, b) = repo.graph_ahead_behind(revision_oid, head_oid)?;
+
    let (a, b) = repo.graph_ahead_behind(revision_oid.into(), head_oid.into())?;
    if a == 0 && b == 0 {
        return Ok(term::Line::new(term::format::dim("up to date")));
    }
@@ -88,7 +90,7 @@ pub fn branches(target: &Oid, repo: &git::raw::Repository) -> anyhow::Result<Vec
            continue;
        }
        if let (Some(oid), Some(name)) = (&r.target(), &r.shorthand()) {
-
            if oid == target {
+
            if target == oid {
                branches.push(name.to_string());
            };
        };
modified crates/radicle-cli/tests/util/environment.rs
@@ -5,6 +5,7 @@ use localtime::LocalTime;
use radicle::cob::cache::COBS_DB_FILE;
use radicle::crypto::ssh::{keystore::MemorySigner, Keystore};
use radicle::crypto::{KeyPair, Seed};
+
use radicle::git;
use radicle::node::policy::store as policy;
use radicle::node::{self, UserAgent};
use radicle::node::{Alias, Config, POLICIES_DB_FILE};
@@ -233,7 +234,7 @@ impl Environment {
    pub fn repository(
        &self,
        has_alias: &impl HasAlias,
-
    ) -> (radicle_cli::git::Repository, radicle_cli::git::Oid) {
+
    ) -> (radicle_cli::git::Repository, git::raw::Oid) {
        radicle::test::fixtures::repository(self.work(has_alias).as_path())
    }

modified crates/radicle-cob/Cargo.toml
@@ -17,15 +17,18 @@ rust-version.workspace = true
default = []
# Only used for testing. Ensures that commit ids are stable.
stable-commit-ids = []
+
test = []

[dependencies]
fastrand = { workspace = true }
-
git2 = { workspace = true, features = ["vendored-libgit2"] }
+
git-ref-format-core = { workspace = true }
+
git2 = { workspace = true, optional = true }
log = { workspace = true }
nonempty = { workspace = true, features = ["serialize"] }
radicle-crypto = { workspace = true, features = ["ssh"] }
radicle-dag = { workspace = true }
-
radicle-git-ext = { workspace = true, features = ["serde"] }
+
radicle-git-metadata = { workspace = true }
+
radicle-oid = { workspace = true, features = ["git2", "serde", "std"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
signature = { workspace = true }
@@ -36,4 +39,6 @@ fastrand = { workspace = true }
qcheck = { workspace = true }
qcheck-macros = { workspace = true }
radicle-crypto = { workspace = true, features = ["test", "git-ref-format-core"] }
+
radicle-git-ref-format = { workspace = true, features = ["macro"] }
+
radicle-oid = { workspace = true, features = ["qcheck"] }
tempfile = { workspace = true }
modified crates/radicle-cob/src/backend.rs
@@ -1,3 +1,7 @@
// Copyright © 2022 The Radicle Link Contributors

+
#[cfg(feature = "git2")]
pub mod git;
+

+
#[cfg(feature = "stable-commit-ids")]
+
pub mod stable;
modified crates/radicle-cob/src/backend/git.rs
@@ -1,9 +1,8 @@
// Copyright © 2022 The Radicle Team

-
pub mod change;
+
mod commit;

-
#[cfg(feature = "stable-commit-ids")]
-
pub mod stable;
+
pub mod change;

/// Environment variable to set to overwrite the commit date for both the author and the committer.
///
modified crates/radicle-cob/src/backend/git/change.rs
@@ -5,11 +5,11 @@ use std::convert::TryFrom;
use std::path::PathBuf;
use std::sync::LazyLock;

-
use git_ext::author::Author;
-
use git_ext::commit::{headers::Headers, Commit};
-
use git_ext::Oid;
+
use metadata::author::Author;
+
use metadata::commit::headers::Headers;
+
use metadata::commit::trailers::OwnedTrailer;
use nonempty::NonEmpty;
-
use radicle_git_ext::commit::trailers::OwnedTrailer;
+
use oid::Oid;

use crate::change::store::Version;
use crate::signatures;
@@ -21,6 +21,8 @@ use crate::{
    trailers, Embed,
};

+
use super::commit::Commit;
+

/// Name of the COB manifest file.
pub const MANIFEST_BLOB_NAME: &str = "manifest";
/// Path under which COB embeds are kept.
@@ -30,8 +32,7 @@ pub mod error {
    use std::str::Utf8Error;
    use std::string::FromUtf8Error;

-
    use git_ext::commit;
-
    use git_ext::Oid;
+
    use oid::Oid;
    use thiserror::Error;

    use crate::signatures::error::Signatures;
@@ -39,7 +40,7 @@ pub mod error {
    #[derive(Debug, Error)]
    pub enum Create {
        #[error(transparent)]
-
        WriteCommit(#[from] commit::error::Write),
+
        WriteCommit(#[from] super::super::commit::error::Write),
        #[error(transparent)]
        FromUtf8(#[from] FromUtf8Error),
        #[error(transparent)]
@@ -55,7 +56,7 @@ pub mod error {
    #[derive(Debug, Error)]
    pub enum Load {
        #[error(transparent)]
-
        Read(#[from] commit::error::Read),
+
        Read(#[from] super::super::commit::error::Read),
        #[error(transparent)]
        Signatures(#[from] Signatures),
        #[error(transparent)]
@@ -123,7 +124,7 @@ impl change::Storage for git2::Repository {

        let (id, timestamp) = write_commit(
            self,
-
            resource.map(|o| *o),
+
            resource.map(|o| o.into()),
            // Commit to tips, extra parents and resource.
            tips.iter()
                .cloned()
@@ -134,7 +135,7 @@ impl change::Storage for git2::Repository {
            signature.clone(),
            related
                .iter()
-
                .map(|p| trailers::CommitTrailer::Related(**p).into()),
+
                .map(|p| trailers::CommitTrailer::Related(*p).into()),
            tree,
        )?;

@@ -153,40 +154,34 @@ impl change::Storage for git2::Repository {

    fn parents_of(&self, id: &Oid) -> Result<Vec<Oid>, Self::LoadError> {
        Ok(self
-
            .find_commit(**id)?
+
            .find_commit(id.into())?
            .parent_ids()
            .map(Oid::from)
            .collect::<Vec<_>>())
    }

    fn manifest_of(&self, id: &Oid) -> Result<crate::Manifest, Self::LoadError> {
-
        let commit = self.find_commit(**id)?;
+
        let commit = self.find_commit(id.into())?;
        let tree = commit.tree()?;
        load_manifest(self, &tree)
    }

    fn load(&self, id: Self::ObjectId) -> Result<Entry, Self::LoadError> {
-
        let commit = Commit::read(self, id.into())?;
-
        let timestamp = git2::Time::from(commit.committer().time).seconds() as u64;
+
        let commit = super::commit::Commit::read(self, id.into())?;
+
        let timestamp = commit.committer().time.seconds() as u64;
        let trailers = parse_trailers(commit.trailers())?;
        let (resources, related): (Vec<_>, Vec<_>) = trailers.iter().partition(|t| match t {
            CommitTrailer::Resource(_) => true,
            CommitTrailer::Related(_) => false,
        });
-
        let mut resources = resources
-
            .into_iter()
-
            .map(|r| r.oid().into())
-
            .collect::<Vec<_>>();
-
        let related = related
-
            .into_iter()
-
            .map(|r| r.oid().into())
-
            .collect::<Vec<_>>();
+
        let mut resources = resources.into_iter().map(|r| r.oid()).collect::<Vec<_>>();
+
        let related = related.into_iter().map(|r| r.oid()).collect::<Vec<_>>();
        let parents = commit
            .parents()
            .map(Oid::from)
            .filter(|p| !resources.contains(p) && !related.contains(p))
            .collect();
-
        let mut signatures = Signatures::try_from(&commit)?
+
        let mut signatures = Signatures::try_from(&*commit)?
            .into_iter()
            .collect::<Vec<_>>();
        let Some((key, sig)) = signatures.pop() else {
@@ -285,7 +280,7 @@ fn write_commit(
) -> Result<(Oid, Timestamp), error::Create> {
    let trailers: Vec<OwnedTrailer> = trailers
        .into_iter()
-
        .chain(resource.map(|r| trailers::CommitTrailer::Resource(r).into()))
+
        .chain(resource.map(|r| trailers::CommitTrailer::Resource(r.into()).into()))
        .collect();
    let author = repo.signature()?;
    #[allow(unused_variables)]
@@ -299,29 +294,37 @@ fn write_commit(
            .map_err(signatures::error::Signatures::from)?
            .as_str(),
    );
-
    let author = Author::try_from(&author)?;
+

+
    let author = Author {
+
        name: String::from_utf8(author.name_bytes().to_vec())?,
+
        email: String::from_utf8(author.email_bytes().to_vec())?,
+
        time: {
+
            let when = author.when();
+
            metadata::author::Time::new(when.seconds(), when.offset_minutes())
+
        },
+
    };

    #[cfg(feature = "stable-commit-ids")]
    // Ensures the commit id doesn't change on every run.
    let (author, timestamp) = {
-
        let stable = crate::git::stable::read_timestamp();
+
        let stable = crate::stable::read_timestamp();
        (
            Author {
-
                time: git_ext::author::Time::new(stable, 0),
+
                time: metadata::author::Time::new(stable, 0),
                ..author
            },
            stable,
        )
    };
-
    let (author, timestamp) = if let Ok(s) = std::env::var(crate::git::GIT_COMMITTER_DATE) {
+
    let (author, timestamp) = if let Ok(s) = std::env::var(super::GIT_COMMITTER_DATE) {
        let Ok(timestamp) = s.trim().parse::<i64>() else {
            panic!(
                "Invalid timestamp value {s:?} for `{}`",
-
                crate::git::GIT_COMMITTER_DATE
+
                super::GIT_COMMITTER_DATE
            );
        };
        let author = Author {
-
            time: git_ext::author::Time::new(timestamp, 0),
+
            time: metadata::author::Time::new(timestamp, 0),
            ..author
        };
        (author, timestamp)
@@ -380,7 +383,7 @@ fn write_manifest(
            let oid = embed.content;
            let path = PathBuf::from(embed.name);

-
            embeds_tree.insert(path, *oid, git2::FileMode::Blob.into())?;
+
            embeds_tree.insert(path, oid.into(), git2::FileMode::Blob.into())?;
        }
        let oid = embeds_tree.write()?;

added crates/radicle-cob/src/backend/git/commit.rs
@@ -0,0 +1,180 @@
+
mod trailers;
+

+
use std::fmt;
+
use std::str::{self, FromStr};
+

+
use git2::{ObjectType, Oid};
+

+
use metadata::author::Author;
+
use metadata::commit::headers::Headers;
+
use metadata::commit::trailers::OwnedTrailer;
+
use metadata::commit::CommitData;
+

+
use trailers::Trailers;
+

+
#[repr(transparent)]
+
pub(super) struct Commit(metadata::commit::CommitData<Oid, Oid>);
+

+
impl Commit {
+
    pub fn new<P, I, T>(
+
        tree: git2::Oid,
+
        parents: P,
+
        author: Author,
+
        committer: Author,
+
        headers: Headers,
+
        message: String,
+
        trailers: I,
+
    ) -> Self
+
    where
+
        P: IntoIterator<Item = Oid>,
+
        I: IntoIterator<Item = T>,
+
        OwnedTrailer: From<T>,
+
    {
+
        Self(CommitData::new(
+
            tree, parents, author, committer, headers, message, trailers,
+
        ))
+
    }
+
}
+

+
impl Commit {
+
    /// Read the [`Commit`] from the `repo` that is expected to be found at
+
    /// `oid`.
+
    pub fn read(repo: &git2::Repository, oid: Oid) -> Result<Self, error::Read> {
+
        let odb = repo.odb()?;
+
        let object = odb.read(oid)?;
+
        Ok(Commit::try_from(object.data())?)
+
    }
+

+
    /// Write the given [`Commit`] to the `repo`. The resulting `Oid`
+
    /// is the identifier for this commit.
+
    pub fn write(&self, repo: &git2::Repository) -> Result<Oid, error::Write> {
+
        let odb = repo.odb().map_err(error::Write::Odb)?;
+
        self.verify_for_write(&odb)?;
+
        Ok(odb.write(ObjectType::Commit, self.to_string().as_bytes())?)
+
    }
+

+
    fn verify_for_write(&self, odb: &git2::Odb) -> Result<(), error::Write> {
+
        for parent in self.0.parents() {
+
            verify_object(odb, &parent, ObjectType::Commit)?;
+
        }
+
        verify_object(odb, self.0.tree(), ObjectType::Tree)?;
+

+
        Ok(())
+
    }
+
}
+

+
fn verify_object(odb: &git2::Odb, oid: &Oid, expected: ObjectType) -> Result<(), error::Write> {
+
    use git2::{Error, ErrorClass, ErrorCode};
+

+
    let (_, kind) = odb
+
        .read_header(*oid)
+
        .map_err(|err| error::Write::OdbRead { oid: *oid, err })?;
+
    if kind != expected {
+
        Err(error::Write::NotCommit {
+
            oid: *oid,
+
            err: Error::new(
+
                ErrorCode::NotFound,
+
                ErrorClass::Object,
+
                format!("Object '{oid}' is not expected object type {expected}"),
+
            ),
+
        })
+
    } else {
+
        Ok(())
+
    }
+
}
+

+
pub mod error {
+
    use std::str;
+

+
    use thiserror::Error;
+

+
    #[derive(Debug, Error)]
+
    pub enum Write {
+
        #[error(transparent)]
+
        Git(#[from] git2::Error),
+
        #[error("the parent '{oid}' provided is not a commit object")]
+
        NotCommit {
+
            oid: git2::Oid,
+
            #[source]
+
            err: git2::Error,
+
        },
+
        #[error("failed to access git odb")]
+
        Odb(#[source] git2::Error),
+
        #[error("failed to read '{oid}' from git odb")]
+
        OdbRead {
+
            oid: git2::Oid,
+
            #[source]
+
            err: git2::Error,
+
        },
+
    }
+

+
    #[derive(Debug, Error)]
+
    pub enum Read {
+
        #[error(transparent)]
+
        Git(#[from] git2::Error),
+
        #[error(transparent)]
+
        Parse(#[from] Parse),
+
    }
+

+
    #[derive(Debug, Error)]
+
    pub enum Parse {
+
        #[error(transparent)]
+
        Git(#[from] git2::Error),
+
        #[error(transparent)]
+
        Header(#[from] metadata::commit::headers::ParseError),
+
        #[error(transparent)]
+
        Utf8(#[from] str::Utf8Error),
+
    }
+
}
+

+
impl TryFrom<&[u8]> for Commit {
+
    type Error = error::Parse;
+

+
    fn try_from(data: &[u8]) -> Result<Self, Self::Error> {
+
        Commit::from_str(str::from_utf8(data)?)
+
    }
+
}
+

+
impl FromStr for Commit {
+
    type Err = error::Parse;
+

+
    fn from_str(buffer: &str) -> Result<Self, Self::Err> {
+
        let (header, message) = buffer
+
            .split_once("\n\n")
+
            .ok_or(metadata::commit::headers::ParseError::InvalidFormat)?;
+

+
        let (tree, parents, author, committer, headers) =
+
            metadata::commit::headers::parse_commit_header(header)?;
+

+
        let trailers = Trailers::parse(message)?;
+

+
        let message = message
+
            .strip_suffix(&trailers.to_string(": "))
+
            .unwrap_or(message)
+
            .to_string();
+

+
        Ok(Self(CommitData::new(
+
            tree,
+
            parents,
+
            author,
+
            committer,
+
            headers,
+
            message,
+
            trailers.iter(),
+
        )))
+
    }
+
}
+

+
impl fmt::Display for Commit {
+
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+
        self.0.fmt(f)
+
    }
+
}
+

+
impl std::ops::Deref for Commit {
+
    type Target = CommitData<git2::Oid, git2::Oid>;
+

+
    fn deref(&self) -> &Self::Target {
+
        &self.0
+
    }
+
}
added crates/radicle-cob/src/backend/git/commit/trailers.rs
@@ -0,0 +1,110 @@
+
use std::{borrow::Cow, fmt, fmt::Write, str::FromStr};
+

+
use git2::{MessageTrailersStrs, MessageTrailersStrsIterator};
+

+
use metadata::commit::trailers::Separator;
+

+
/// A Git commit's set of trailers that are left in the commit's
+
/// message.
+
///
+
/// Trailers are key/value pairs in the last paragraph of a message,
+
/// not including any patches or conflicts that may be present.
+
///
+
/// # Usage
+
///
+
/// To construct `Trailers`, you can use [`Trailers::parse`] or its
+
/// `FromStr` implementation.
+
///
+
/// To iterate over the trailers, you can use [`Trailers::iter`].
+
///
+
/// To render the trailers to a `String`, you can use
+
/// [`Trailers::to_string`] or its `Display` implementation (note that
+
/// it will default to using `": "` as the separator.
+
///
+
/// # Examples
+
///
+
/// ```text
+
/// Add new functionality
+
///
+
/// Making code better with new functionality.
+
///
+
/// X-Signed-Off-By: Alex Sellier
+
/// X-Co-Authored-By: Fintan Halpenny
+
/// ```
+
///
+
/// The trailers in the above example are:
+
///
+
/// ```text
+
/// X-Signed-Off-By: Alex Sellier
+
/// X-Co-Authored-By: Fintan Halpenny
+
/// ```
+
pub struct Trailers {
+
    inner: MessageTrailersStrs,
+
}
+

+
impl Trailers {
+
    pub fn parse(message: &str) -> Result<Self, git2::Error> {
+
        Ok(Self {
+
            inner: git2::message_trailers_strs(message)?,
+
        })
+
    }
+

+
    pub fn iter(&self) -> Iter<'_> {
+
        Iter {
+
            inner: self.inner.iter(),
+
        }
+
    }
+

+
    pub fn to_string<'a, S>(&self, sep: S) -> String
+
    where
+
        S: Separator<'a>,
+
    {
+
        let mut buf = String::new();
+
        for (i, trailer) in self.iter().enumerate() {
+
            if i > 0 {
+
                writeln!(buf).ok();
+
            }
+

+
            write!(buf, "{}", trailer.display(sep.sep_for(&trailer.token))).ok();
+
        }
+
        writeln!(buf).ok();
+
        buf
+
    }
+
}
+

+
impl fmt::Display for Trailers {
+
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
        f.write_str(&self.to_string(": "))
+
    }
+
}
+

+
impl FromStr for Trailers {
+
    type Err = git2::Error;
+

+
    fn from_str(s: &str) -> Result<Self, Self::Err> {
+
        Self::parse(s)
+
    }
+
}
+

+
pub struct Iter<'a> {
+
    inner: MessageTrailersStrsIterator<'a>,
+
}
+

+
impl<'a> Iterator for Iter<'a> {
+
    type Item = metadata::commit::trailers::Trailer<'a>;
+

+
    fn next(&mut self) -> Option<Self::Item> {
+
        let (token, value) = self.inner.next()?;
+
        Some(metadata::commit::trailers::Trailer {
+
            token: {
+
                // This code used to live in the same module with `Token`,
+
                // but was separated because it depends on `git2`.
+
                // We have no way of directly constructing a `Token`, anymore
+
                // but `git2` still guarantees that the trailer is well-formed.
+
                metadata::commit::trailers::Token::try_from(token)
+
                    .expect("token from `git2` must be valid")
+
            },
+
            value: Cow::Borrowed(value),
+
        })
+
    }
+
}
deleted crates/radicle-cob/src/backend/git/stable.rs
@@ -1,79 +0,0 @@
-
use std::{cell::Cell, ops::Add};
-

-
thread_local! {
-
    /// The constant time used by the stable-commit-ids feature.
-
    pub static STABLE_TIME: Cell<i64> = const { Cell::new(1514817556) };
-
    /// An incrementing counter to advance the `STABLE_TIME` value with in
-
    /// [`with_advanced_timestamp`].
-
    pub static STEP: Cell<Step> = Cell::new(Step::default());
-
}
-

-
#[derive(Clone, Copy)]
-
struct Step(i64);
-

-
impl Default for Step {
-
    fn default() -> Self {
-
        Self(1)
-
    }
-
}
-

-
impl Add<Step> for i64 {
-
    type Output = i64;
-

-
    fn add(self, rhs: Step) -> Self::Output {
-
        self + rhs.0
-
    }
-
}
-

-
impl Add<i64> for Step {
-
    type Output = Step;
-

-
    fn add(self, rhs: i64) -> Self::Output {
-
        Step(self.0 + rhs)
-
    }
-
}
-

-
/// Read the current value of `STABLE_TIME`.
-
///
-
/// # Panics
-
///
-
/// The `STABLE_TIME` is declared in `thread_local`, and so the panic
-
/// information is repeated here.
-
///
-
/// Panics if the key currently has its destructor running, and it may panic if
-
/// the destructor has previously been run for this thread.
-
#[allow(clippy::unwrap_used)]
-
pub fn read_timestamp() -> i64 {
-
    STABLE_TIME.get()
-
}
-

-
/// Perform an action `f` that would rely on the `STABLE_TIME` value. This will
-
/// advance the `STABLE_TIME` by an increment of `1` for each time it is called,
-
/// within the same thread.
-
///
-
/// # Usage
-
///
-
/// ```rust, ignore
-
/// let oid1 = with_advanced_timestamp(|| cob.update("New revision OID"));
-
/// let oid2 = with_advanced_timestamp(|| cob.update("Another revision OID"));
-
/// ```
-
///
-
/// # Panics
-
///
-
/// The `STABLE_TIME` is declared in `thread_local`, and so the panic
-
/// information is repeated here.
-
///
-
/// Panics if the key currently has its destructor running, and it may panic if
-
/// the destructor has previously been run for this thread.
-
#[allow(clippy::unwrap_used)]
-
pub fn with_advanced_timestamp<F, T>(f: F) -> T
-
where
-
    F: FnOnce() -> T,
-
{
-
    let step = STEP.get();
-
    let original = read_timestamp();
-
    STABLE_TIME.replace(original + step);
-
    let result = f();
-
    STEP.replace(step + 1);
-
    result
-
}
added crates/radicle-cob/src/backend/stable.rs
@@ -0,0 +1,79 @@
+
use std::{cell::Cell, ops::Add};
+

+
thread_local! {
+
    /// The constant time used by the stable-commit-ids feature.
+
    pub static STABLE_TIME: Cell<i64> = const { Cell::new(1514817556) };
+
    /// An incrementing counter to advance the `STABLE_TIME` value with in
+
    /// [`with_advanced_timestamp`].
+
    pub static STEP: Cell<Step> = Cell::new(Step::default());
+
}
+

+
#[derive(Clone, Copy)]
+
struct Step(i64);
+

+
impl Default for Step {
+
    fn default() -> Self {
+
        Self(1)
+
    }
+
}
+

+
impl Add<Step> for i64 {
+
    type Output = i64;
+

+
    fn add(self, rhs: Step) -> Self::Output {
+
        self + rhs.0
+
    }
+
}
+

+
impl Add<i64> for Step {
+
    type Output = Step;
+

+
    fn add(self, rhs: i64) -> Self::Output {
+
        Step(self.0 + rhs)
+
    }
+
}
+

+
/// Read the current value of `STABLE_TIME`.
+
///
+
/// # Panics
+
///
+
/// The `STABLE_TIME` is declared in `thread_local`, and so the panic
+
/// information is repeated here.
+
///
+
/// Panics if the key currently has its destructor running, and it may panic if
+
/// the destructor has previously been run for this thread.
+
#[allow(clippy::unwrap_used)]
+
pub fn read_timestamp() -> i64 {
+
    STABLE_TIME.get()
+
}
+

+
/// Perform an action `f` that would rely on the `STABLE_TIME` value. This will
+
/// advance the `STABLE_TIME` by an increment of `1` for each time it is called,
+
/// within the same thread.
+
///
+
/// # Usage
+
///
+
/// ```rust, ignore
+
/// let oid1 = with_advanced_timestamp(|| cob.update("New revision OID"));
+
/// let oid2 = with_advanced_timestamp(|| cob.update("Another revision OID"));
+
/// ```
+
///
+
/// # Panics
+
///
+
/// The `STABLE_TIME` is declared in `thread_local`, and so the panic
+
/// information is repeated here.
+
///
+
/// Panics if the key currently has its destructor running, and it may panic if
+
/// the destructor has previously been run for this thread.
+
#[allow(clippy::unwrap_used)]
+
pub fn with_advanced_timestamp<F, T>(f: F) -> T
+
where
+
    F: FnOnce() -> T,
+
{
+
    let step = STEP.get();
+
    let original = read_timestamp();
+
    STABLE_TIME.replace(original + step);
+
    let result = f();
+
    STEP.replace(step + 1);
+
    result
+
}
modified crates/radicle-cob/src/change.rs
@@ -1,6 +1,6 @@
// Copyright © 2021 The Radicle Link Contributors

-
use git_ext::Oid;
+
use oid::Oid;

pub mod store;
pub use store::{Contents, EntryId, Storage, Template, Timestamp};
modified crates/radicle-cob/src/change/store.rs
@@ -3,14 +3,15 @@
use std::{error::Error, fmt, num::NonZeroUsize};

use nonempty::NonEmpty;
-
use radicle_git_ext::Oid;
+
use oid::Oid;
use serde::{Deserialize, Serialize};

+
use crate::object::collaboration::error::{Create, Update};
use crate::{signatures, TypeName};

/// Change entry storage.
pub trait Storage {
-
    type StoreError: Error + Send + Sync + 'static;
+
    type StoreError: Error + Send + Sync + 'static + Into<Create> + Into<Update>;
    type LoadError: Error + Send + Sync + 'static;

    type ObjectId;
@@ -194,6 +195,7 @@ pub struct Embed<T = Vec<u8>> {
    pub content: T,
}

+
#[cfg(feature = "git2")]
impl<T: From<Oid>> Embed<T> {
    /// Create a new embed.
    pub fn store(
@@ -210,6 +212,7 @@ impl<T: From<Oid>> Embed<T> {
    }
}

+
#[cfg(feature = "git2")]
impl Embed<Vec<u8>> {
    /// Get the object id of the embedded content.
    pub fn oid(&self) -> Oid {
modified crates/radicle-cob/src/change_graph.rs
@@ -3,8 +3,8 @@
use std::ops::ControlFlow;
use std::{cmp::Ordering, collections::BTreeSet};

-
use git_ext::Oid;
-
use radicle_dag::Dag;
+
use dag::Dag;
+
use oid::Oid;

use crate::{
    change, object, object::collaboration::Evaluate, signatures::ExtendedSignature,
modified crates/radicle-cob/src/history.rs
@@ -2,8 +2,8 @@
#![allow(clippy::too_many_arguments)]
use std::{cmp::Ordering, collections::BTreeSet, ops::ControlFlow};

-
use git_ext::Oid;
-
use radicle_dag::Dag;
+
use dag::Dag;
+
use oid::Oid;

pub use crate::change::{Contents, Entry, EntryId, Timestamp};

@@ -77,7 +77,7 @@ impl History {
        self.graph.node(id, change);

        for tip in tips {
-
            self.graph.dependency(id, (*tip).into());
+
            self.graph.dependency(id, tip);
        }
    }

modified crates/radicle-cob/src/lib.rs
@@ -59,12 +59,20 @@ extern crate qcheck;
#[macro_use(quickcheck)]
extern crate qcheck_macros;

+
extern crate git_ref_format_core as fmt;
extern crate radicle_crypto as crypto;
-
extern crate radicle_git_ext as git_ext;
+
extern crate radicle_dag as dag;
+
extern crate radicle_git_metadata as metadata;
+
extern crate radicle_oid as oid;

mod backend;
+

+
#[cfg(all(any(test, feature = "test"), feature = "git2"))]
pub use backend::git;

+
#[cfg(feature = "stable-commit-ids")]
+
pub use backend::stable;
+

mod change_graph;
mod trailers;

@@ -105,19 +113,9 @@ mod tests;
///
///   * [`object::Storage`]
///
-
/// **Note**: [`change::Storage`] is already implemented for
-
/// [`git2::Repository`]. It is expected that the underlying storage
-
/// for `object::Storage` will also be `git2::Repository`, but if not
-
/// please open an issue to change the definition of `Store` :)
pub trait Store
where
    Self: object::Storage
-
        + change::Storage<
-
            StoreError = git::change::error::Create,
-
            LoadError = git::change::error::Load,
-
            ObjectId = git_ext::Oid,
-
            Parent = git_ext::Oid,
-
            Signatures = ExtendedSignature,
-
        >,
+
        + change::Storage<ObjectId = oid::Oid, Parent = oid::Oid, Signatures = ExtendedSignature>,
{
}
modified crates/radicle-cob/src/object.rs
@@ -1,9 +1,9 @@
// Copyright © 2022 The Radicle Link Contributors

-
use std::{convert::TryFrom as _, fmt, ops::Deref, str::FromStr};
+
use std::{convert::TryFrom as _, ops::Deref, str::FromStr};

-
use git_ext::ref_format::{Component, RefString};
-
use git_ext::Oid;
+
use fmt::{Component, RefString};
+
use oid::Oid;
use serde::{Deserialize, Serialize};
use thiserror::Error;

@@ -19,7 +19,7 @@ pub use storage::{Commit, Objects, Reference, Storage};
#[derive(Debug, Error)]
pub enum ParseObjectId {
    #[error(transparent)]
-
    Git(#[from] git2::Error),
+
    Git(#[from] oid::str::ParseOidError),
}

/// The id of an object
@@ -48,12 +48,14 @@ impl From<&Oid> for ObjectId {
    }
}

+
#[cfg(feature = "git2")]
impl From<git2::Oid> for ObjectId {
    fn from(oid: git2::Oid) -> Self {
        Oid::from(oid).into()
    }
}

+
#[cfg(feature = "git2")]
impl From<&git2::Oid> for ObjectId {
    fn from(oid: &git2::Oid) -> Self {
        ObjectId(Oid::from(*oid))
@@ -68,8 +70,8 @@ impl Deref for ObjectId {
    }
}

-
impl fmt::Display for ObjectId {
-
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
impl std::fmt::Display for ObjectId {
+
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}
modified crates/radicle-cob/src/object/collaboration.rs
@@ -2,8 +2,8 @@
use std::convert::Infallible;
use std::fmt::Debug;

-
use git_ext::Oid;
use nonempty::NonEmpty;
+
use oid::Oid;

use crate::change::store::{Manifest, Version};
use crate::{change, Entry, History, ObjectId, TypeName};
@@ -101,11 +101,11 @@ impl<R> Evaluate<R> for NonEmpty<Entry> {
/// [`TypeName`] and [`ObjectId`] from it.
///
/// This assumes that the `refname` is in a
-
/// [`git_ext::ref_format::Qualified`] format. If it has any
+
/// [`fmt::Qualified`] format. If it has any
/// `refs/namespaces`, they will be stripped to access the underlying
-
/// [`git_ext::ref_format::Qualified`] format.
+
/// [`fmt::Qualified`] format.
///
-
/// In the [`git_ext::ref_format::Qualified`] format it assumes that the
+
/// In the [`fmt::Qualified`] format it assumes that the
/// reference name is of the form:
///
///   `refs/<category>/<typename>/<object_id>[/<rest>*]`
@@ -115,14 +115,14 @@ impl<R> Evaluate<R> for NonEmpty<Entry> {
///
/// Also note that this will return `None` if:
///
-
///   * The `refname` is not [`git_ext::ref_format::Qualified`]
+
///   * The `refname` is not [`fmt::Qualified`]
///   * The parsing of the [`ObjectId`] fails
///   * The parsing of the [`TypeName`] fails
pub fn parse_refstr<R>(name: &R) -> Option<(TypeName, ObjectId)>
where
-
    R: AsRef<git_ext::ref_format::RefStr>,
+
    R: AsRef<fmt::RefStr>,
{
-
    use git_ext::ref_format::Qualified;
+
    use fmt::Qualified;
    let name = name.as_ref();
    let refs_cobs = match name.to_namespaced() {
        None => Qualified::from_refstr(name)?,
modified crates/radicle-cob/src/object/collaboration/create.rs
@@ -4,7 +4,6 @@ use nonempty::NonEmpty;

use crate::Embed;
use crate::Evaluate;
-
use crate::Store;

use super::*;

@@ -23,7 +22,7 @@ pub struct Create {
}

impl Create {
-
    fn template(self) -> change::Template<git_ext::Oid> {
+
    fn template(self) -> change::Template<oid::Oid> {
        change::Template {
            type_name: self.type_name,
            tips: Vec::new(),
@@ -38,7 +37,7 @@ impl Create {
///
/// The `storage` is the backing storage for storing
/// [`crate::Entry`]s at content-addressable locations. Please see
-
/// [`Store`] for further information.
+
/// [`crate::Store`] for further information.
///
/// The `signer` is expected to be a cryptographic signing key. This
/// ensures that the objects origin is cryptographically verifiable.
@@ -57,19 +56,24 @@ pub fn create<T, S, G>(
    signer: &G,
    resource: Option<Oid>,
    related: Vec<Oid>,
-
    identifier: &S::Namespace,
+
    identifier: &<S as crate::object::Storage>::Namespace,
    args: Create,
) -> Result<CollaborativeObject<T>, error::Create>
where
    T: Evaluate<S>,
-
    S: Store,
+
    S: crate::object::Storage
+
        + crate::change::Storage<
+
            ObjectId = crate::object::Oid,
+
            Parent = crate::object::Oid,
+
            Signatures = crate::ExtendedSignature,
+
        >,
    G: signature::Signer<crate::ExtendedSignature>,
{
    let type_name = args.type_name.clone();
    let version = args.version;
    let init_change = storage
        .store(resource, related, signer, args.template())
-
        .map_err(error::Create::from)?;
+
        .map_err(Into::<error::Create>::into)?;
    let object_id = init_change.id().into();
    let object = T::init(&init_change, storage).map_err(error::Create::evaluate)?;

modified crates/radicle-cob/src/object/collaboration/error.rs
@@ -2,14 +2,13 @@

use thiserror::Error;

-
use crate::git;
-

#[derive(Debug, Error)]
pub enum Create {
    #[error(transparent)]
    Evaluate(Box<dyn std::error::Error + Send + Sync + 'static>),
    #[error(transparent)]
-
    CreateChange(#[from] git::change::error::Create),
+
    #[cfg(feature = "git2")]
+
    CreateChange(#[from] crate::backend::git::change::error::Create),
    #[error("failed to updated references for during object creation: {err}")]
    Refs {
        #[source]
@@ -39,6 +38,7 @@ pub enum Retrieve {
    #[error(transparent)]
    Evaluate(Box<dyn std::error::Error + Send + Sync + 'static>),
    #[error(transparent)]
+
    #[cfg(feature = "git2")]
    Git(#[from] git2::Error),
    #[error("failed to get references during object retrieval: {err}")]
    Refs {
@@ -62,13 +62,15 @@ pub enum Update {
    #[error("no object found")]
    NoSuchObject,
    #[error(transparent)]
-
    CreateChange(#[from] git::change::error::Create),
+
    #[cfg(feature = "git2")]
+
    CreateChange(#[from] crate::backend::git::change::error::Create),
    #[error("failed to get references during object update: {err}")]
    Refs {
        #[source]
        err: Box<dyn std::error::Error + Send + Sync + 'static>,
    },
    #[error(transparent)]
+
    #[cfg(feature = "git2")]
    Git(#[from] git2::Error),
    #[error(transparent)]
    Io(#[from] std::io::Error),
modified crates/radicle-cob/src/object/collaboration/get.rs
@@ -1,6 +1,8 @@
// Copyright © 2022 The Radicle Link Contributors

-
use crate::{change_graph::ChangeGraph, CollaborativeObject, Evaluate, ObjectId, Store, TypeName};
+
use crypto::ssh::ExtendedSignature;
+

+
use crate::{change_graph::ChangeGraph, CollaborativeObject, Evaluate, ObjectId, TypeName};

use super::error;

@@ -8,7 +10,7 @@ use super::error;
///
/// The `storage` is the backing storage for storing
/// [`crate::Entry`]s at content-addressable locations. Please see
-
/// [`Store`] for further information.
+
/// [`crate::Store`] for further information.
///
/// The `typename` is the type of object to be found, while the
/// `object_id` is the identifier for the particular object under that
@@ -20,7 +22,12 @@ pub fn get<T, S>(
) -> Result<Option<CollaborativeObject<T>>, error::Retrieve>
where
    T: Evaluate<S>,
-
    S: Store,
+
    S: crate::object::Storage,
+
    S: crate::change::Storage<
+
        ObjectId = crate::object::Oid,
+
        Parent = crate::object::Oid,
+
        Signatures = ExtendedSignature,
+
    >,
{
    let tip_refs = storage
        .objects(typename, oid)
modified crates/radicle-cob/src/object/collaboration/info.rs
@@ -6,9 +6,10 @@

use std::collections::BTreeSet;

-
use git_ext::Oid;
+
use crypto::ssh::ExtendedSignature;
+
use oid::Oid;

-
use crate::{change_graph::ChangeGraph, ObjectId, Store, TypeName};
+
use crate::{change_graph::ChangeGraph, ObjectId, TypeName};

use super::error;

@@ -28,7 +29,7 @@ pub struct ChangeGraphInfo {
///
/// The `storage` is the backing storage for storing
/// [`crate::Entry`]s at content-addressable locations. Please see
-
/// [`Store`] for further information.
+
/// [`crate::Store`] for further information.
///
/// The `typename` is the type of object to be found, while the `oid`
/// is the identifier for the particular object under that type.
@@ -38,7 +39,12 @@ pub fn changegraph<S>(
    oid: &ObjectId,
) -> Result<Option<ChangeGraphInfo>, error::Retrieve>
where
-
    S: Store,
+
    S: crate::object::Storage,
+
    S: crate::change::Storage<
+
        ObjectId = crate::object::Oid,
+
        Parent = crate::object::Oid,
+
        Signatures = ExtendedSignature,
+
    >,
{
    let tip_refs = storage
        .objects(typename, oid)
modified crates/radicle-cob/src/object/collaboration/list.rs
@@ -1,6 +1,6 @@
// Copyright © 2022 The Radicle Link Contributors

-
use crate::{change_graph::ChangeGraph, CollaborativeObject, Evaluate, Store, TypeName};
+
use crate::{change_graph::ChangeGraph, CollaborativeObject, Evaluate, TypeName};

use super::error;

@@ -8,7 +8,7 @@ use super::error;
///
/// The `storage` is the backing storage for storing
/// [`crate::Entry`]s at content-addressable locations. Please see
-
/// [`Store`] for further information.
+
/// [`crate::Store`] for further information.
///
/// The `typename` is the type of objects to be listed.
pub fn list<T, S>(
@@ -17,7 +17,12 @@ pub fn list<T, S>(
) -> Result<Vec<CollaborativeObject<T>>, error::Retrieve>
where
    T: Evaluate<S>,
-
    S: Store,
+
    S: crate::object::Storage,
+
    S: crate::change::Storage<
+
        ObjectId = crate::object::Oid,
+
        Parent = crate::object::Oid,
+
        Signatures = crate::ExtendedSignature,
+
    >,
{
    let references = storage
        .types(typename)
modified crates/radicle-cob/src/object/collaboration/remove.rs
@@ -1,6 +1,6 @@
// Copyright © 2022 The Radicle Link Contributors

-
use crate::{ObjectId, Store, TypeName};
+
use crate::{ObjectId, TypeName};

use super::error;

@@ -8,7 +8,7 @@ use super::error;
///
/// The `storage` is the backing storage for storing
/// [`crate::Entry`]s at content-addressable locations. Please see
-
/// [`Store`] for further information.
+
/// [`crate::Store`] for further information.
///
/// The `typename` is the type of object to be found, while the
/// `object_id` is the identifier for the particular object under that
@@ -20,7 +20,7 @@ pub fn remove<S>(
    oid: &ObjectId,
) -> Result<(), error::Remove>
where
-
    S: Store,
+
    S: crate::object::Storage,
{
    storage
        .remove(identifier, typename, oid)
modified crates/radicle-cob/src/object/collaboration/update.rs
@@ -1,12 +1,12 @@
// Copyright © 2022 The Radicle Link Contributors
use std::iter;

-
use git_ext::Oid;
use nonempty::NonEmpty;
+
use oid::Oid;

use crate::{
    change, change_graph::ChangeGraph, history::EntryId, CollaborativeObject, Embed, Evaluate,
-
    ExtendedSignature, ObjectId, Store, TypeName,
+
    ExtendedSignature, ObjectId, TypeName,
};

use super::error;
@@ -40,7 +40,7 @@ pub struct Update {
///
/// The `storage` is the backing storage for storing
/// [`crate::Entry`]s at content-addressable locations. Please see
-
/// [`Store`] for further information.
+
/// [`crate::Store`] for further information.
///
/// The `signer` is expected to be a cryptographic signing key. This
/// ensures that the objects origin is cryptographically verifiable.
@@ -65,7 +65,8 @@ pub fn update<T, S, G>(
) -> Result<Updated<T>, error::Update>
where
    T: Evaluate<S>,
-
    S: Store,
+
    S: crate::object::Storage,
+
    S: change::Storage<ObjectId = Oid, Parent = Oid, Signatures = ExtendedSignature>,
    G: signature::Signer<ExtendedSignature>,
{
    let Update {
@@ -86,18 +87,20 @@ where
        graph.evaluate(storage).map_err(error::Update::evaluate)?;

    // Create a commit for this change, but don't update any references yet.
-
    let entry = storage.store(
-
        resource,
-
        related,
-
        signer,
-
        change::Template {
-
            tips: object.history.tips().into_iter().collect(),
-
            embeds,
-
            contents: changes,
-
            type_name: typename.clone(),
-
            message,
-
        },
-
    )?;
+
    let entry = storage
+
        .store(
+
            resource,
+
            related,
+
            signer,
+
            change::Template {
+
                tips: object.history.tips().into_iter().collect(),
+
                embeds,
+
                contents: changes,
+
                type_name: typename.clone(),
+
                message,
+
            },
+
        )
+
        .map_err(Into::<error::Update>::into)?;
    let head = entry.id;
    let parents = entry.parents.to_vec();

modified crates/radicle-cob/src/object/storage.rs
@@ -2,8 +2,8 @@

use std::{collections::BTreeMap, error::Error};

-
use git_ext::ref_format::RefString;
-
use git_ext::Oid;
+
use fmt::RefString;
+
use oid::Oid;

use crate::change::EntryId;
use crate::{ObjectId, TypeName};
@@ -93,41 +93,43 @@ pub trait Storage {
pub mod convert {
    use std::str;

-
    use git_ext::ref_format::RefString;
+
    use fmt::RefString;
    use thiserror::Error;

-
    use super::{Commit, Reference};
-

    #[derive(Debug, Error)]
    pub enum Error {
        #[error("the reference '{name}' does not point to a commit object")]
        NotCommit {
            name: RefString,
+
            #[cfg(feature = "git2")]
            #[source]
            err: git2::Error,
        },
        #[error(transparent)]
-
        Ref(#[from] git_ext::ref_format::Error),
+
        Ref(#[from] fmt::Error),
        #[error(transparent)]
        Utf8(#[from] str::Utf8Error),
    }

-
    impl<'a> TryFrom<git2::Reference<'a>> for Reference {
+
    #[cfg(feature = "git2")]
+
    impl<'a> TryFrom<git2::Reference<'a>> for super::Reference {
        type Error = Error;

        fn try_from(value: git2::Reference<'a>) -> Result<Self, Self::Error> {
            let name = RefString::try_from(str::from_utf8(value.name_bytes())?)?;
-
            let target = Commit::from(value.peel_to_commit().map_err(|err| Error::NotCommit {
-
                name: name.clone(),
-
                err,
-
            })?);
+
            let target =
+
                super::Commit::from(value.peel_to_commit().map_err(|err| Error::NotCommit {
+
                    name: name.clone(),
+
                    err,
+
                })?);
            Ok(Self { name, target })
        }
    }

-
    impl<'a> From<git2::Commit<'a>> for Commit {
+
    #[cfg(feature = "git2")]
+
    impl<'a> From<git2::Commit<'a>> for super::Commit {
        fn from(commit: git2::Commit<'a>) -> Self {
-
            Commit {
+
            Self {
                id: commit.id().into(),
            }
        }
modified crates/radicle-cob/src/signatures.rs
@@ -8,9 +8,9 @@ use std::{
};

use crypto::{ssh, PublicKey};
-
use git_ext::commit::{
+
use metadata::commit::{
    headers::Signature::{Pgp, Ssh},
-
    Commit,
+
    CommitData,
};

pub use ssh::ExtendedSignature;
@@ -55,10 +55,10 @@ impl From<Signatures> for BTreeMap<PublicKey, crypto::Signature> {
    }
}

-
impl TryFrom<&Commit> for Signatures {
+
impl<Tree, Parent> TryFrom<&CommitData<Tree, Parent>> for Signatures {
    type Error = error::Signatures;

-
    fn try_from(value: &Commit) -> Result<Self, Self::Error> {
+
    fn try_from(value: &CommitData<Tree, Parent>) -> Result<Self, Self::Error> {
        value
            .signatures()
            .filter_map(|signature| {
modified crates/radicle-cob/src/signatures/error.rs
@@ -1,6 +1,6 @@
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>

-
use radicle_crypto::ssh::ExtendedSignatureError;
+
use crypto::ssh::ExtendedSignatureError;
use thiserror::Error;

#[derive(Debug, Error)]
modified crates/radicle-cob/src/test.rs
@@ -1,7 +1,11 @@
+
#[cfg(feature = "git2")]
pub mod identity;
+
#[cfg(feature = "git2")]
pub use identity::{Person, Project, RemoteProject};

+
#[cfg(feature = "git2")]
pub mod storage;
+
#[cfg(feature = "git2")]
pub use storage::Storage;

pub mod arbitrary;
modified crates/radicle-cob/src/test/arbitrary.rs
@@ -26,11 +26,7 @@ impl Arbitrary for TypeName {

impl Arbitrary for ObjectId {
    fn arbitrary(g: &mut qcheck::Gen) -> Self {
-
        let mut rng = fastrand::Rng::with_seed(u64::arbitrary(g));
-
        let bytes = iter::repeat_with(|| rng.u8(..))
-
            .take(20)
-
            .collect::<Vec<_>>();
-
        Self::from(git_ext::Oid::try_from(bytes.as_slice()).unwrap())
+
        Self::from(oid::Oid::arbitrary(g))
    }
}

modified crates/radicle-cob/src/test/identity.rs
@@ -1,7 +1,9 @@
pub mod project;
pub use project::{Project, RemoteProject};

+
#[cfg(feature = "git2")]
pub mod person;
+
#[cfg(feature = "git2")]
pub use person::Person;

#[derive(Clone, Debug, PartialEq, Eq)]
modified crates/radicle-cob/src/test/identity/person.rs
@@ -1,4 +1,4 @@
-
use git_ext::Oid;
+
use oid::Oid;
use serde::{Deserialize, Serialize};

use crate::test::storage::{self, Storage};
modified crates/radicle-cob/src/test/identity/project.rs
@@ -1,6 +1,6 @@
use std::collections::BTreeSet;

-
use git_ext::Oid;
+
use oid::Oid;
use serde::{Deserialize, Serialize};

use crate::test;
modified crates/radicle-cob/src/test/storage.rs
@@ -1,6 +1,8 @@
use std::{collections::BTreeMap, convert::TryFrom as _};

-
use radicle_git_ext::ref_format::{refname, Component};
+
use radicle_git_ref_format::refname;
+

+
use fmt::Component;
use tempfile::TempDir;

use crate::{
@@ -29,7 +31,7 @@ pub mod error {
        #[error(transparent)]
        Git(#[from] git2::Error),
        #[error(transparent)]
-
        Format(#[from] git_ext::ref_format::Error),
+
        Format(#[from] fmt::Error),
    }
}

@@ -93,16 +95,16 @@ impl change::Storage for Storage {
        self.as_raw().load(id)
    }

-
    fn parents_of(&self, id: &git_ext::Oid) -> Result<Vec<git_ext::Oid>, Self::LoadError> {
+
    fn parents_of(&self, id: &oid::Oid) -> Result<Vec<radicle_oid::Oid>, Self::LoadError> {
        Ok(self
            .as_raw()
-
            .find_commit(**id)?
+
            .find_commit(id.into())?
            .parent_ids()
-
            .map(git_ext::Oid::from)
+
            .map(oid::Oid::from)
            .collect::<Vec<_>>())
    }

-
    fn manifest_of(&self, id: &git_ext::Oid) -> Result<crate::Manifest, Self::LoadError> {
+
    fn manifest_of(&self, id: &oid::Oid) -> Result<crate::Manifest, Self::LoadError> {
        self.as_raw().manifest_of(id)
    }
}
modified crates/radicle-cob/src/tests.rs
@@ -1,238 +1,274 @@
-
use std::ops::ControlFlow;
-

-
use crypto::test::signer::MockSigner;
-
use crypto::{PublicKey, Signer};
-
use git_ext::ref_format::{refname, Component, RefString};
-
use nonempty::{nonempty, NonEmpty};
-
use qcheck::Arbitrary;
-

-
use crate::{
-
    create, get, list, object, test::arbitrary::Invalid, update, Create, Entry, ObjectId, TypeName,
-
    Update, Updated, Version,
-
};
-

-
use super::test;
-

-
#[test]
-
fn roundtrip() {
-
    let storage = test::Storage::new();
-
    let signer = gen::<MockSigner>(1);
-
    let terry = test::Person::new(&storage, "terry", *signer.public_key()).unwrap();
-
    let proj = test::Project::new(&storage, "discworld", *signer.public_key()).unwrap();
-
    let proj = test::RemoteProject {
-
        project: proj,
-
        person: terry,
-
    };
-
    let typename = "xyz.rad.issue".parse::<TypeName>().unwrap();
-
    let cob = create::<NonEmpty<Entry>, _, _>(
-
        &storage,
-
        &signer,
-
        Some(proj.project.content_id),
-
        vec![],
-
        signer.public_key(),
-
        Create {
-
            contents: nonempty!(Vec::new()),
-
            type_name: typename.clone(),
-
            message: "creating xyz.rad.issue".to_string(),
-
            embeds: vec![],
-
            version: Version::default(),
-
        },
-
    )
-
    .unwrap();
-

-
    let expected = get(&storage, &typename, cob.id())
-
        .unwrap()
-
        .expect("BUG: cob was missing");
-

-
    assert_eq!(cob, expected);
-
}
+
use fmt::{Component, RefString};

-
#[test]
-
fn list_cobs() {
-
    let storage = test::Storage::new();
-
    let signer = gen::<MockSigner>(1);
-
    let terry = test::Person::new(&storage, "terry", *signer.public_key()).unwrap();
-
    let proj = test::Project::new(&storage, "discworld", *signer.public_key()).unwrap();
-
    let proj = test::RemoteProject {
-
        project: proj,
-
        person: terry,
-
    };
-
    let typename = "xyz.rad.issue".parse::<TypeName>().unwrap();
-
    let issue_1 = create::<NonEmpty<Entry>, _, _>(
-
        &storage,
-
        &signer,
-
        Some(proj.project.content_id),
-
        vec![],
-
        signer.public_key(),
-
        Create {
-
            contents: nonempty!(b"issue 1".to_vec()),
-
            type_name: typename.clone(),
-
            message: "creating xyz.rad.issue".to_string(),
-
            embeds: vec![],
-
            version: Version::default(),
-
        },
-
    )
-
    .unwrap();
-

-
    let issue_2 = create(
-
        &storage,
-
        &signer,
-
        Some(proj.project.content_id),
-
        vec![],
-
        signer.public_key(),
-
        Create {
-
            contents: nonempty!(b"issue 2".to_vec()),
-
            type_name: typename.clone(),
-
            message: "commenting xyz.rad.issue".to_string(),
-
            embeds: vec![],
-
            version: Version::default(),
-
        },
-
    )
-
    .unwrap();
-

-
    let mut expected = list(&storage, &typename).unwrap();
-
    expected.sort_by(|x, y| x.id().cmp(y.id()));
-

-
    let mut actual = vec![issue_1, issue_2];
-
    actual.sort_by(|x, y| x.id().cmp(y.id()));
-

-
    assert_eq!(actual, expected);
-
}
+
use radicle_git_ref_format::refname;

-
#[test]
-
fn update_cob() {
-
    let storage = test::Storage::new();
-
    let signer = gen::<MockSigner>(1);
-
    let terry = test::Person::new(&storage, "terry", *signer.public_key()).unwrap();
-
    let proj = test::Project::new(&storage, "discworld", *signer.public_key()).unwrap();
-
    let proj = test::RemoteProject {
-
        project: proj,
-
        person: terry,
-
    };
-
    let typename = "xyz.rad.issue".parse::<TypeName>().unwrap();
-
    let cob = create::<NonEmpty<Entry>, _, _>(
-
        &storage,
-
        &signer,
-
        Some(proj.project.content_id),
-
        vec![],
-
        signer.public_key(),
-
        Create {
-
            contents: nonempty!(Vec::new()),
-
            type_name: typename.clone(),
-
            message: "creating xyz.rad.issue".to_string(),
-
            embeds: vec![],
-
            version: Version::default(),
-
        },
-
    )
-
    .unwrap();
-

-
    let not_expected = get::<NonEmpty<Entry>, _>(&storage, &typename, cob.id())
-
        .unwrap()
-
        .expect("BUG: cob was missing");
-

-
    let Updated { object, .. } = update(
-
        &storage,
-
        &signer,
-
        Some(proj.project.content_id),
-
        vec![],
-
        signer.public_key(),
-
        Update {
-
            changes: nonempty!(b"issue 1".to_vec()),
-
            object_id: *cob.id(),
-
            type_name: typename.clone(),
-
            embeds: vec![],
-
            message: "commenting xyz.rad.issue".to_string(),
-
        },
-
    )
-
    .unwrap();
-

-
    let expected = get(&storage, &typename, object.id())
-
        .unwrap()
-
        .expect("BUG: cob was missing");
-

-
    assert_ne!(object, not_expected);
-
    assert_eq!(object, expected, "{object:#?} {expected:#?}");
-
}
+
use crate::{object, test::arbitrary::Invalid, ObjectId, TypeName};

-
#[test]
-
fn traverse_cobs() {
-
    let storage = test::Storage::new();
-
    let neil_signer = gen::<MockSigner>(2);
-
    let neil = test::Person::new(&storage, "gaiman", *neil_signer.public_key()).unwrap();
-
    let terry_signer = gen::<MockSigner>(1);
-
    let terry = test::Person::new(&storage, "pratchett", *terry_signer.public_key()).unwrap();
-
    let proj = test::Project::new(&storage, "discworld", *terry_signer.public_key()).unwrap();
-
    let terry_proj = test::RemoteProject {
-
        project: proj.clone(),
-
        person: terry,
-
    };
-
    let neil_proj = test::RemoteProject {
-
        project: proj,
-
        person: neil,
-
    };
-
    let typename = "xyz.rad.issue".parse::<TypeName>().unwrap();
-
    let cob = create::<NonEmpty<Entry>, _, _>(
-
        &storage,
-
        &terry_signer,
-
        Some(terry_proj.project.content_id),
-
        vec![],
-
        terry_signer.public_key(),
-
        Create {
-
            contents: nonempty!(b"issue 1".to_vec()),
-
            type_name: typename.clone(),
-
            message: "creating xyz.rad.issue".to_string(),
-
            embeds: vec![],
-
            version: Version::default(),
-
        },
-
    )
-
    .unwrap();
-
    copy_to(
-
        storage.as_raw(),
-
        terry_signer.public_key(),
-
        &neil_proj,
-
        &typename,
-
        *cob.id(),
-
    )
-
    .unwrap();
-

-
    let Updated { object, .. } = update::<NonEmpty<Entry>, _, _>(
-
        &storage,
-
        &neil_signer,
-
        Some(neil_proj.project.content_id),
-
        vec![],
-
        neil_signer.public_key(),
-
        Update {
-
            changes: nonempty!(b"issue 2".to_vec()),
-
            object_id: *cob.id(),
-
            type_name: typename,
-
            embeds: vec![],
-
            message: "commenting on xyz.rad.issue".to_string(),
-
        },
-
    )
-
    .unwrap();
-

-
    let root = object.history.root().id;
-
    // traverse over the history and filter by changes that were only authorized by terry
-
    let contents = object
-
        .history()
-
        .traverse(Vec::new(), &[root], |mut acc, _, entry| {
-
            if entry.author() == terry_signer.public_key() {
-
                acc.push(entry.contents().head.clone());
-
            }
-
            ControlFlow::Continue(acc)
-
        });
+
#[cfg(feature = "git2")]
+
mod git {
+
    use std::ops::ControlFlow;

-
    assert_eq!(contents, vec![b"issue 1".to_vec()]);
+
    use crypto::test::signer::MockSigner;
+
    use crypto::{PublicKey, Signer};
+
    use nonempty::{nonempty, NonEmpty};
+
    use qcheck::Arbitrary;

-
    // traverse over the history and filter by changes that were only authorized by neil
-
    let contents = object
-
        .history()
-
        .traverse(Vec::new(), &[root], |mut acc, _, entry| {
-
            acc.push(entry.contents().head.clone());
-
            ControlFlow::Continue(acc)
-
        });
+
    use crate::{
+
        create, get, list, update, Create, Entry, ObjectId, TypeName, Update, Updated, Version,
+
    };

-
    assert_eq!(contents, vec![b"issue 1".to_vec(), b"issue 2".to_vec()]);
+
    use crate::test;
+

+
    #[test]
+
    fn roundtrip() {
+
        let storage = test::Storage::new();
+
        let signer = gen::<MockSigner>(1);
+
        let terry = test::Person::new(&storage, "terry", *signer.public_key()).unwrap();
+
        let proj = test::Project::new(&storage, "discworld", *signer.public_key()).unwrap();
+
        let proj = test::RemoteProject {
+
            project: proj,
+
            person: terry,
+
        };
+
        let typename = "xyz.rad.issue".parse::<TypeName>().unwrap();
+
        let cob = create::<NonEmpty<Entry>, _, _>(
+
            &storage,
+
            &signer,
+
            Some(proj.project.content_id),
+
            vec![],
+
            signer.public_key(),
+
            Create {
+
                contents: nonempty!(Vec::new()),
+
                type_name: typename.clone(),
+
                message: "creating xyz.rad.issue".to_string(),
+
                embeds: vec![],
+
                version: Version::default(),
+
            },
+
        )
+
        .unwrap();
+

+
        let expected = get(&storage, &typename, cob.id())
+
            .unwrap()
+
            .expect("BUG: cob was missing");
+

+
        assert_eq!(cob, expected);
+
    }
+

+
    #[test]
+
    fn list_cobs() {
+
        let storage = test::Storage::new();
+
        let signer = gen::<MockSigner>(1);
+
        let terry = test::Person::new(&storage, "terry", *signer.public_key()).unwrap();
+
        let proj = test::Project::new(&storage, "discworld", *signer.public_key()).unwrap();
+
        let proj = test::RemoteProject {
+
            project: proj,
+
            person: terry,
+
        };
+
        let typename = "xyz.rad.issue".parse::<TypeName>().unwrap();
+
        let issue_1 = create::<NonEmpty<Entry>, _, _>(
+
            &storage,
+
            &signer,
+
            Some(proj.project.content_id),
+
            vec![],
+
            signer.public_key(),
+
            Create {
+
                contents: nonempty!(b"issue 1".to_vec()),
+
                type_name: typename.clone(),
+
                message: "creating xyz.rad.issue".to_string(),
+
                embeds: vec![],
+
                version: Version::default(),
+
            },
+
        )
+
        .unwrap();
+

+
        let issue_2 = create(
+
            &storage,
+
            &signer,
+
            Some(proj.project.content_id),
+
            vec![],
+
            signer.public_key(),
+
            Create {
+
                contents: nonempty!(b"issue 2".to_vec()),
+
                type_name: typename.clone(),
+
                message: "commenting xyz.rad.issue".to_string(),
+
                embeds: vec![],
+
                version: Version::default(),
+
            },
+
        )
+
        .unwrap();
+

+
        let mut expected = list(&storage, &typename).unwrap();
+
        expected.sort_by(|x, y| x.id().cmp(y.id()));
+

+
        let mut actual = vec![issue_1, issue_2];
+
        actual.sort_by(|x, y| x.id().cmp(y.id()));
+

+
        assert_eq!(actual, expected);
+
    }
+

+
    #[test]
+
    fn update_cob() {
+
        let storage = test::Storage::new();
+
        let signer = gen::<MockSigner>(1);
+
        let terry = test::Person::new(&storage, "terry", *signer.public_key()).unwrap();
+
        let proj = test::Project::new(&storage, "discworld", *signer.public_key()).unwrap();
+
        let proj = test::RemoteProject {
+
            project: proj,
+
            person: terry,
+
        };
+
        let typename = "xyz.rad.issue".parse::<TypeName>().unwrap();
+
        let cob = create::<NonEmpty<Entry>, _, _>(
+
            &storage,
+
            &signer,
+
            Some(proj.project.content_id),
+
            vec![],
+
            signer.public_key(),
+
            Create {
+
                contents: nonempty!(Vec::new()),
+
                type_name: typename.clone(),
+
                message: "creating xyz.rad.issue".to_string(),
+
                embeds: vec![],
+
                version: Version::default(),
+
            },
+
        )
+
        .unwrap();
+

+
        let not_expected = get::<NonEmpty<Entry>, _>(&storage, &typename, cob.id())
+
            .unwrap()
+
            .expect("BUG: cob was missing");
+

+
        let Updated { object, .. } = update(
+
            &storage,
+
            &signer,
+
            Some(proj.project.content_id),
+
            vec![],
+
            signer.public_key(),
+
            Update {
+
                changes: nonempty!(b"issue 1".to_vec()),
+
                object_id: *cob.id(),
+
                type_name: typename.clone(),
+
                embeds: vec![],
+
                message: "commenting xyz.rad.issue".to_string(),
+
            },
+
        )
+
        .unwrap();
+

+
        let expected = get(&storage, &typename, object.id())
+
            .unwrap()
+
            .expect("BUG: cob was missing");
+

+
        assert_ne!(object, not_expected);
+
        assert_eq!(object, expected, "{object:#?} {expected:#?}");
+
    }
+

+
    #[test]
+
    fn traverse_cobs() {
+
        let storage = test::Storage::new();
+
        let neil_signer = gen::<MockSigner>(2);
+
        let neil = test::Person::new(&storage, "gaiman", *neil_signer.public_key()).unwrap();
+
        let terry_signer = gen::<MockSigner>(1);
+
        let terry = test::Person::new(&storage, "pratchett", *terry_signer.public_key()).unwrap();
+
        let proj = test::Project::new(&storage, "discworld", *terry_signer.public_key()).unwrap();
+
        let terry_proj = test::RemoteProject {
+
            project: proj.clone(),
+
            person: terry,
+
        };
+
        let neil_proj = test::RemoteProject {
+
            project: proj,
+
            person: neil,
+
        };
+
        let typename = "xyz.rad.issue".parse::<TypeName>().unwrap();
+
        let cob = create::<NonEmpty<Entry>, _, _>(
+
            &storage,
+
            &terry_signer,
+
            Some(terry_proj.project.content_id),
+
            vec![],
+
            terry_signer.public_key(),
+
            Create {
+
                contents: nonempty!(b"issue 1".to_vec()),
+
                type_name: typename.clone(),
+
                message: "creating xyz.rad.issue".to_string(),
+
                embeds: vec![],
+
                version: Version::default(),
+
            },
+
        )
+
        .unwrap();
+
        copy_to(
+
            storage.as_raw(),
+
            terry_signer.public_key(),
+
            &neil_proj,
+
            &typename,
+
            *cob.id(),
+
        )
+
        .unwrap();
+

+
        let Updated { object, .. } = update::<NonEmpty<Entry>, _, _>(
+
            &storage,
+
            &neil_signer,
+
            Some(neil_proj.project.content_id),
+
            vec![],
+
            neil_signer.public_key(),
+
            Update {
+
                changes: nonempty!(b"issue 2".to_vec()),
+
                object_id: *cob.id(),
+
                type_name: typename,
+
                embeds: vec![],
+
                message: "commenting on xyz.rad.issue".to_string(),
+
            },
+
        )
+
        .unwrap();
+

+
        let root = object.history.root().id;
+
        // traverse over the history and filter by changes that were only authorized by terry
+
        let contents = object
+
            .history()
+
            .traverse(Vec::new(), &[root], |mut acc, _, entry| {
+
                if entry.author() == terry_signer.public_key() {
+
                    acc.push(entry.contents().head.clone());
+
                }
+
                ControlFlow::Continue(acc)
+
            });
+

+
        assert_eq!(contents, vec![b"issue 1".to_vec()]);
+

+
        // traverse over the history and filter by changes that were only authorized by neil
+
        let contents = object
+
            .history()
+
            .traverse(Vec::new(), &[root], |mut acc, _, entry| {
+
                acc.push(entry.contents().head.clone());
+
                ControlFlow::Continue(acc)
+
            });
+

+
        assert_eq!(contents, vec![b"issue 1".to_vec(), b"issue 2".to_vec()]);
+
    }
+

+
    fn copy_to(
+
        repo: &git2::Repository,
+
        from: &PublicKey,
+
        to: &test::RemoteProject,
+
        typename: &TypeName,
+
        object: ObjectId,
+
    ) -> Result<(), git2::Error> {
+
        let original = {
+
            let name = format!("refs/rad/{from}/cobs/{typename}/{object}");
+
            let r = repo.find_reference(&name)?;
+
            r.target().unwrap()
+
        };
+

+
        let name = format!(
+
            "refs/rad/{}/cobs/{}/{}",
+
            to.identifier().to_path(),
+
            typename,
+
            object
+
        );
+
        repo.reference(&name, original, false, "copying object reference")?;
+
        Ok(())
+
    }
+

+
    fn gen<T: Arbitrary>(size: usize) -> T {
+
        let mut gen = qcheck::Gen::new(size);
+

+
        T::arbitrary(&mut gen)
+
    }
}

#[quickcheck]
@@ -297,32 +333,3 @@ fn invalid_parse_refstr(oid: Invalid<ObjectId>, typename: TypeName) {
        None
    );
}
-

-
fn gen<T: Arbitrary>(size: usize) -> T {
-
    let mut gen = qcheck::Gen::new(size);
-

-
    T::arbitrary(&mut gen)
-
}
-

-
fn copy_to(
-
    repo: &git2::Repository,
-
    from: &PublicKey,
-
    to: &test::RemoteProject,
-
    typename: &TypeName,
-
    object: ObjectId,
-
) -> Result<(), git2::Error> {
-
    let original = {
-
        let name = format!("refs/rad/{from}/cobs/{typename}/{object}");
-
        let r = repo.find_reference(&name)?;
-
        r.target().unwrap()
-
    };
-

-
    let name = format!(
-
        "refs/rad/{}/cobs/{}/{}",
-
        to.identifier().to_path(),
-
        typename,
-
        object
-
    );
-
    repo.reference(&name, original, false, "copying object reference")?;
-
    Ok(())
-
}
modified crates/radicle-cob/src/trailers.rs
@@ -1,6 +1,6 @@
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>

-
use git_ext::commit::trailers::{OwnedTrailer, Token, Trailer};
+
use metadata::commit::trailers::{OwnedTrailer, Token, Trailer};
use std::ops::Deref as _;

pub mod error {
@@ -12,21 +12,22 @@ pub mod error {
        WrongToken,
        #[error("no value for Rad-Resource")]
        NoValue,
-
        #[error("invalid git OID")]
-
        InvalidOid,
+
        /// Invalid object ID.
+
        #[error("invalid oid: {0}")]
+
        InvalidOid(#[from] radicle_oid::str::ParseOidError),
    }
}

/// Commit trailer for COB commits.
pub enum CommitTrailer {
    /// Points to the owning resource.
-
    Resource(git2::Oid),
+
    Resource(oid::Oid),
    /// Points to a related change.
-
    Related(git2::Oid),
+
    Related(oid::Oid),
}

impl CommitTrailer {
-
    pub fn oid(&self) -> git2::Oid {
+
    pub fn oid(&self) -> oid::Oid {
        match self {
            Self::Resource(oid) => *oid,
            Self::Related(oid) => *oid,
@@ -38,12 +39,11 @@ impl TryFrom<&Trailer<'_>> for CommitTrailer {
    type Error = error::InvalidResourceTrailer;

    fn try_from(Trailer { value, token }: &Trailer<'_>) -> Result<Self, Self::Error> {
-
        let ext_oid =
-
            git_ext::Oid::try_from(value.as_ref()).map_err(|_| Self::Error::InvalidOid)?;
+
        let oid = value.as_ref().parse::<oid::Oid>()?;
        if token.deref() == "Rad-Resource" {
-
            Ok(CommitTrailer::Resource(ext_oid.into()))
+
            Ok(CommitTrailer::Resource(oid))
        } else if token.deref() == "Rad-Related" {
-
            Ok(CommitTrailer::Related(ext_oid.into()))
+
            Ok(CommitTrailer::Related(oid))
        } else {
            Err(Self::Error::WrongToken)
        }
modified crates/radicle-cob/src/type_name.rs
@@ -1,8 +1,8 @@
// Copyright © 2022 The Radicle Link Contributors

-
use std::{fmt, str::FromStr};
+
use std::str::FromStr;

-
use git_ext::ref_format::{Component, RefString};
+
use fmt::{Component, RefString};
use serde::{Deserialize, Serialize};
use thiserror::Error;

@@ -24,8 +24,8 @@ impl TypeName {
    }
}

-
impl fmt::Display for TypeName {
-
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
impl std::fmt::Display for TypeName {
+
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(self.0.as_str())
    }
}
modified crates/radicle-crypto/Cargo.toml
@@ -25,7 +25,7 @@ multibase = { workspace = true }
qcheck = { workspace = true, optional = true }
git-ref-format-core = { workspace = true, optional = true }
radicle-ssh = { workspace = true, optional = true }
-
serde = { workspace = true, features = ["derive"] }
+
serde = { workspace = true, features = ["derive", "std"] }
signature = { workspace = true }
sqlite = { workspace = true, features = ["bundled"], optional = true }
ssh-key = { version = "0.6.3", default-features = false, features = ["std", "encryption", "getrandom"], optional = true }
modified crates/radicle-fetch/Cargo.toml
@@ -20,5 +20,6 @@ gix-transport = { version = "0.44.0", features = ["blocking-client"] }
log = { workspace = true, features = ["std"] }
nonempty = { workspace = true }
radicle = { workspace = true }
-
radicle-git-ext = { workspace = true, features = ["bstr"] }
+
radicle-oid = { workspace = true, features = ["gix"] }
+
radicle-git-ref-format = { workspace = true, features = ["bstr"] }
thiserror = { workspace = true }

\ No newline at end of file
modified crates/radicle-fetch/src/git.rs
@@ -3,21 +3,3 @@ pub(crate) mod packfile;
pub(crate) mod repository;

pub mod refs;
-

-
pub(crate) mod oid {
-
    //! Helper functions for converting to/from [`radicle::git::Oid`] and
-
    //! [`ObjectId`].
-

-
    use gix_hash::ObjectId;
-
    use radicle::git::Oid;
-

-
    /// Convert from an [`ObjectId`] to an [`Oid`].
-
    pub fn to_oid(oid: ObjectId) -> Oid {
-
        Oid::try_from(oid.as_bytes()).expect("invalid gix Oid")
-
    }
-

-
    /// Convert from an [`Oid`] to an [`ObjectId`].
-
    pub fn to_object_id(oid: Oid) -> ObjectId {
-
        ObjectId::from(gix_hash::oid::from_bytes_unchecked(oid.as_ref()))
-
    }
-
}
modified crates/radicle-fetch/src/git/mem.rs
@@ -1,6 +1,7 @@
use std::collections::HashMap;

-
use radicle::git::{Component, Oid, Qualified, RefString};
+
use radicle::git::fmt::{Component, Qualified, RefString};
+
use radicle::git::Oid;
use radicle::prelude::PublicKey;

use super::refs::{Applied, RefUpdate, Update};
modified crates/radicle-fetch/src/git/refs/update.rs
@@ -18,7 +18,8 @@
use std::collections::BTreeMap;

use either::Either;
-
use radicle::git::{Namespaced, Oid, Qualified};
+
use radicle::git::fmt::{Namespaced, Qualified};
+
use radicle::git::Oid;
use radicle::prelude::PublicKey;

pub use radicle::storage::RefUpdate;
modified crates/radicle-fetch/src/git/repository.rs
@@ -2,7 +2,11 @@ pub mod error;

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

use super::refs::{Applied, Policy, RefUpdate, Update};
@@ -48,7 +52,7 @@ pub fn contains(repo: &Repository, oid: Oid) -> Result<bool, error::Contains> {
/// - The object does not peel to a commit
/// - Attempting to find the object fails
fn find_and_peel(repo: &Repository, oid: Oid) -> Result<Oid, error::Ancestry> {
-
    match repo.backend.find_object(*oid, None) {
+
    match repo.backend.find_object(oid.into(), None) {
        Ok(object) => Ok(object
            .peel(git::raw::ObjectType::Commit)
            .map_err(|err| error::Ancestry::Peel { oid, err })?
@@ -80,7 +84,7 @@ pub fn ahead_behind(

    let (ahead, behind) = repo
        .backend
-
        .graph_ahead_behind(*new_commit, *old_commit)
+
        .graph_ahead_behind(new_commit.into(), old_commit.into())
        .map_err(|err| error::Ancestry::Check {
            old: old_commit,
            new: new_commit,
@@ -100,7 +104,7 @@ pub fn refname_to_id<'a, N>(repo: &Repository, refname: N) -> Result<Option<Oid>
where
    N: Into<Qualified<'a>>,
{
-
    use radicle::git::raw::ErrorCode::NotFound;
+
    use git::raw::ErrorCode::NotFound;

    let refname = refname.into();
    match repo.backend.refname_to_id(refname.as_ref()) {
@@ -170,7 +174,7 @@ fn direct<'a>(
        });
    };

-
    if prev == *target {
+
    if target == prev {
        // If the two objects are identical, their ancestry does not matter,
        // we can always skip the update.
        return Ok(RefUpdate::Skipped {
@@ -193,7 +197,7 @@ fn direct<'a>(

        let target = repo
            .backend
-
            .find_object(*target, ANY_KIND)
+
            .find_object(target.into(), ANY_KIND)
            .map_err(|err| error::Update::Ancestry(error::Ancestry::Object { oid: target, err }))?;

        match (prev.kind(), target.kind()) {
@@ -276,7 +280,7 @@ fn prune<'a>(
    name: Namespaced<'a>,
    prev: Either<Oid, Qualified<'a>>,
) -> Result<Updated<'a>, error::Update> {
-
    use radicle::git::raw::ObjectType;
+
    use git::raw::ObjectType;

    match find(repo, &name)? {
        Some(mut r) => {
modified crates/radicle-fetch/src/git/repository/error.rs
@@ -1,9 +1,13 @@
-
use radicle::git::{ext, raw, Namespaced, Oid, Qualified};
+
use radicle::git::{
+
    self,
+
    fmt::{Namespaced, Qualified},
+
    Oid,
+
};
use thiserror::Error;

#[derive(Debug, Error)]
#[error("could not open Git ODB")]
-
pub struct Contains(#[source] pub raw::Error);
+
pub struct Contains(#[source] pub git::raw::Error);

#[derive(Debug, Error)]
pub enum Ancestry {
@@ -14,19 +18,19 @@ pub enum Ancestry {
        old: Oid,
        new: Oid,
        #[source]
-
        err: raw::Error,
+
        err: git::raw::Error,
    },
    #[error("failed to peel object to commit {oid}: {err}")]
    Peel {
        oid: Oid,
        #[source]
-
        err: raw::Error,
+
        err: git::raw::Error,
    },
    #[error("failed to find object {oid}: {err}")]
    Object {
        oid: Oid,
        #[source]
-
        err: raw::Error,
+
        err: git::raw::Error,
    },
}

@@ -35,15 +39,15 @@ pub enum Ancestry {
pub struct Resolve {
    pub name: Qualified<'static>,
    #[source]
-
    pub err: raw::Error,
+
    pub err: git::raw::Error,
}

#[derive(Debug, Error)]
#[error("failed to scan for refs matching {pattern}")]
pub struct Scan {
-
    pub pattern: radicle::git::PatternString,
+
    pub pattern: radicle::git::fmt::refspec::PatternString,
    #[source]
-
    pub err: ext::Error,
+
    pub err: git::raw::Error,
}

#[derive(Debug, Error)]
@@ -55,19 +59,19 @@ pub enum Update {
        name: Namespaced<'static>,
        target: Oid,
        #[source]
-
        err: raw::Error,
+
        err: git::raw::Error,
    },
    #[error("failed to delete reference {name}")]
    Delete {
        name: Namespaced<'static>,
        #[source]
-
        err: raw::Error,
+
        err: git::raw::Error,
    },
    #[error("failed to find ref {name}")]
    Find {
        name: Namespaced<'static>,
        #[source]
-
        err: raw::Error,
+
        err: git::raw::Error,
    },
    #[error("non-fast-forward update of {name} (current: {cur}, new: {new})")]
    NonFF {
@@ -76,7 +80,7 @@ pub enum Update {
        cur: Oid,
    },
    #[error("failed to peel ref to object")]
-
    Peel(#[source] raw::Error),
+
    Peel(#[source] git::raw::Error),
    #[error(transparent)]
    Resolve(#[from] Resolve),

modified crates/radicle-fetch/src/refs.rs
@@ -1,7 +1,11 @@
use bstr::{BString, ByteSlice};
use either::Either;
use radicle::crypto::PublicKey;
-
use radicle::git::{self, Component, Namespaced, Oid, Qualified};
+
use radicle::git::{
+
    self,
+
    fmt::{Component, Namespaced, Qualified},
+
    Oid,
+
};
use thiserror::Error;

pub use radicle::git::refs::storage::Special;
@@ -26,7 +30,6 @@ pub enum Error {
pub(crate) fn unpack_ref<'a>(
    r: gix_protocol::handshake::Ref,
) -> Result<(ReceivedRefname<'a>, Oid), Error> {
-
    use crate::git::oid;
    use gix_protocol::handshake::Ref;

    match r {
@@ -43,7 +46,7 @@ pub(crate) fn unpack_ref<'a>(
            full_ref_name,
            object,
            ..
-
        } => ReceivedRefname::try_from(full_ref_name).map(|name| (name, oid::to_oid(object))),
+
        } => ReceivedRefname::try_from(full_ref_name).map(|name| (name, object.into())),
        Ref::Unborn { full_ref_name, .. } => {
            unreachable!("BUG: unborn ref {}", full_ref_name)
        }
modified crates/radicle-fetch/src/stage.rs
@@ -37,7 +37,7 @@ use either::Either;
use gix_protocol::handshake::Ref;
use nonempty::NonEmpty;
use radicle::crypto::PublicKey;
-
use radicle::git::{refname, Component, Namespaced, Qualified};
+
use radicle::git::fmt::{refname, Component, Namespaced, Qualified};
use radicle::storage::git::Repository;
use radicle::storage::refs::{RefsAt, Special};
use radicle::storage::ReadRepository;
@@ -52,7 +52,7 @@ use crate::{policy, refs};

pub mod error {
    use radicle::crypto::PublicKey;
-
    use radicle::git::RefString;
+
    use radicle::git::fmt::RefString;
    use thiserror::Error;

    use crate::transport::WantsHavesError;
modified crates/radicle-fetch/src/state.rs
@@ -3,7 +3,7 @@ use std::time::Instant;

use gix_protocol::handshake;
use radicle::crypto::PublicKey;
-
use radicle::git::{Oid, Qualified};
+
use radicle::git::{fmt::Qualified, Oid};
use radicle::identity::{Did, Doc, DocError};

use radicle::prelude::Verified;
modified crates/radicle-fetch/src/transport.rs
@@ -13,12 +13,11 @@ use gix_protocol::handshake;
use gix_transport::client;
use gix_transport::Protocol;
use gix_transport::Service;
+
use radicle::git::fmt::Qualified;
use radicle::git::Oid;
-
use radicle::git::Qualified;
use radicle::storage::git::Repository;
use thiserror::Error;

-
use crate::git::oid;
use crate::git::packfile::Keepfile;
use crate::git::repository;

@@ -163,7 +162,7 @@ where

            let idx = File::at(pack_path, gix_hash::Kind::Sha1)?;
            for oid in wants_haves.wants {
-
                if idx.lookup(oid::to_object_id(oid)).is_none() {
+
                if idx.lookup(oid).is_none() {
                    return Err(Error::NotFound(oid));
                }
            }
modified crates/radicle-fetch/src/transport/fetch.rs
@@ -9,7 +9,7 @@ use gix_protocol::fetch::negotiate::one_round::State;
use gix_protocol::handshake;
use gix_protocol::handshake::Ref;

-
use crate::git::{oid, packfile};
+
use crate::git::packfile;

use super::{agent_name, Connection, WantsHaves};

@@ -109,7 +109,7 @@ impl fetch::Negotiate for Negotiate {
    ) -> bool {
        let mut has_want = false;
        for oid in &self.wants_haves.wants {
-
            arguments.want(oid::to_object_id(*oid));
+
            arguments.want(oid);
            has_want = true;
        }
        has_want
@@ -126,7 +126,7 @@ impl fetch::Negotiate for Negotiate {
        _previous_response: Option<&fetch::Response>,
    ) -> Result<(fetch::negotiate::Round, bool), fetch::negotiate::Error> {
        for oid in &self.wants_haves.haves {
-
            arguments.have(oid::to_object_id(*oid));
+
            arguments.have(oid);
        }

        let round = fetch::negotiate::Round {
added crates/radicle-git-metadata/Cargo.toml
@@ -0,0 +1,13 @@
+
[package]
+
name = "radicle-git-metadata"
+
description = "Radicle structs that carry Git commit metadata"
+
homepage.workspace = true
+
repository.workspace = true
+
version = "0.1.0"
+
edition.workspace = true
+
license.workspace = true
+
keywords = ["radicle", "git", "metadata"]
+
rust-version.workspace = true
+

+
[dependencies]
+
thiserror = { workspace = true }

\ No newline at end of file
added crates/radicle-git-metadata/src/author.rs
@@ -0,0 +1,125 @@
+
use std::{
+
    fmt,
+
    num::ParseIntError,
+
    str::{self, FromStr},
+
};
+

+
use thiserror::Error;
+

+
/// The data for indicating authorship of an action within
+
/// [`crate::commit::CommitData`].
+
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
+
pub struct Author {
+
    /// Name corresponding to `user.name` in the git config.
+
    ///
+
    /// Note: this must not contain `<` or `>`.
+
    pub name: String,
+
    /// Email corresponding to `user.email` in the git config.
+
    ///
+
    /// Note: this must not contain `<` or `>`.
+
    pub email: String,
+
    /// The time of this author's action.
+
    pub time: Time,
+
}
+

+
/// The time of a [`Author`]'s action.
+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
+
pub struct Time {
+
    seconds: i64,
+
    offset: i32,
+
}
+

+
impl Time {
+
    pub fn new(seconds: i64, offset: i32) -> Self {
+
        Self { seconds, offset }
+
    }
+

+
    /// Return the time, in seconds, since the epoch.
+
    pub fn seconds(&self) -> i64 {
+
        self.seconds
+
    }
+

+
    /// Return the timezone offset, in minutes.
+
    pub fn offset(&self) -> i32 {
+
        self.offset
+
    }
+

+
    fn from_components<'a>(cs: &mut impl Iterator<Item = &'a str>) -> Result<Self, ParseError> {
+
        let offset = match cs.next() {
+
            None => Err(ParseError::Missing("offset")),
+
            Some(offset) => Self::parse_offset(offset).map_err(ParseError::Offset),
+
        }?;
+
        let time = match cs.next() {
+
            None => return Err(ParseError::Missing("time")),
+
            Some(time) => time.parse::<i64>().map_err(ParseError::Time)?,
+
        };
+
        Ok(Self::new(time, offset))
+
    }
+

+
    fn parse_offset(offset: &str) -> Result<i32, ParseIntError> {
+
        // The offset is in the form of timezone offset,
+
        // e.g. +0200, -0100.  This needs to be converted into
+
        // minutes. The first two digits in the offset are the
+
        // number of hours in the offset, while the latter two
+
        // digits are the number of minutes in the offset.
+
        let tz_offset = offset.parse::<i32>()?;
+
        let hours = tz_offset / 100;
+
        let minutes = tz_offset % 100;
+
        Ok(hours * 60 + minutes)
+
    }
+
}
+

+
impl FromStr for Time {
+
    type Err = ParseError;
+

+
    fn from_str(s: &str) -> Result<Self, Self::Err> {
+
        Self::from_components(&mut s.split(' ').rev())
+
    }
+
}
+

+
impl fmt::Display for Time {
+
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
        let sign = if self.offset.is_negative() { '-' } else { '+' };
+
        let hours = self.offset.abs() / 60;
+
        let minutes = self.offset.abs() % 60;
+
        write!(f, "{} {}{:0>2}{:0>2}", self.seconds, sign, hours, minutes)
+
    }
+
}
+

+
impl fmt::Display for Author {
+
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
        write!(f, "{} <{}> {}", self.name, self.email, self.time,)
+
    }
+
}
+

+
#[derive(Debug, Error)]
+
pub enum ParseError {
+
    #[error("missing '{0}' while parsing person signature")]
+
    Missing(&'static str),
+
    #[error("offset was incorrect format while parsing person signature")]
+
    Offset(#[source] ParseIntError),
+
    #[error("time was incorrect format while parsing person signature")]
+
    Time(#[source] ParseIntError),
+
}
+

+
impl FromStr for Author {
+
    type Err = ParseError;
+

+
    fn from_str(s: &str) -> Result<Self, Self::Err> {
+
        // Splitting the string in 4 subcomponents is expected to give back the
+
        // following iterator entries: timezone offset, time, email, and name
+
        let mut components = s.rsplitn(4, ' ');
+
        let time = Time::from_components(&mut components)?;
+
        let email = components
+
            .next()
+
            .ok_or(ParseError::Missing("email"))?
+
            .trim_matches(|c| c == '<' || c == '>')
+
            .to_owned();
+
        let name = components.next().ok_or(ParseError::Missing("name"))?;
+
        Ok(Self {
+
            name: name.to_owned(),
+
            email: email.to_owned(),
+
            time,
+
        })
+
    }
+
}
added crates/radicle-git-metadata/src/commit.rs
@@ -0,0 +1,179 @@
+
pub mod headers;
+
pub mod trailers;
+

+
use core::fmt;
+
use std::str;
+

+
use headers::{Headers, Signature};
+
use trailers::{OwnedTrailer, Trailer};
+

+
use crate::author::Author;
+

+
/// A git commit in its object description form, i.e. the output of
+
/// `git cat-file` for a commit object.
+
#[derive(Debug)]
+
pub struct CommitData<Tree, Parent> {
+
    tree: Tree,
+
    parents: Vec<Parent>,
+
    author: Author,
+
    committer: Author,
+
    headers: Headers,
+
    message: String,
+
    trailers: Vec<OwnedTrailer>,
+
}
+

+
impl<Tree, Parent> CommitData<Tree, Parent> {
+
    pub fn new<P, I, T>(
+
        tree: Tree,
+
        parents: P,
+
        author: Author,
+
        committer: Author,
+
        headers: Headers,
+
        message: String,
+
        trailers: I,
+
    ) -> Self
+
    where
+
        P: IntoIterator<Item = Parent>,
+
        I: IntoIterator<Item = T>,
+
        OwnedTrailer: From<T>,
+
    {
+
        let trailers = trailers.into_iter().map(OwnedTrailer::from).collect();
+
        let parents = parents.into_iter().collect();
+
        Self {
+
            tree,
+
            parents,
+
            author,
+
            committer,
+
            headers,
+
            message,
+
            trailers,
+
        }
+
    }
+

+
    /// The tree this commit points to.
+
    pub fn tree(&self) -> &Tree {
+
        &self.tree
+
    }
+

+
    /// The parents of this commit.
+
    pub fn parents(&self) -> impl Iterator<Item = Parent> + '_
+
    where
+
        Parent: Clone,
+
    {
+
        self.parents.iter().cloned()
+
    }
+

+
    /// The author of this commit, i.e. the header corresponding to `author`.
+
    pub fn author(&self) -> &Author {
+
        &self.author
+
    }
+

+
    /// The committer of this commit, i.e. the header corresponding to
+
    /// `committer`.
+
    pub fn committer(&self) -> &Author {
+
        &self.committer
+
    }
+

+
    /// The message body of this commit.
+
    pub fn message(&self) -> &str {
+
        &self.message
+
    }
+

+
    /// The [`Signature`]s found in this commit, i.e. the headers corresponding
+
    /// to `gpgsig`.
+
    pub fn signatures(&self) -> impl Iterator<Item = Signature> + '_ {
+
        self.headers.signatures()
+
    }
+

+
    /// The [`Headers`] found in this commit.
+
    ///
+
    /// Note: these do not include `tree`, `parent`, `author`, and `committer`.
+
    pub fn headers(&self) -> impl Iterator<Item = (&str, &str)> {
+
        self.headers.iter()
+
    }
+

+
    /// Iterate over the [`Headers`] values that match the provided `name`.
+
    pub fn values<'a>(&'a self, name: &'a str) -> impl Iterator<Item = &'a str> + 'a {
+
        self.headers.values(name)
+
    }
+

+
    /// Push a header to the end of the headers section.
+
    pub fn push_header(&mut self, name: &str, value: &str) {
+
        self.headers.push(name, value.trim());
+
    }
+

+
    pub fn trailers(&self) -> impl Iterator<Item = &OwnedTrailer> {
+
        self.trailers.iter()
+
    }
+

+
    /// Convert the `CommitData::tree` into a value of type `U`. The
+
    /// conversion function `f` can be fallible.
+
    ///
+
    /// For example, `map_tree` can be used to turn raw tree data into
+
    /// an `Oid` by writing it to a repository.
+
    pub fn map_tree<U, E, F>(self, f: F) -> Result<CommitData<U, Parent>, E>
+
    where
+
        F: FnOnce(Tree) -> Result<U, E>,
+
    {
+
        Ok(CommitData {
+
            tree: f(self.tree)?,
+
            parents: self.parents,
+
            author: self.author,
+
            committer: self.committer,
+
            headers: self.headers,
+
            message: self.message,
+
            trailers: self.trailers,
+
        })
+
    }
+

+
    /// Convert the [`CommitData::parents`] into a vector containing
+
    /// values of type `U`. The conversion function `f` can be
+
    /// fallible.
+
    ///
+
    /// For example, this can be used to resolve the object identifiers
+
    /// to their respective full commits.
+
    pub fn map_parents<U, E, F>(self, f: F) -> Result<CommitData<Tree, U>, E>
+
    where
+
        F: FnMut(Parent) -> Result<U, E>,
+
    {
+
        Ok(CommitData {
+
            tree: self.tree,
+
            parents: self
+
                .parents
+
                .into_iter()
+
                .map(f)
+
                .collect::<Result<Vec<_>, _>>()?,
+
            author: self.author,
+
            committer: self.committer,
+
            headers: self.headers,
+
            message: self.message,
+
            trailers: self.trailers,
+
        })
+
    }
+
}
+

+
impl<Tree: fmt::Display, Parent: fmt::Display> fmt::Display for CommitData<Tree, Parent> {
+
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+
        writeln!(f, "tree {}", self.tree)?;
+
        for parent in self.parents.iter() {
+
            writeln!(f, "parent {parent}")?;
+
        }
+
        writeln!(f, "author {}", self.author)?;
+
        writeln!(f, "committer {}", self.committer)?;
+

+
        for (name, value) in self.headers.iter() {
+
            writeln!(f, "{name} {}", value.replace('\n', "\n "))?;
+
        }
+
        writeln!(f)?;
+
        write!(f, "{}", self.message.trim())?;
+
        writeln!(f)?;
+

+
        if !self.trailers.is_empty() {
+
            writeln!(f)?;
+
        }
+
        for trailer in self.trailers.iter() {
+
            writeln!(f, "{}", Trailer::from(trailer).display(": "))?;
+
        }
+
        Ok(())
+
    }
+
}
added crates/radicle-git-metadata/src/commit/headers.rs
@@ -0,0 +1,168 @@
+
use core::fmt;
+
use std::borrow::Cow;
+

+
const BEGIN_SSH: &str = "-----BEGIN SSH SIGNATURE-----\n";
+
const BEGIN_PGP: &str = "-----BEGIN PGP SIGNATURE-----\n";
+

+
/// A collection of headers stored in [`super::CommitData`].
+
///
+
/// Note: these do not include `tree`, `parent`, `author`, and `committer`.
+
#[derive(Clone, Debug, Default)]
+
pub struct Headers(pub(super) Vec<(String, String)>);
+

+
/// A `gpgsig` signature stored in [`super::CommitData`].
+
#[derive(Debug)]
+
pub enum Signature<'a> {
+
    /// A PGP signature, i.e. starts with `-----BEGIN PGP SIGNATURE-----`.
+
    Pgp(Cow<'a, str>),
+
    /// A SSH signature, i.e. starts with `-----BEGIN SSH SIGNATURE-----`.
+
    Ssh(Cow<'a, str>),
+
}
+

+
impl<'a> Signature<'a> {
+
    fn from_str(s: &'a str) -> Result<Self, UnknownScheme> {
+
        if s.starts_with(BEGIN_SSH) {
+
            Ok(Signature::Ssh(Cow::Borrowed(s)))
+
        } else if s.starts_with(BEGIN_PGP) {
+
            Ok(Signature::Pgp(Cow::Borrowed(s)))
+
        } else {
+
            Err(UnknownScheme)
+
        }
+
    }
+
}
+

+
impl fmt::Display for Signature<'_> {
+
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+
        match self {
+
            Signature::Pgp(pgp) => f.write_str(pgp.as_ref()),
+
            Signature::Ssh(ssh) => f.write_str(ssh.as_ref()),
+
        }
+
    }
+
}
+

+
pub struct UnknownScheme;
+

+
impl Headers {
+
    pub fn new() -> Self {
+
        Headers(Vec::new())
+
    }
+

+
    pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
+
        self.0.iter().map(|(k, v)| (k.as_str(), v.as_str()))
+
    }
+

+
    pub fn values<'a>(&'a self, name: &'a str) -> impl Iterator<Item = &'a str> + 'a {
+
        self.iter()
+
            .filter_map(move |(k, v)| (k == name).then_some(v))
+
    }
+

+
    pub fn signatures(&self) -> impl Iterator<Item = Signature> + '_ {
+
        self.0.iter().filter_map(|(k, v)| {
+
            if k == "gpgsig" {
+
                Signature::from_str(v).ok()
+
            } else {
+
                None
+
            }
+
        })
+
    }
+

+
    /// Push a header to the end of the headers section.
+
    pub fn push(&mut self, name: &str, value: &str) {
+
        self.0.push((name.to_owned(), value.trim().to_owned()));
+
    }
+
}
+

+
#[derive(Debug, thiserror::Error)]
+
pub enum ParseError {
+
    #[error("missing tree")]
+
    MissingTree,
+
    #[error("invalid tree")]
+
    InvalidTree,
+
    #[error("invalid format")]
+
    InvalidFormat,
+
    #[error("invalid parent")]
+
    InvalidParent,
+
    #[error("invalid header")]
+
    InvalidHeader,
+
    #[error("invalid author")]
+
    InvalidAuthor,
+
    #[error("missing author")]
+
    MissingAuthor,
+
    #[error("invalid committer")]
+
    InvalidCommitter,
+
    #[error("missing committer")]
+
    MissingCommitter,
+
}
+

+
pub fn parse_commit_header<
+
    Tree: std::str::FromStr,
+
    Parent: std::str::FromStr,
+
    Signature: std::str::FromStr,
+
>(
+
    header: &str,
+
) -> Result<(Tree, Vec<Parent>, Signature, Signature, Headers), ParseError> {
+
    let mut lines = header.lines();
+

+
    let tree = match lines.next() {
+
        Some(tree) => tree
+
            .strip_prefix("tree ")
+
            .map(Tree::from_str)
+
            .transpose()
+
            .map_err(|_| ParseError::InvalidTree)?
+
            .ok_or(ParseError::MissingTree)?,
+
        None => return Err(ParseError::MissingTree),
+
    };
+

+
    let mut parents = Vec::new();
+
    let mut author: Option<Signature> = None;
+
    let mut committer: Option<Signature> = None;
+
    let mut headers = Headers::new();
+

+
    for line in lines {
+
        // Check if a signature is still being parsed
+
        if let Some(rest) = line.strip_prefix(' ') {
+
            let value: &mut String = headers
+
                .0
+
                .last_mut()
+
                .map(|(_, v)| v)
+
                .ok_or(ParseError::InvalidFormat)?;
+
            value.push('\n');
+
            value.push_str(rest);
+
            continue;
+
        }
+

+
        if let Some((name, value)) = line.split_once(' ') {
+
            match name {
+
                "parent" => parents.push(
+
                    value
+
                        .parse::<Parent>()
+
                        .map_err(|_| ParseError::InvalidParent)?,
+
                ),
+
                "author" => {
+
                    author = Some(
+
                        value
+
                            .parse::<Signature>()
+
                            .map_err(|_| ParseError::InvalidAuthor)?,
+
                    )
+
                }
+
                "committer" => {
+
                    committer = Some(
+
                        value
+
                            .parse::<Signature>()
+
                            .map_err(|_| ParseError::InvalidCommitter)?,
+
                    )
+
                }
+
                _ => headers.push(name, value),
+
            }
+
            continue;
+
        }
+
    }
+

+
    Ok((
+
        tree,
+
        parents,
+
        author.ok_or(ParseError::MissingAuthor)?,
+
        committer.ok_or(ParseError::MissingCommitter)?,
+
        headers,
+
    ))
+
}
added crates/radicle-git-metadata/src/commit/trailers.rs
@@ -0,0 +1,127 @@
+
use std::{borrow::Cow, fmt, ops::Deref};
+

+
pub trait Separator<'a> {
+
    fn sep_for(&self, token: &Token) -> &'a str;
+
}
+

+
impl<'a> Separator<'a> for &'a str {
+
    fn sep_for(&self, _: &Token) -> &'a str {
+
        self
+
    }
+
}
+

+
impl<'a, F> Separator<'a> for F
+
where
+
    F: Fn(&Token) -> &'a str,
+
{
+
    fn sep_for(&self, token: &Token) -> &'a str {
+
        self(token)
+
    }
+
}
+

+
#[derive(Debug, Clone, Eq, PartialEq)]
+
pub struct Token<'a>(&'a str);
+

+
impl Deref for Token<'_> {
+
    type Target = str;
+

+
    fn deref(&self) -> &Self::Target {
+
        self.0
+
    }
+
}
+

+
impl<'a> TryFrom<&'a str> for Token<'a> {
+
    type Error = &'static str;
+

+
    fn try_from(s: &'a str) -> Result<Self, Self::Error> {
+
        let is_token = s.chars().all(|c| c.is_alphanumeric() || c == '-');
+
        if is_token {
+
            Ok(Token(s))
+
        } else {
+
            Err("token contains invalid characters")
+
        }
+
    }
+
}
+

+
pub struct Display<'a> {
+
    trailer: &'a Trailer<'a>,
+
    separator: &'a str,
+
}
+

+
impl fmt::Display for Display<'_> {
+
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+
        write!(
+
            f,
+
            "{}{}{}",
+
            self.trailer.token.deref(),
+
            self.separator,
+
            self.trailer.value,
+
        )
+
    }
+
}
+

+
/// A trailer is a key/value pair found in the last paragraph of a Git
+
/// commit message, not including any patches or conflicts that may be
+
/// present.
+
#[derive(Debug, Clone, Eq, PartialEq)]
+
pub struct Trailer<'a> {
+
    pub token: Token<'a>,
+
    pub value: Cow<'a, str>,
+
}
+

+
impl<'a> Trailer<'a> {
+
    pub fn display(&'a self, separator: &'a str) -> Display<'a> {
+
        Display {
+
            trailer: self,
+
            separator,
+
        }
+
    }
+

+
    pub fn to_owned(&self) -> OwnedTrailer {
+
        OwnedTrailer::from(self)
+
    }
+
}
+

+
/// A version of the [`Trailer`] which owns its token and
+
/// value. Useful for when you need to carry trailers around in a long
+
/// lived data structure.
+
#[derive(Debug)]
+
pub struct OwnedTrailer {
+
    pub token: OwnedToken,
+
    pub value: String,
+
}
+

+
#[derive(Debug)]
+
pub struct OwnedToken(String);
+

+
impl Deref for OwnedToken {
+
    type Target = str;
+

+
    fn deref(&self) -> &Self::Target {
+
        &self.0
+
    }
+
}
+

+
impl<'a> From<&Trailer<'a>> for OwnedTrailer {
+
    fn from(t: &Trailer<'a>) -> Self {
+
        OwnedTrailer {
+
            token: OwnedToken(t.token.0.to_string()),
+
            value: t.value.to_string(),
+
        }
+
    }
+
}
+

+
impl<'a> From<Trailer<'a>> for OwnedTrailer {
+
    fn from(t: Trailer<'a>) -> Self {
+
        (&t).into()
+
    }
+
}
+

+
impl<'a> From<&'a OwnedTrailer> for Trailer<'a> {
+
    fn from(t: &'a OwnedTrailer) -> Self {
+
        Trailer {
+
            token: Token(t.token.0.as_str()),
+
            value: Cow::from(&t.value),
+
        }
+
    }
+
}
added crates/radicle-git-metadata/src/lib.rs
@@ -0,0 +1,2 @@
+
pub mod author;
+
pub mod commit;
added crates/radicle-git-ref-format/Cargo.toml
@@ -0,0 +1,18 @@
+
[package]
+
name = "radicle-git-ref-format"
+
description = "Radicle re-exports and macros for `git-ref-format-core`"
+
homepage.workspace = true
+
repository.workspace = true
+
version = "0.1.0"
+
edition.workspace = true
+
license.workspace = true
+
keywords = ["radicle", "git", "refname", "ref", "references"]
+
rust-version.workspace = true
+

+
[features]
+
macro = []
+
bstr = ["git-ref-format-core/bstr"]
+
serde = ["git-ref-format-core/serde"]
+

+
[dependencies]
+
git-ref-format-core.workspace = true
added crates/radicle-git-ref-format/src/lib.rs
@@ -0,0 +1,298 @@
+
#![no_std]
+

+
//! [`git_ref_format`]: https://crates.io/crates/git-ref-format
+
//! [`radicle-git-ext`]: https://crates.io/crates/radicle-git-ext
+
//!
+
//! This crate depends on and re-exports from [`git_ref_format_core`].
+
//!
+
//! ## Macros
+
//!
+
//! Instead of providing procedural macros, like [`git_ref_format`]
+
//! it just provides much simpler declarative macros, guarded by the feature
+
//! flag `macro`.
+
//!
+
//! ### Benefits
+
//!
+
//! - Does not depend on [`radicle-git-ext`].
+
//! - Does not pull in procedural macro dependencies.
+
//! - Has much smaller compile-time overhead than [`git_ref_format`].
+
//!
+
//! ### Drawback
+
//!
+
//! The main drawback is that the macros in this crate cannot provide compile
+
//! time validation of the argument. Thus, these macros must be used in
+
//! conjunction with testing: If all generated objects are used in tests, and
+
//! these tests are run, then the guarantees are equally strong. Consumers that
+
//! do not or cannot test their code should not use the macros then.
+

+
pub use git_ref_format_core::*;
+

+
/// Create a [`git_ref_format_core::RefString`] from a string literal.
+
///
+
/// Similar to [`core::debug_assert`], an optimized build will not validate
+
/// (but rather perform an unsafe conversion) unless `-C debug-assertions` is
+
/// passed to the compiler.
+
#[cfg(any(feature = "macro", test))]
+
#[macro_export]
+
macro_rules! refname {
+
    ($arg:literal) => {{
+
        use $crate::RefString;
+

+
        #[cfg(debug_assertions)]
+
        {
+
            RefString::try_from($arg).expect(core::concat!(
+
                "literal `",
+
                $arg,
+
                "` must be a valid reference name"
+
            ))
+
        }
+

+
        #[cfg(not(debug_assertions))]
+
        {
+
            extern crate alloc;
+

+
            use alloc::string::String;
+

+
            let s: String = $arg.to_owned();
+
            unsafe { core::mem::transmute::<_, RefString>(s) }
+
        }
+
    }};
+
}
+

+
/// Create a [`git_ref_format_core::Qualified`] from a string literal.
+
///
+
/// Similar to [`core::debug_assert`], an optimized build will not validate
+
/// (but rather perform an unsafe conversion) unless `-C debug-assertions` is
+
/// passed to the compiler.
+
#[cfg(any(feature = "macro", test))]
+
#[macro_export]
+
macro_rules! qualified {
+
    ($arg:literal) => {{
+
        use $crate::Qualified;
+

+
        #[cfg(debug_assertions)]
+
        {
+
            Qualified::from_refstr($crate::refname!($arg)).expect(core::concat!(
+
                "literal `",
+
                $arg,
+
                "` must be of the form 'refs/<category>/<name>'"
+
            ))
+
        }
+

+
        #[cfg(not(debug_assertions))]
+
        {
+
            extern crate alloc;
+

+
            use core::mem::transmute;
+

+
            use alloc::borrow::Cow;
+
            use alloc::string::String;
+

+
            use $crate::{RefStr, RefString};
+

+
            let s: String = $arg.to_owned();
+
            let refstring: RefString = unsafe { transmute(s) };
+
            let cow: Cow<'_, RefStr> = Cow::Owned(refstring);
+
            let qualified: Qualified = unsafe { transmute(cow) };
+

+
            qualified
+
        }
+
    }};
+
}
+

+
/// Create a [`git_ref_format_core::Component`] from a string literal.
+
///
+
/// Similar to [`core::debug_assert`], an optimized build will not validate
+
/// (but rather perform an unsafe conversion) unless `-C debug-assertions` is
+
/// passed to the compiler.
+
#[cfg(any(feature = "macro", test))]
+
#[macro_export]
+
macro_rules! component {
+
    ($arg:literal) => {{
+
        use $crate::Component;
+

+
        #[cfg(debug_assertions)]
+
        {
+
            Component::from_refstr($crate::refname!($arg)).expect(core::concat!(
+
                "literal `",
+
                $arg,
+
                "` must be a valid component (cannot contain '/')"
+
            ))
+
        }
+

+
        #[cfg(not(debug_assertions))]
+
        {
+
            extern crate alloc;
+

+
            use core::mem::transmute;
+

+
            use alloc::borrow::Cow;
+
            use alloc::string::String;
+

+
            use $crate::{RefStr, RefString};
+

+
            let s: String = $arg.to_owned();
+
            let refstring: RefString = unsafe { transmute(s) };
+
            let cow: Cow<'_, RefStr> = Cow::Owned(refstring);
+
            let component: Component = unsafe { transmute(cow) };
+

+
            component
+
        }
+
    }};
+
}
+

+
/// Create a [`git_ref_format_core::refspec::PatternString`] from a string literal.
+
///
+
/// Similar to [`core::debug_assert`], an optimized build will not validate
+
/// (but rather perform an unsafe conversion) unless `-C debug-assertions` is
+
/// passed to the compiler.
+
#[cfg(any(feature = "macro", test))]
+
#[macro_export]
+
macro_rules! pattern {
+
    ($arg:literal) => {{
+
        use $crate::refspec::PatternString;
+

+
        #[cfg(debug_assertions)]
+
        {
+
            PatternString::try_from($arg).expect(core::concat!(
+
                "literal `",
+
                $arg,
+
                "` must be a valid refspec pattern"
+
            ))
+
        }
+

+
        #[cfg(not(debug_assertions))]
+
        {
+
            extern crate alloc;
+

+
            use alloc::string::String;
+

+
            let s: String = $arg.to_owned();
+
            unsafe { core::mem::transmute::<_, PatternString>(s) }
+
        }
+
    }};
+
}
+

+
/// Create a [`git_ref_format_core::refspec::QualifiedPattern`] from a string literal.
+
///
+
/// Similar to [`core::debug_assert`], an optimized build will not validate
+
/// (but rather perform an unsafe conversion) unless `-C debug-assertions` is
+
/// passed to the compiler.
+
#[cfg(any(feature = "macro", test))]
+
#[macro_export]
+
macro_rules! qualified_pattern {
+
    ($arg:literal) => {{
+
        use $crate::refspec::QualifiedPattern;
+

+
        #[cfg(debug_assertions)]
+
        {
+
            use core::concat;
+

+
            use $crate::refspec::PatternStr;
+

+
            let pattern = PatternStr::try_from_str($arg).expect(concat!(
+
                "literal `",
+
                $arg,
+
                "` must be a valid refspec pattern"
+
            ));
+

+
            QualifiedPattern::from_patternstr(pattern).expect(concat!(
+
                "literal `",
+
                $arg,
+
                "` must be a valid qualified refspec pattern"
+
            ))
+
        }
+

+
        #[cfg(not(debug_assertions))]
+
        {
+
            extern crate alloc;
+

+
            use core::mem::transmute;
+

+
            use alloc::borrow::Cow;
+
            use alloc::string::String;
+

+
            use $crate::refspec::{PatternStr, PatternString};
+

+
            let s: String = $arg.to_owned();
+
            let pattern: PatternString = unsafe { transmute(s) };
+
            let cow: Cow<'_, PatternStr> = Cow::Owned(pattern);
+
            let qualified: QualifiedPattern = unsafe { transmute(cow) };
+

+
            qualified
+
        }
+
    }};
+
}
+

+
#[cfg(test)]
+
mod test {
+
    #[test]
+
    fn refname() {
+
        let _ = crate::refname!("refs/heads/main");
+
        let _ = crate::refname!("refs/tags/v1.0.0");
+
        let _ = crate::refname!("refs/remotes/origin/main");
+
        let _ = crate::refname!("a");
+
    }
+

+
    #[test]
+
    #[should_panic]
+
    fn refname_invalid() {
+
        let _ = crate::refname!("a~b");
+
    }
+

+
    #[test]
+
    fn qualified() {
+
        let _ = crate::qualified!("refs/heads/main");
+
        let _ = crate::qualified!("refs/tags/v1.0.0");
+
        let _ = crate::qualified!("refs/remotes/origin/main");
+
    }
+

+
    #[test]
+
    #[should_panic]
+
    fn qualified_invalid() {
+
        let _ = crate::qualified!("a");
+
    }
+

+
    #[test]
+
    fn component() {
+
        let _ = crate::component!("a");
+
    }
+

+
    #[test]
+
    #[should_panic]
+
    fn component_invalid() {
+
        let _ = crate::component!("a/b");
+
    }
+

+
    #[test]
+
    fn pattern() {
+
        let _ = crate::pattern!("refs/heads/main");
+
        let _ = crate::pattern!("refs/tags/v1.0.0");
+
        let _ = crate::pattern!("refs/remotes/origin/main");
+

+
        let _ = crate::pattern!("a");
+
        let _ = crate::pattern!("a/*");
+
        let _ = crate::pattern!("*");
+
        let _ = crate::pattern!("a/b*");
+
        let _ = crate::pattern!("a/b*/c");
+
        let _ = crate::pattern!("a/*/c");
+
    }
+

+
    #[test]
+
    fn qualified_pattern() {
+
        let _ = crate::qualified_pattern!("refs/heads/main");
+
        let _ = crate::qualified_pattern!("refs/tags/v1.0.0");
+
        let _ = crate::qualified_pattern!("refs/remotes/origin/main");
+

+
        let _ = crate::qualified_pattern!("refs/heads/main/*");
+
        let _ = crate::qualified_pattern!("refs/tags/v*");
+
        let _ = crate::qualified_pattern!("refs/remotes/origin/main");
+
        let _ = crate::qualified_pattern!("refs/remotes/origin/department/*/person");
+
    }
+

+
    #[test]
+
    #[should_panic]
+
    fn qualified_pattern_invalid() {
+
        let _ = crate::qualified_pattern!("a/*/b");
+
    }
+
}
modified crates/radicle-node/Cargo.toml
@@ -31,9 +31,6 @@ nonempty = { workspace = true, features = ["serialize"] }
qcheck = { workspace = true, optional = true }
radicle = { workspace = true, features = ["logger"] }
radicle-fetch = { workspace = true }
-
# N.b. this is required to use macros, even though it's re-exported
-
# through radicle
-
radicle-git-ext = { workspace = true, features = ["serde"] }
radicle-protocol = { workspace = true }
radicle-signals = { workspace = true }
sqlite = { workspace = true, features = ["bundled"] }
modified crates/radicle-node/src/test/handle.rs
@@ -4,7 +4,7 @@ use std::sync::{Arc, Mutex};
use std::time;

use radicle::crypto::PublicKey;
-
use radicle::git;
+
use radicle::git::Oid;
use radicle::storage::refs::RefsAt;

use crate::identity::RepoId;
@@ -108,7 +108,7 @@ impl radicle::node::Handle for Handle {

        Ok(RefsAt {
            remote: self.nid()?,
-
            at: git::raw::Oid::zero().into(),
+
            at: Oid::sha1_zero(),
        })
    }

modified crates/radicle-node/src/test/node.rs
@@ -16,7 +16,7 @@ use radicle::crypto::ssh::keystore::MemorySigner;
use radicle::crypto::test::signer::MockSigner;
use radicle::crypto::Signature;
use radicle::git;
-
use radicle::git::refname;
+
use radicle::git::fmt::refname;
use radicle::identity::{RepoId, Visibility};
use radicle::node::config::ConnectAddress;
use radicle::node::policy::store as policy;
@@ -363,7 +363,7 @@ impl<G: Signer<Signature> + cyphernet::Ecdh> NodeHandle<G> {
    /// of the new commit, and the reference will be updated.
    ///
    /// The `rad/sigrefs` are then updated to reflect the new change.
-
    pub fn commit_to(&self, rid: RepoId, refname: impl AsRef<git::RefStr>) {
+
    pub fn commit_to(&self, rid: RepoId, refname: impl AsRef<git::fmt::RefStr>) {
        use radicle::test::arbitrary;

        let refname = refname.as_ref();
@@ -516,14 +516,14 @@ impl<G: cyphernet::Ecdh<Pk = NodeId> + Signer<Signature> + Clone> Node<G> {
        );

        // Push local branches to storage.
-
        let mut refs = Vec::<(git::Qualified, git::Qualified)>::new();
+
        let mut refs = Vec::<(git::fmt::Qualified, git::fmt::Qualified)>::new();
        for branch in repo.branches(Some(git::raw::BranchType::Local)).unwrap() {
            let (branch, _) = branch.unwrap();
-
            let name = git::RefString::try_from(branch.name().unwrap().unwrap()).unwrap();
+
            let name = git::fmt::RefString::try_from(branch.name().unwrap().unwrap()).unwrap();

            refs.push((
-
                git::lit::refs_heads(&name).into(),
-
                git::lit::refs_heads(&name).into(),
+
                git::fmt::lit::refs_heads(&name).into(),
+
                git::fmt::lit::refs_heads(&name).into(),
            ));
        }
        git::push(repo, "rad", refs.iter().map(|(a, b)| (a, b))).unwrap();
modified crates/radicle-node/src/test/peer.rs
@@ -138,7 +138,7 @@ impl<G: crypto::signature::Signer<crypto::Signature>> Peer<Storage, G> {
            &repo,
            name.try_into().unwrap(),
            description,
-
            radicle::git::refname!("master"),
+
            radicle::git::fmt::refname!("master"),
            Visibility::default(),
            self.signer(),
            self.storage(),
modified crates/radicle-node/src/tests.rs
@@ -1567,7 +1567,7 @@ fn test_queued_fetch_from_ann_same_rid() {
    let refname = carol
        .id()
        .to_namespace()
-
        .join(git::refname!("refs/sigrefs"));
+
        .join(git::fmt::refname!("refs/sigrefs"));

    // Finish the 1st fetch.
    // Ensure the ref is in the storage and cache.
@@ -1749,7 +1749,7 @@ fn test_init_and_seed() {
        &repo,
        "alice".try_into().unwrap(),
        "alice's repo",
-
        git::refname!("master"),
+
        git::fmt::refname!("master"),
        Visibility::default(),
        alice.signer(),
        alice.storage(),
modified crates/radicle-node/src/tests/e2e.rs
@@ -3,6 +3,7 @@ use std::{collections::HashSet, thread, time};
use radicle::cob::Title;
use test_log::test;

+
use radicle::git::raw::ErrorExt as _;
use radicle::node::device::Device;
use radicle::node::policy::Scope;
use radicle::node::Event;
@@ -248,7 +249,7 @@ fn test_replication_ref_in_sigrefs() {
    bob.storage
        .repository_mut(acme)
        .unwrap()
-
        .reference(&bob.id, &git::qualified!("refs/heads/master"))
+
        .reference(&bob.id, &git::fmt::qualified!("refs/heads/master"))
        .unwrap()
        .delete()
        .unwrap();
@@ -271,7 +272,7 @@ fn test_replication_ref_in_sigrefs() {
            .storage
            .repository(acme)
            .unwrap()
-
            .reference(&bob.id, &git::qualified!("refs/heads/master"))
+
            .reference(&bob.id, &git::fmt::qualified!("refs/heads/master"))
            .is_ok(),
        "refs/namespaces/{}/refs/heads/master does not exist",
        bob.id
@@ -292,8 +293,8 @@ fn test_replication_invalid() {
    // Create some unsigned refs for Carol in Bob's storage.
    repo.raw()
        .reference(
-
            &git::qualified!("refs/heads/carol").with_namespace(carol.public_key().into()),
-
            *head,
+
            &git::fmt::qualified!("refs/heads/carol").with_namespace(carol.public_key().into()),
+
            head.into(),
            true,
            &String::default(),
        )
@@ -579,7 +580,7 @@ fn test_clone() {
        .canonical_head()
        .unwrap();

-
    assert_eq!(oid, *canonical);
+
    assert_eq!(canonical, oid);

    // Make sure that bob has refs/rad/id set
    assert!(bob
@@ -1193,7 +1194,6 @@ fn missing_default_branch() {

#[test]
fn missing_delegate_default_branch() {
-
    use radicle::git::raw;
    use radicle::identity::Identity;
    use radicle::storage::git::Repository;
    let tmp = tempfile::tempdir().unwrap();
@@ -1238,7 +1238,7 @@ fn missing_delegate_default_branch() {
        );
        assert!(matches!(
            default_branch,
-
            Err(radicle::git::Error::Git(e)) if e.code() == raw::ErrorCode::NotFound
+
            Err(e) if e.is_not_found()
        ));
    };

@@ -1528,10 +1528,10 @@ fn test_fetch_emits_canonical_ref_update() {
    let result = bob.handle.fetch(rid, alice.id, DEFAULT_TIMEOUT).unwrap();
    assert!(result.is_success());

-
    let default_branch: git::Qualified = {
+
    let default_branch: git::fmt::Qualified = {
        let repo = alice.storage.repository(rid).unwrap();
        let proj = repo.project().unwrap();
-
        git::lit::refs_heads(proj.default_branch()).into()
+
        git::fmt::lit::refs_heads(proj.default_branch()).into()
    };
    alice.commit_to(rid, &default_branch);

modified crates/radicle-node/src/worker/fetch.rs
@@ -196,7 +196,7 @@ fn notify(
                // for sigref verification.
                continue;
            }
-
            if let Some(rest) = r.strip_prefix(git::refname!("refs/heads/patches")) {
+
            if let Some(rest) = r.strip_prefix(git::fmt::refname!("refs/heads/patches")) {
                if radicle::cob::ObjectId::from_str(rest.as_str()).is_ok() {
                    // Don't notify about patch branches, since we already get
                    // notifications about patch updates.
@@ -400,7 +400,7 @@ fn set_canonical_refs(
                let oid = object.id();
                if let Err(e) = repo.backend.reference(
                    refname.clone().as_str(),
-
                    *oid,
+
                    oid.into(),
                    true,
                    "set-canonical-reference from fetch (radicle)",
                ) {
added crates/radicle-oid/Cargo.toml
@@ -0,0 +1,31 @@
+
[package]
+
name = "radicle-oid"
+
description = "Radicle representation of Git object identifiers"
+
homepage.workspace = true
+
repository.workspace = true
+
version = "0.1.0"
+
edition.workspace = true
+
license.workspace = true
+
keywords = ["radicle", "git", "oid"]
+
rust-version.workspace = true
+

+
# For documentation of features refer to the module documentation in `./lib.rs`
+
[features]
+
default = ["sha1", "std"]
+
gix = ["dep:gix-hash"]
+
std = []
+
sha1 = []
+

+
[dependencies]
+
git2 = { workspace = true, optional = true, default-features = false }
+
gix-hash = { version = "0.15.1", optional = true, default-features = false }
+
qcheck = { workspace = true, optional = true, default-features = false }
+
radicle-git-ref-format = { workspace = true, optional = true, default-features = false }
+
schemars = { workspace = true, optional = true, default-features = false }
+
serde = { workspace = true, optional = true, default-features = false }
+

+
[dev-dependencies]
+
git2 = { workspace = true }
+
gix-hash = { version = "0.15.1" }
+
qcheck = { workspace = true }
+
qcheck-macros = { workspace = true }

\ No newline at end of file
added crates/radicle-oid/src/lib.rs
@@ -0,0 +1,575 @@
+
#![no_std]
+

+
//! This is a `no_std` crate which carries the struct [`Oid`] that represents
+
//! Git object identifiers. Currently, only SHA-1 digests are supported.
+
//!
+
//! # Feature Flags
+
//!
+
//! The default features are `sha1` and `std`.
+
//!
+
//! ## `sha1`
+
//!
+
//! Enabled by default, since SHA-1 is commonly used. Currently, this feature is
+
//! also *required* to build the crate. In the future, after support for other
+
//! hashes is added, it might become possible to build the crate without support
+
//! for SHA-1.
+
//!
+
//! ## `std`
+
//!
+
//! [`Hash`]: ::doc_std::hash::Hash
+
//!
+
//! Enabled by default, since it is expected that most dependents will use the
+
//! standard library.
+
//!
+
//! Provides an implementation of [`Hash`].
+
//!
+
//! ## `git2`
+
//!
+
//! [`git2::Oid`]: ::git2::Oid
+
//!
+
//! Provides conversions to/from [`git2::Oid`].
+
//!
+
//! Note that as of version 0.19.0,
+
//!
+
//! ## `gix`
+
//!
+
//! [`ObjectId`]: ::gix_hash::ObjectId
+
//!
+
//! Provides conversions to/from [`ObjectId`].
+
//!
+
//! ## `schemars`
+
//!
+
//! [`JsonSchema`]: ::schemars::JsonSchema
+
//!
+
//! Provides an implementation of [`JsonSchema`].
+
//!
+
//! ## `serde`
+
//!
+
//! [`Serialize`]: ::serde::ser::Serialize
+
//! [`Deserialize`]: ::serde::de::Deserialize
+
//!
+
//! Provides implementations of [`Serialize`] and [`Deserialize`].
+
//!
+
//! ## `qcheck`
+
//!
+
//! [`qcheck::Arbitrary`]: ::qcheck::Arbitrary
+
//!
+
//! Provides an implementation of [`qcheck::Arbitrary`].
+
//!
+
//! ## `radicle-git-ref-format`
+
//!
+
//! [`radicle_git_ref_format::Component`]: ::radicle_git_ref_format::Component
+
//! [`radicle_git_ref_format::RefString`]: ::radicle_git_ref_format::RefString
+
//!
+
//! Conversion to [`radicle_git_ref_format::Component`]
+
//! (and also [`radicle_git_ref_format::RefString`]).
+

+
#[cfg(doc)]
+
extern crate std as doc_std;
+

+
extern crate alloc;
+

+
// Remove this once other hashes (e.g., SHA-256, and potentially others)
+
// are supported, and this crate can build without [`Oid::Sha1`].
+
#[cfg(not(feature = "sha1"))]
+
compile_error!("The `sha1` feature is required.");
+

+
const SHA1_DIGEST_LEN: usize = 20;
+

+
#[derive(PartialEq, Eq, Ord, PartialOrd, Clone, Copy)]
+
#[non_exhaustive]
+
pub enum Oid {
+
    Sha1([u8; SHA1_DIGEST_LEN]),
+
}
+

+
/// Conversions to/from SHA-1.
+
// Note that we deliberately do not implement `From<[u8; 20]>` and `Into<[u8; 20]>`,
+
// for forwards compatibility: What if another hash with digests of the same
+
// length becomes popular?
+
impl Oid {
+
    pub fn from_sha1(digest: [u8; SHA1_DIGEST_LEN]) -> Self {
+
        Self::Sha1(digest)
+
    }
+

+
    pub fn into_sha1(&self) -> Option<[u8; SHA1_DIGEST_LEN]> {
+
        match self {
+
            Oid::Sha1(digest) => Some(*digest),
+
        }
+
    }
+

+
    pub fn sha1_zero() -> Self {
+
        Self::Sha1([0u8; SHA1_DIGEST_LEN])
+
    }
+
}
+

+
/// Interaction with zero.
+
impl Oid {
+
    /// Test whether all bytes in this object identifier are zero.
+
    /// See also [`::git2::Oid::is_zero`].
+
    pub fn is_zero(&self) -> bool {
+
        match self {
+
            Oid::Sha1(ref array) => array.iter().all(|b| *b == 0),
+
        }
+
    }
+
}
+

+
impl AsRef<[u8]> for Oid {
+
    fn as_ref(&self) -> &[u8] {
+
        match self {
+
            Oid::Sha1(ref array) => array,
+
        }
+
    }
+
}
+

+
impl From<Oid> for alloc::boxed::Box<[u8]> {
+
    fn from(oid: Oid) -> Self {
+
        match oid {
+
            Oid::Sha1(array) => alloc::boxed::Box::new(array),
+
        }
+
    }
+
}
+

+
pub mod str {
+
    use super::{Oid, SHA1_DIGEST_LEN};
+
    use core::str;
+

+
    /// Length of the string representation of a SHA-1 digest in hexadecimal notation.
+
    pub(super) const SHA1_DIGEST_STR_LEN: usize = SHA1_DIGEST_LEN * 2;
+

+
    impl str::FromStr for Oid {
+
        type Err = error::ParseOidError;
+

+
        fn from_str(s: &str) -> Result<Self, Self::Err> {
+
            use error::ParseOidError::*;
+

+
            let len = s.len();
+
            if len != SHA1_DIGEST_STR_LEN {
+
                return Err(Len(len));
+
            }
+

+
            let mut bytes = [0u8; SHA1_DIGEST_LEN];
+
            for i in 0..SHA1_DIGEST_LEN {
+
                bytes[i] = u8::from_str_radix(&s[i * 2..=i * 2 + 1], 16)
+
                    .map_err(|source| At { index: i, source })?;
+
            }
+

+
            Ok(Self::Sha1(bytes))
+
        }
+
    }
+

+
    pub mod error {
+
        use core::{fmt, num};
+

+
        use super::SHA1_DIGEST_STR_LEN;
+

+
        pub enum ParseOidError {
+
            Len(usize),
+
            At {
+
                index: usize,
+
                source: num::ParseIntError,
+
            },
+
        }
+

+
        impl fmt::Display for ParseOidError {
+
            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
                use ParseOidError::*;
+
                match self {
+
                    Len(len) => {
+
                        write!(f, "invalid length (have {len}, want {SHA1_DIGEST_STR_LEN})")
+
                    }
+
                    At { index, source } => write!(
+
                        f,
+
                        "parse error at byte {index} (characters {} and {}): {source}",
+
                        index * 2,
+
                        index * 2 + 1
+
                    ),
+
                }
+
            }
+
        }
+

+
        impl fmt::Debug for ParseOidError {
+
            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
                fmt::Display::fmt(self, f)
+
            }
+
        }
+

+
        impl core::error::Error for ParseOidError {
+
            fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
+
                match self {
+
                    ParseOidError::At { source, .. } => Some(source),
+
                    _ => None,
+
                }
+
            }
+
        }
+
    }
+

+
    pub use error::ParseOidError;
+

+
    #[cfg(test)]
+
    mod test {
+
        use super::*;
+
        use alloc::string::ToString;
+
        use qcheck_macros::quickcheck;
+

+
        #[test]
+
        fn fixture() {
+
            assert_eq!(
+
                "123456789abcdef0123456789abcdef012345678"
+
                    .parse::<Oid>()
+
                    .unwrap(),
+
                Oid::from_sha1([
+
                    0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a,
+
                    0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78,
+
                ])
+
            );
+
        }
+

+
        #[test]
+
        fn zero() {
+
            assert_eq!(
+
                "0000000000000000000000000000000000000000"
+
                    .parse::<Oid>()
+
                    .unwrap(),
+
                Oid::sha1_zero()
+
            );
+
        }
+

+
        #[quickcheck]
+
        fn git2_roundtrip(oid: Oid) {
+
            let other = git2::Oid::from(oid);
+
            let other = other.to_string();
+
            let other = other.parse::<Oid>().unwrap();
+
            assert_eq!(oid, other);
+
        }
+

+
        #[quickcheck]
+
        fn gix_roundrip(oid: Oid) {
+
            let other = gix_hash::ObjectId::from(oid);
+
            let other = other.to_string();
+
            let other = other.parse::<Oid>().unwrap();
+
            assert_eq!(oid, other);
+
        }
+
    }
+
}
+

+
mod fmt {
+
    use alloc::format;
+
    use core::fmt;
+

+
    use super::Oid;
+

+
    impl fmt::Display for Oid {
+
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
            match self {
+
                Oid::Sha1(digest) =>
+
                // SAFETY (for all 20 blocks below): The length of `digest` is
+
                // known to be `SHA1_DIGEST_LEN`, which is 20.
+
                // The indices below are manually verified to not be out of bounds.
+
                format!(
+
                    "{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
+
                    unsafe { digest.get_unchecked(0) },
+
                    unsafe { digest.get_unchecked(1) },
+
                    unsafe { digest.get_unchecked(2) },
+
                    unsafe { digest.get_unchecked(3) },
+
                    unsafe { digest.get_unchecked(4) },
+
                    unsafe { digest.get_unchecked(5) },
+
                    unsafe { digest.get_unchecked(6) },
+
                    unsafe { digest.get_unchecked(7) },
+
                    unsafe { digest.get_unchecked(8) },
+
                    unsafe { digest.get_unchecked(9) },
+
                    unsafe { digest.get_unchecked(10) },
+
                    unsafe { digest.get_unchecked(11) },
+
                    unsafe { digest.get_unchecked(12) },
+
                    unsafe { digest.get_unchecked(13) },
+
                    unsafe { digest.get_unchecked(14) },
+
                    unsafe { digest.get_unchecked(15) },
+
                    unsafe { digest.get_unchecked(16) },
+
                    unsafe { digest.get_unchecked(17) },
+
                    unsafe { digest.get_unchecked(18) },
+
                    unsafe { digest.get_unchecked(19) },
+
                ).fmt(f)
+
            }
+
        }
+
    }
+

+
    impl fmt::Debug for Oid {
+
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
            fmt::Display::fmt(self, f)
+
        }
+
    }
+

+
    #[cfg(test)]
+
    mod test {
+
        use super::*;
+
        use alloc::string::ToString;
+
        use qcheck_macros::quickcheck;
+

+
        #[test]
+
        fn fixture() {
+
            assert_eq!(
+
                Oid::from_sha1([
+
                    0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a,
+
                    0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78,
+
                ])
+
                .to_string(),
+
                "123456789abcdef0123456789abcdef012345678"
+
            );
+
        }
+

+
        #[test]
+
        fn zero() {
+
            assert_eq!(
+
                Oid::sha1_zero().to_string(),
+
                "0000000000000000000000000000000000000000"
+
            );
+
        }
+

+
        #[quickcheck]
+
        fn git2(oid: Oid) {
+
            assert_eq!(oid.to_string(), git2::Oid::from(oid).to_string());
+
        }
+

+
        #[quickcheck]
+
        fn gix(oid: Oid) {
+
            assert_eq!(oid.to_string(), gix_hash::ObjectId::from(oid).to_string());
+
        }
+
    }
+
}
+

+
#[cfg(feature = "std")]
+
mod std {
+
    extern crate std;
+

+
    use super::Oid;
+

+
    mod hash {
+
        use std::hash;
+

+
        use super::*;
+

+
        #[allow(clippy::derived_hash_with_manual_eq)]
+
        impl hash::Hash for Oid {
+
            fn hash<H: hash::Hasher>(&self, state: &mut H) {
+
                state.write(self.as_ref());
+
            }
+
        }
+
    }
+
}
+

+
#[cfg(any(feature = "gix", test))]
+
mod gix {
+
    use gix_hash::ObjectId as Other;
+

+
    use super::Oid;
+

+
    impl From<Other> for Oid {
+
        fn from(other: Other) -> Self {
+
            match other {
+
                Other::Sha1(digest) => Self::Sha1(digest),
+
            }
+
        }
+
    }
+

+
    impl From<Oid> for Other {
+
        fn from(oid: Oid) -> Other {
+
            match oid {
+
                Oid::Sha1(digest) => Other::Sha1(digest),
+
            }
+
        }
+
    }
+

+
    impl core::cmp::PartialEq<Other> for Oid {
+
        fn eq(&self, other: &Other) -> bool {
+
            match (self, other) {
+
                (Oid::Sha1(a), Other::Sha1(b)) => a == b,
+
            }
+
        }
+
    }
+

+
    impl AsRef<gix_hash::oid> for Oid {
+
        fn as_ref(&self) -> &gix_hash::oid {
+
            match self {
+
                Oid::Sha1(digest) => gix_hash::oid::from_bytes_unchecked(digest),
+
            }
+
        }
+
    }
+

+
    #[cfg(test)]
+
    mod test {
+
        use super::*;
+
        use gix_hash::Kind;
+

+
        #[test]
+
        fn zero() {
+
            assert!(Oid::sha1_zero() == Other::null(Kind::Sha1));
+
        }
+
    }
+
}
+

+
#[cfg(any(feature = "git2", test))]
+
mod git2 {
+
    use ::git2::Oid as Other;
+

+
    use super::*;
+

+
    const EXPECT: &str = "git2::Oid must be exactly 20 bytes long";
+

+
    impl From<Other> for Oid {
+
        fn from(other: Other) -> Self {
+
            Self::Sha1(other.as_bytes().try_into().expect(EXPECT))
+
        }
+
    }
+

+
    impl From<Oid> for Other {
+
        fn from(oid: Oid) -> Self {
+
            match oid {
+
                Oid::Sha1(array) => Other::from_bytes(&array).expect(EXPECT),
+
            }
+
        }
+
    }
+

+
    impl From<&Oid> for Other {
+
        fn from(oid: &Oid) -> Self {
+
            match oid {
+
                Oid::Sha1(array) => Other::from_bytes(array).expect(EXPECT),
+
            }
+
        }
+
    }
+

+
    impl core::cmp::PartialEq<Other> for Oid {
+
        fn eq(&self, other: &Other) -> bool {
+
            other.as_bytes() == AsRef::<[u8]>::as_ref(&self)
+
        }
+
    }
+

+
    #[cfg(test)]
+
    mod test {
+
        use super::*;
+

+
        #[test]
+
        fn zero() {
+
            assert!(Oid::sha1_zero() == Other::zero());
+
        }
+
    }
+
}
+

+
#[cfg(any(test, feature = "qcheck"))]
+
mod test {
+
    mod qcheck {
+
        use ::qcheck::{Arbitrary, Gen};
+

+
        use crate::*;
+

+
        impl Arbitrary for Oid {
+
            fn arbitrary(g: &mut Gen) -> Self {
+
                let slice = [0u8; SHA1_DIGEST_LEN];
+
                g.fill(slice);
+
                Self::Sha1(slice)
+
            }
+
        }
+
    }
+
}
+

+
#[cfg(feature = "serde")]
+
mod serde {
+
    mod ser {
+
        use ::serde::ser;
+

+
        use crate::*;
+

+
        impl ser::Serialize for Oid {
+
            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+
            where
+
                S: ser::Serializer,
+
            {
+
                serializer.collect_str(self)
+
            }
+
        }
+
    }
+

+
    mod de {
+
        use core::fmt;
+

+
        use ::serde::de;
+

+
        use crate::*;
+

+
        impl<'de> de::Deserialize<'de> for Oid {
+
            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+
            where
+
                D: de::Deserializer<'de>,
+
            {
+
                struct OidVisitor;
+

+
                impl<'de> de::Visitor<'de> for OidVisitor {
+
                    type Value = Oid;
+

+
                    fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
+
                        use crate::str::SHA1_DIGEST_STR_LEN;
+
                        write!(f, "a Git object identifier (SHA-1 digest in hexadecimal notation; {SHA1_DIGEST_STR_LEN} characters; {SHA1_DIGEST_LEN} bytes)")
+
                    }
+

+
                    fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
+
                    where
+
                        E: de::Error,
+
                    {
+
                        s.parse().map_err(de::Error::custom)
+
                    }
+
                }
+

+
                deserializer.deserialize_str(OidVisitor)
+
            }
+
        }
+
    }
+
}
+

+
#[cfg(feature = "radicle-git-ref-format")]
+
mod radicle_git_ref_format {
+
    use ::radicle_git_ref_format::{Component, RefString};
+

+
    use super::*;
+

+
    impl From<&Oid> for Component<'_> {
+
        fn from(id: &Oid) -> Self {
+
            Component::from_refstr(RefString::from(id))
+
                .expect("Git object identifiers are valid component strings")
+
        }
+
    }
+

+
    impl From<&Oid> for RefString {
+
        fn from(id: &Oid) -> Self {
+
            RefString::try_from(alloc::format!("{id}"))
+
                .expect("Git object identifiers are valid reference strings")
+
        }
+
    }
+
}
+

+
#[cfg(feature = "schemars")]
+
mod schemars {
+
    use alloc::{borrow::Cow, format};
+

+
    use ::schemars::{json_schema, JsonSchema, Schema, SchemaGenerator};
+

+
    use super::Oid;
+

+
    impl JsonSchema for Oid {
+
        fn schema_name() -> Cow<'static, str> {
+
            "Oid".into()
+
        }
+

+
        fn schema_id() -> Cow<'static, str> {
+
            concat!(module_path!(), "::Oid").into()
+
        }
+

+
        fn json_schema(_: &mut SchemaGenerator) -> Schema {
+
            use crate::{str::SHA1_DIGEST_STR_LEN, SHA1_DIGEST_LEN};
+
            json_schema!({
+
                "description": format!("A Git object identifier (SHA-1 digest in hexadecimal notation; {SHA1_DIGEST_STR_LEN} characters; {SHA1_DIGEST_LEN} bytes)"),
+
                "type": "string",
+
                "maxLength": SHA1_DIGEST_STR_LEN,
+
                "minLength": SHA1_DIGEST_STR_LEN,
+
                "pattern":  format!("^[0-9a-fA-F]{{{SHA1_DIGEST_STR_LEN}}}$"),
+
            })
+
        }
+
    }
+
}
modified crates/radicle-protocol/Cargo.toml
@@ -23,7 +23,6 @@ nonempty = { workspace = true, features = ["serialize"] }
qcheck = { workspace = true, optional = true }
radicle = { workspace = true, features = ["logger"] }
radicle-fetch = { workspace = true }
-
radicle-git-ext = { workspace = true, features = ["serde"] }
sqlite = { workspace = true, features = ["bundled"] }
scrypt = { version = "0.11.0", default-features = false }
serde = { workspace = true, features = ["derive"] }
modified crates/radicle-protocol/src/service.rs
@@ -186,8 +186,6 @@ pub enum Error {
    #[error(transparent)]
    Git(#[from] radicle::git::raw::Error),
    #[error(transparent)]
-
    GitExt(#[from] radicle::git::ext::Error),
-
    #[error(transparent)]
    Storage(#[from] storage::Error),
    #[error(transparent)]
    Gossip(#[from] gossip::Error),
modified crates/radicle-protocol/src/wire.rs
@@ -20,6 +20,7 @@ use cyphernet::addr::tor;
use radicle::crypto::{PublicKey, Signature, Unverified};
use radicle::git;
use radicle::git::fmt;
+
use radicle::git::raw;
use radicle::identity::RepoId;
use radicle::node;
use radicle::node::Alias;
@@ -285,7 +286,7 @@ where
    }
}

-
impl Encode for git::RefString {
+
impl Encode for git::fmt::RefString {
    fn encode(&self, buf: &mut impl BufMut) {
        self.as_str().encode(buf)
    }
@@ -300,7 +301,8 @@ impl Encode for Signature {
impl Encode for git::Oid {
    fn encode(&self, buf: &mut impl BufMut) {
        // Nb. We use length-encoding here to support future SHA-2 object ids.
-
        self.as_bytes().encode(buf)
+
        let bytes: &[u8] = self.as_ref();
+
        bytes.encode(buf)
    }
}

@@ -321,7 +323,7 @@ impl Decode for Refs {

        for _ in 0..len {
            let name = String::decode(buf)?;
-
            let name = git::RefString::try_from(name).map_err(Invalid::from)?;
+
            let name = git::fmt::RefString::try_from(name).map_err(Invalid::from)?;
            let oid = git::Oid::decode(buf)?;

            refs.insert(name, oid);
@@ -330,10 +332,10 @@ impl Decode for Refs {
    }
}

-
impl Decode for git::RefString {
+
impl Decode for git::fmt::RefString {
    fn decode(buf: &mut impl Buf) -> Result<Self, Error> {
        let ref_str = String::decode(buf)?;
-
        Ok(git::RefString::try_from(ref_str).map_err(Invalid::from)?)
+
        Ok(git::fmt::RefString::try_from(ref_str).map_err(Invalid::from)?)
    }
}

@@ -365,7 +367,7 @@ where

impl Decode for git::Oid {
    fn decode(buf: &mut impl Buf) -> Result<Self, Error> {
-
        const LEN_EXPECTED: usize = mem::size_of::<git::raw::Oid>();
+
        const LEN_EXPECTED: usize = mem::size_of::<raw::Oid>();

        let len = Size::decode(buf)? as usize;

@@ -378,7 +380,7 @@ impl Decode for git::Oid {
        }

        let buf: [u8; LEN_EXPECTED] = Decode::decode(buf)?;
-
        let oid = git::raw::Oid::from_bytes(&buf).expect("the buffer is exactly the right size");
+
        let oid = raw::Oid::from_bytes(&buf).expect("the buffer is exactly the right size");
        let oid = git::Oid::from(oid);

        Ok(oid)
@@ -627,7 +629,7 @@ mod tests {

    #[quickcheck]
    fn prop_oid(input: [u8; 20]) {
-
        roundtrip(git::Oid::try_from(input.as_slice()).unwrap());
+
        roundtrip(git::Oid::from_sha1(input));
    }

    #[test]
modified crates/radicle-protocol/src/worker/fetch.rs
@@ -36,12 +36,12 @@ impl FetchResult {
/// corresponding targets.
#[derive(Clone, Default, Debug)]
pub struct UpdatedCanonicalRefs {
-
    inner: BTreeMap<git::Qualified<'static>, git::Oid>,
+
    inner: BTreeMap<git::fmt::Qualified<'static>, git::Oid>,
}

impl IntoIterator for UpdatedCanonicalRefs {
-
    type Item = (git::Qualified<'static>, git::Oid);
-
    type IntoIter = std::collections::btree_map::IntoIter<git::Qualified<'static>, git::Oid>;
+
    type Item = (git::fmt::Qualified<'static>, git::Oid);
+
    type IntoIter = std::collections::btree_map::IntoIter<git::fmt::Qualified<'static>, git::Oid>;

    fn into_iter(self) -> Self::IntoIter {
        self.inner.into_iter()
@@ -51,12 +51,12 @@ impl IntoIterator for UpdatedCanonicalRefs {
impl UpdatedCanonicalRefs {
    /// Insert a new updated entry for the canonical reference identified by
    /// `refname` and its new `target.`
-
    pub fn updated(&mut self, refname: git::Qualified<'static>, target: git::Oid) {
+
    pub fn updated(&mut self, refname: git::fmt::Qualified<'static>, target: git::Oid) {
        self.inner.insert(refname, target);
    }

    /// Return an iterator of all the updates.
-
    pub fn iter(&self) -> impl Iterator<Item = (&git::Qualified<'static>, &git::Oid)> {
+
    pub fn iter(&self) -> impl Iterator<Item = (&git::fmt::Qualified<'static>, &git::Oid)> {
        self.inner.iter()
    }
}
modified crates/radicle-protocol/src/worker/fetch/error.rs
@@ -2,7 +2,7 @@ use std::io;

use thiserror::Error;

-
use radicle::{cob, git, identity, storage};
+
use radicle::{cob, git::raw, identity, storage};
use radicle_fetch as fetch;

#[derive(Debug, Error)]
@@ -10,7 +10,7 @@ pub enum Fetch {
    #[error(transparent)]
    Run(#[from] fetch::Error),
    #[error(transparent)]
-
    Git(#[from] git::raw::Error),
+
    Git(#[from] raw::Error),
    #[error(transparent)]
    Storage(#[from] storage::Error),
    #[error(transparent)]
modified crates/radicle-remote-helper/Cargo.toml
@@ -19,5 +19,4 @@ log = { workspace = true }
radicle = { workspace = true }
radicle-cli = { workspace = true }
radicle-crypto = { workspace = true }
-
radicle-git-ext = { workspace = true }
thiserror = { workspace = true }

\ No newline at end of file
modified crates/radicle-remote-helper/src/fetch.rs
@@ -19,9 +19,9 @@ pub enum Error {
    /// Invalid reference name.
    #[error("invalid ref: {0}")]
    InvalidRef(#[from] radicle::git::fmt::Error),
-
    /// Git error.
-
    #[error("git: {0}")]
-
    InvalidOid(#[source] git::raw::Error),
+
    /// Invalid object ID.
+
    #[error("invalid oid: {0}")]
+
    InvalidOid(#[from] radicle::git::ParseOidError),

    /// Error fetching pack from storage to working copy.
    #[error("`git fetch-pack` failed with exit status {status}, stderr and stdout follow:\n{stderr}\n{stdout}")]
@@ -34,7 +34,7 @@ pub enum Error {

/// Run a git fetch command.
pub fn run<R: ReadRepository>(
-
    mut refs: Vec<(git::Oid, git::RefString)>,
+
    mut refs: Vec<(git::Oid, git::fmt::RefString)>,
    stored: R,
    stdin: &io::Stdin,
    verbosity: Verbosity,
@@ -45,8 +45,8 @@ pub fn run<R: ReadRepository>(
        let tokens = read_line(stdin, &mut line)?;
        match tokens.as_slice() {
            ["fetch", oid, refstr] => {
-
                let oid = git::Oid::from_str(oid).map_err(Error::InvalidOid)?;
-
                let refstr = git::RefString::try_from(*refstr)?;
+
                let oid = git::Oid::from_str(oid)?;
+
                let refstr = git::fmt::RefString::try_from(*refstr)?;

                refs.push((oid, refstr));
            }
modified crates/radicle-remote-helper/src/list.rs
@@ -19,7 +19,7 @@ pub enum Error {
    Identity(#[from] radicle::identity::DocError),
    /// Git error.
    #[error(transparent)]
-
    Git(#[from] radicle::git::ext::Error),
+
    Git(#[from] radicle::git::raw::Error),
    /// Profile error.
    #[error(transparent)]
    Profile(#[from] profile::Error),
@@ -56,8 +56,8 @@ pub fn for_fetch<R: ReadRepository + cob::Store<Namespace = NodeId> + 'static>(
        // List canonical references.
        // Skip over `refs/rad/*`, since those are not meant to be fetched into a working copy.
        for glob in [
-
            git::refspec::pattern!("refs/heads/*"),
-
            git::refspec::pattern!("refs/tags/*"),
+
            git::fmt::pattern!("refs/heads/*"),
+
            git::fmt::pattern!("refs/tags/*"),
        ] {
            for (name, oid) in stored.references_glob(&glob)? {
                println!("{oid} {name}");
@@ -81,8 +81,8 @@ pub fn for_push<R: ReadRepository>(profile: &Profile, stored: &R) -> Result<(),
    // Only our own refs can be pushed to.
    for (name, oid) in stored.references_of(profile.id())? {
        // Only branches and tags can be pushed to.
-
        if name.starts_with(git::refname!("refs/heads").as_str())
-
            || name.starts_with(git::refname!("refs/tags").as_str())
+
        if name.starts_with(git::fmt::refname!("refs/heads").as_str())
+
            || name.starts_with(git::fmt::refname!("refs/tags").as_str())
        {
            println!("{oid} {name}");
        }
modified crates/radicle-remote-helper/src/main.rs
@@ -117,6 +117,9 @@ pub enum Error {
    /// List error.
    #[error(transparent)]
    List(#[from] list::Error),
+
    /// Invalid object ID.
+
    #[error("invalid oid: {0}")]
+
    InvalidOid(#[from] radicle::git::ParseOidError),
}

/// Models values for the `verbosity` option, see
@@ -162,13 +165,13 @@ pub enum Branch {
    /// Create a branch with the same name as the upstream branch (i.e. `patches/<patch id>`).
    MirrorUpstream,
    /// Create a branch with the provided name.
-
    Provided(git::RefString),
+
    Provided(git::fmt::RefString),
}

impl Branch {
    /// Return the branch name to be used for the local branch when creating a
    /// patch.
-
    pub fn to_branch_name(self, object: &radicle::patch::PatchId) -> Option<git::Qualified> {
+
    pub fn to_branch_name(self, object: &radicle::patch::PatchId) -> Option<git::fmt::Qualified> {
        match self {
            Self::None => None,
            Self::MirrorUpstream => Some(git::refs::patch(object)),
@@ -207,12 +210,15 @@ pub fn run(profile: radicle::Profile) -> Result<(), Error> {
    // module is aware of that.
    cli::Paint::set_terminal(cli::TerminalFile::Stderr);

-
    let (remote, url): (Option<git::RefString>, Url) = {
+
    let (remote, url): (Option<git::fmt::RefString>, Url) = {
        let args = env::args().skip(1).take(2).collect::<Vec<_>>();

        match args.as_slice() {
            [url] => (None, url.parse()?),
-
            [remote, url] => (git::RefString::try_from(remote.as_str()).ok(), url.parse()?),
+
            [remote, url] => (
+
                git::fmt::RefString::try_from(remote.as_str()).ok(),
+
                url.parse()?,
+
            ),

            _ => {
                return Err(Error::InvalidArguments(args));
@@ -274,7 +280,7 @@ pub fn run(profile: radicle::Profile) -> Result<(), Error> {
            }
            ["fetch", oid, refstr] => {
                let oid = git::Oid::from_str(oid)?;
-
                let refstr = git::RefString::try_from(*refstr)?;
+
                let refstr = git::fmt::RefString::try_from(*refstr)?;

                return Ok(fetch::run(
                    vec![(oid, refstr)],
@@ -347,7 +353,9 @@ fn push_option(args: &[&str], opts: &mut Options) -> Result<(), Error> {

                    opts.base = Some(git::Oid::from(commit.id()));
                }
-
                "patch.branch" => opts.branch = Branch::Provided(git::RefString::try_from(val)?),
+
                "patch.branch" => {
+
                    opts.branch = Branch::Provided(git::fmt::RefString::try_from(val)?)
+
                }
                other => {
                    return Err(Error::UnsupportedPushOption(other.to_owned()));
                }
modified crates/radicle-remote-helper/src/push.rs
@@ -43,7 +43,7 @@ pub enum Error {
    NoKey,
    /// User tried to delete the canonical branch.
    #[error("refusing to delete default branch ref '{0}'")]
-
    DeleteForbidden(git::RefString),
+
    DeleteForbidden(git::fmt::RefString),
    /// Identity document error.
    #[error("doc: {0}")]
    Doc(#[from] radicle::identity::doc::DocError),
@@ -65,9 +65,6 @@ pub enum Error {
    /// Git error.
    #[error("git: {0}")]
    Git(#[from] git::raw::Error),
-
    /// Git extension error.
-
    #[error("git: {0}")]
-
    GitExt(#[from] git::ext::Error),
    /// Storage error.
    #[error(transparent)]
    Storage(#[from] radicle::storage::Error),
@@ -136,9 +133,9 @@ pub enum Error {
/// Push command.
enum Command {
    /// Update ref.
-
    Push(git::Refspec<git::Oid, git::RefString>),
+
    Push(git::fmt::refspec::Refspec<git::Oid, git::fmt::RefString>),
    /// Delete ref.
-
    Delete(git::RefString),
+
    Delete(git::fmt::RefString),
}

#[derive(Debug, thiserror::Error)]
@@ -173,7 +170,7 @@ impl Command {
        let Some((src, dst)) = s.split_once(':') else {
            return Err(CommandError::Empty { rev: s.to_string() });
        };
-
        let dst = git::RefString::try_from(dst).map_err(|err| CommandError::Delete {
+
        let dst = git::fmt::RefString::try_from(dst).map_err(|err| CommandError::Delete {
            rev: dst.to_string(),
            err,
        })?;
@@ -195,12 +192,12 @@ impl Command {
                .id()
                .into();

-
            Ok(Self::Push(git::Refspec { src, dst, force }))
+
            Ok(Self::Push(git::fmt::refspec::Refspec { src, dst, force }))
        }
    }

    /// Return the destination refname.
-
    fn dst(&self) -> &git::RefStr {
+
    fn dst(&self) -> &git::fmt::RefStr {
        match self {
            Self::Push(rs) => rs.dst.as_refstr(),
            Self::Delete(rs) => rs,
@@ -211,30 +208,30 @@ impl Command {
enum PushAction {
    OpenPatch,
    UpdatePatch {
-
        dst: git::Qualified<'static>,
+
        dst: git::fmt::Qualified<'static>,
        patch: patch::PatchId,
    },
    PushRef {
-
        dst: git::Qualified<'static>,
+
        dst: git::fmt::Qualified<'static>,
    },
}

impl PushAction {
-
    fn new(dst: &git::RefString) -> Result<Self, error::PushAction> {
+
    fn new(dst: &git::fmt::RefString) -> Result<Self, error::PushAction> {
        if dst == &*rad::PATCHES_REFNAME {
            Ok(Self::OpenPatch)
        } else {
-
            let dst = git::Qualified::from_refstr(dst)
+
            let dst = git::fmt::Qualified::from_refstr(dst)
                .ok_or_else(|| error::PushAction::InvalidRef {
                    refname: dst.clone(),
                })?
                .to_owned();

-
            if let Some(oid) = dst.strip_prefix(git::refname!("refs/heads/patches")) {
+
            if let Some(oid) = dst.strip_prefix(git::fmt::refname!("refs/heads/patches")) {
                let patch = git::Oid::from_str(oid)
-
                    .map_err(|err| error::PushAction::InvalidPatchId {
+
                    .map_err(|source| error::PushAction::InvalidPatchId {
                        suffix: oid.to_string(),
-
                        source: err,
+
                        source,
                    })
                    .map(patch::PatchId::from)?;
                Ok(Self::UpdatePatch { dst, patch })
@@ -248,7 +245,7 @@ impl PushAction {
/// Run a git push command.
pub fn run(
    mut specs: Vec<String>,
-
    remote: Option<git::RefString>,
+
    remote: Option<git::fmt::RefString>,
    url: Url,
    stored: &storage::git::Repository,
    profile: &Profile,
@@ -290,7 +287,7 @@ pub fn run(
    let identity = stored.identity()?;
    let project = identity.project()?;
    let canonical_ref = git::refs::branch(project.default_branch());
-
    let mut set_canonical_refs: Vec<(git::Qualified, git::canonical::Object)> =
+
    let mut set_canonical_refs: Vec<(git::fmt::Qualified, git::canonical::Object)> =
        Vec::with_capacity(specs.len());

    // Rely on the environment variable `GIT_DIR`.
@@ -317,7 +314,7 @@ pub fn run(
                    .map(|_| None)
                    .map_err(Error::from)
            }
-
            Command::Push(git::Refspec { src, dst, force }) => {
+
            Command::Push(git::fmt::refspec::Refspec { src, dst, force }) => {
                let patches = crate::patches_mut(profile, stored)?;
                let action = PushAction::new(dst)?;

@@ -373,7 +370,7 @@ pub fn run(
                        // canonical branch.
                        if let Some(canonical) = rules.canonical(dst.clone(), stored) {
                            let object = working
-
                                .find_object(**src, None)
+
                                .find_object(src.into(), None)
                                .map(|obj| git::canonical::Object::new(&obj))?
                                .ok_or(Error::UnknownObjectType { oid: *src })?;

@@ -429,10 +426,10 @@ pub fn run(
            }

            match stored.backend.refname_to_id(refname.as_str()) {
-
                Ok(new) if new != *oid => {
+
                Ok(new) if oid != new => {
                    stored.backend.reference(
                        refname.as_str(),
-
                        *oid,
+
                        oid.into(),
                        true,
                        "set-canonical-reference from git-push (radicle)",
                    )?;
@@ -441,7 +438,7 @@ pub fn run(
                Err(e) if e.code() == git::raw::ErrorCode::NotFound => {
                    stored.backend.reference(
                        refname.as_str(),
-
                        *oid,
+
                        oid.into(),
                        true,
                        "set-canonical-reference from git-push (radicle)",
                    )?;
@@ -505,7 +502,7 @@ fn patch_base(
/// [`Drop::drop`].
struct TempPatchRef<'a> {
    stored: &'a storage::git::Repository,
-
    reference: git::Namespaced<'a>,
+
    reference: git::fmt::Namespaced<'a>,
}

impl<'a> TempPatchRef<'a> {
@@ -539,7 +536,7 @@ impl<'a> Drop for TempPatchRef<'a> {
/// Open a new patch.
fn patch_open<G>(
    head: &git::Oid,
-
    upstream: &Option<git::RefString>,
+
    upstream: &Option<git::fmt::RefString>,
    nid: &NodeId,
    working: &git::raw::Repository,
    stored: &storage::git::Repository,
@@ -563,7 +560,7 @@ where
    }

    let (title, description) =
-
        term::patch::get_create_message(opts.message, &stored.backend, &base, head)?;
+
        term::patch::get_create_message(opts.message, &stored.backend, &base.into(), &head.into())?;

    let patch = if opts.draft {
        patches.draft(
@@ -607,24 +604,29 @@ where
    let refname = git::refs::patch(&patch).with_namespace(nid.into());
    let _ = stored.raw().reference(
        refname.as_str(),
-
        **head,
+
        head.into(),
        true,
        "Create reference for patch head",
    )?;

    if let Some(upstream) = upstream {
        if let Some(local_branch) = opts.branch.to_branch_name(&patch) {
-
            fn strip_refs_heads(qualified: git::Qualified) -> git::RefString {
+
            fn strip_refs_heads(qualified: git::fmt::Qualified) -> git::fmt::RefString {
                let (_refs, _heads, x, xs) = qualified.non_empty_components();
                std::iter::once(x).chain(xs).collect()
            }

-
            working.reference(&local_branch, **head, true, "Create local branch for patch")?;
+
            working.reference(
+
                &local_branch,
+
                head.into(),
+
                true,
+
                "Create local branch for patch",
+
            )?;

            let remote_branch = git::refs::workdir::patch_upstream(&patch);
            let remote_branch = working.reference(
                &remote_branch,
-
                **head,
+
                head.into(),
                true,
                "Create remote tracking branch for patch",
            )?;
@@ -667,7 +669,7 @@ where
#[allow(clippy::too_many_arguments)]
fn patch_update<G>(
    head: &git::Oid,
-
    dst: &git::Qualified,
+
    dst: &git::fmt::Qualified,
    force: bool,
    patch_id: patch::PatchId,
    nid: &NodeId,
@@ -703,7 +705,8 @@ where
    let (latest_id, latest) = patch.latest();
    let latest = latest.clone();

-
    let message = term::patch::get_update_message(opts.message, &stored.backend, &latest, head)?;
+
    let message =
+
        term::patch::get_update_message(opts.message, &stored.backend, &latest, &head.into())?;

    let dst = dst.with_namespace(nid.into());
    push_ref(head, &dst, force, stored.raw(), opts.verbosity)?;
@@ -744,7 +747,7 @@ where

fn push<G>(
    src: &git::Oid,
-
    dst: &git::Qualified,
+
    dst: &git::fmt::Qualified,
    force: bool,
    nid: &NodeId,
    working: &git::raw::Repository,
@@ -768,7 +771,7 @@ where

    if let Some(old) = old {
        let proj = stored.project()?;
-
        let master = &*git::Qualified::from(git::lit::refs_heads(proj.default_branch()));
+
        let master = &*git::fmt::Qualified::from(git::fmt::lit::refs_heads(proj.default_branch()));

        // If we're pushing to the project's default branch, we want to see if any patches got
        // merged or reverted, and if so, update the patch COB.
@@ -800,8 +803,8 @@ where
{
    // Find all commits reachable from the old OID but not from the new OID.
    let mut revwalk = stored.revwalk()?;
-
    revwalk.push(*old)?;
-
    revwalk.hide(*new)?;
+
    revwalk.push(old.into())?;
+
    revwalk.hide(new.into())?;

    // List of commits that have been dropped.
    let dropped = revwalk
@@ -943,7 +946,7 @@ where
/// Push a single reference to storage.
fn push_ref(
    src: &git::Oid,
-
    dst: &git::Namespaced,
+
    dst: &git::fmt::Namespaced,
    force: bool,
    stored: &git::raw::Repository,
    verbosity: Verbosity,
@@ -951,7 +954,7 @@ fn push_ref(
    let path = dunce::canonicalize(stored.path())?.display().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.
-
    let refspec = git::Refspec { src, dst, force };
+
    let refspec = git::fmt::refspec::Refspec { src, dst, force };

    let mut args = vec!["send-pack".to_string()];

modified crates/radicle-remote-helper/src/push/canonical.rs
@@ -40,7 +40,7 @@ where
    /// copy, and that checks that any two commits are related in the graph.
    ///
    /// Ensures that the new head and the canonical commit do not diverge.
-
    pub fn quorum(self) -> Result<(git::Qualified<'a>, canonical::Object), QuorumError> {
+
    pub fn quorum(self) -> Result<(git::fmt::Qualified<'a>, canonical::Object), QuorumError> {
        self.canonical
            .quorum()
            .map(|QuorumWithConvergence { quorum, .. }| (quorum.refname, quorum.object))
modified crates/radicle-remote-helper/src/push/error.rs
@@ -37,10 +37,10 @@ pub struct HeadsDiverge {
#[derive(Debug, Error)]
pub enum PushAction {
    #[error("invalid reference {refname}, expected qualified reference starting with `refs/`")]
-
    InvalidRef { refname: git::RefString },
-
    #[error("found refs/heads/patches/{suffix} where {suffix} was an invalid Patch ID")]
+
    InvalidRef { refname: git::fmt::RefString },
+
    #[error("found refs/heads/patches/{suffix} where {suffix} was an invalid Patch ID: {source}")]
    InvalidPatchId {
        suffix: String,
-
        source: git::raw::Error,
+
        source: radicle::git::ParseOidError,
    },
}
modified crates/radicle-term/Cargo.toml
@@ -24,7 +24,7 @@ thiserror = { workspace = true }
unicode-display-width = "0.3.0"
unicode-segmentation = "1.7.1"
zeroize = { workspace = true }
-
git2 = { workspace = true, features = ["vendored-libgit2"], optional = true }
+
git2 = { workspace = true, optional = true }
shlex = { workspace = true }

[target.'cfg(unix)'.dependencies]
modified crates/radicle/CHANGELOG.md
@@ -18,6 +18,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

+
- Re-exports from `git2` at `radicle::git::raw` were limited, using
+
  the heartwood workspace as a filter. Dependents that require members that
+
  are not exported anymore will have to depend on `git2` directly.
+
- Some re-exports from `git-ref-format-core` were moved from `radicle::git`
+
  to `radicle::fmt`.
+
- The crate now re-exports `radicle::git::Oid` from a new `radicle-oid` crate,
+
  in an effort to decrease dependence on `git2` via `radicle-git-ext`. This
+
  new object identifier type does not implement `Deref` anymore. Use `Into`
+
  to convert to a `git2::Oid` as necessary.
+
- Re-exports of `radicle-git-ext` were removed, as this dependency is removed.
+
  Instead of `radicle_git_ext::Error`, use `git2::Error` (re-exported as
+
  `radicle::git::raw::Error`) together with the new extension trait
+
  `radicle::git::raw::ErrorExt`.
+

### Deprecated

- `radicle::node::Handle::announce_refs` is deprecated in favor of
modified crates/radicle/Cargo.toml
@@ -11,8 +11,9 @@ rust-version.workspace = true

[features]
default = []
-
test = ["tempfile", "qcheck", "radicle-crypto/test"]
+
test = ["tempfile", "qcheck", "radicle-crypto/test", "radicle-cob/test"]
logger = ["colored", "chrono"]
+
schemars = ["radicle-oid/schemars", "dep:schemars"]

[dependencies]
amplify = { workspace = true, features = ["std"] }
@@ -32,11 +33,12 @@ log = { workspace = true, features = ["std"] }
multibase = { workspace = true }
nonempty = { workspace = true, features = ["serialize"] }
qcheck = { workspace = true, optional = true }
-
radicle-cob = { workspace = true }
+
radicle-cob = { workspace = true, features = ["git2"] }
radicle-crypto = { workspace = true, features = ["git-ref-format-core", "ssh", "sqlite", "cyphernet"] }
-
radicle-git-ext = { workspace = true, features = ["serde"] }
+
radicle-git-ref-format = { workspace = true, features = ["macro", "serde"] }
+
radicle-oid = { workspace = true, features = ["git2", "serde", "std", "sha1"] }
radicle-ssh = { workspace = true }
-
schemars = { workspace = true, optional = true }
+
schemars = { workspace = true, optional = true, features = ["derive", "std"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true, features = ["preserve_order"] }
serde-untagged = "0.1.7"
@@ -58,6 +60,7 @@ jsonschema = { version = "0.30", default-features = false }
pretty_assertions = { workspace = true }
qcheck = { workspace = true }
qcheck-macros = { workspace = true }
-
radicle-cob = { workspace = true, features = ["stable-commit-ids"] }
+
radicle-cob = { workspace = true, features = ["stable-commit-ids", "test"] }
radicle-crypto = { workspace = true, features = ["test"] }
+
radicle-git-metadata = { workspace = true }
tempfile = { workspace = true }
modified crates/radicle/src/cob.rs
@@ -13,6 +13,9 @@ pub mod thread;
#[cfg(test)]
pub mod test;

+
#[cfg(test)]
+
pub use radicle_cob::stable;
+

pub use cache::{migrate, MigrateCallback};
pub use common::*;
pub use op::{ActorId, Op};
@@ -21,7 +24,7 @@ pub use radicle_cob::{
    CollaborativeObject, Contents, Create, Embed, Entry, Evaluate, History, Manifest, ObjectId,
    Store, TypeName, Update, Updated, Version,
};
-
pub use radicle_cob::{create, get, git, list, remove, update};
+
pub use radicle_cob::{create, get, list, remove, update};

/// The exact identifier for a particular COB.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize)]
@@ -66,17 +69,17 @@ impl TypedId {
        self.type_name == *identity::TYPENAME
    }

-
    /// Parse a [`crate::git::Namespaced`] refname into a [`TypedId`].
+
    /// Parse a [`crate::git::fmt::Namespaced`] refname into a [`TypedId`].
    ///
    /// All namespaces are stripped before parsing the suffix for the
    /// [`TypedId`] (see [`TypedId::from_qualified`]).
    pub fn from_namespaced(
-
        n: &crate::git::Namespaced,
+
        n: &crate::git::fmt::Namespaced,
    ) -> Result<Option<Self>, ParseIdentifierError> {
        Self::from_qualified(&n.strip_namespace_recursive())
    }

-
    /// Parse a [`crate::git::Qualified`] refname into a [`TypedId`].
+
    /// Parse a [`crate::git::fmt::Qualified`] refname into a [`TypedId`].
    ///
    /// The refname is expected to be of the form:
    ///     `refs/cobs/<type name>/<object id>`
@@ -87,7 +90,9 @@ impl TypedId {
    ///
    /// This will fail if the refname is of the correct form, but the
    /// type name or object id fail to parse.
-
    pub fn from_qualified(q: &crate::git::Qualified) -> Result<Option<Self>, ParseIdentifierError> {
+
    pub fn from_qualified(
+
        q: &crate::git::fmt::Qualified,
+
    ) -> Result<Option<Self>, ParseIdentifierError> {
        match q.non_empty_iter() {
            ("refs", "cobs", type_name, mut id) => {
                let Some(id) = id.next() else {
modified crates/radicle/src/cob/common.rs
@@ -355,7 +355,7 @@ impl From<Oid> for Uri {
    }
}

-
impl TryFrom<&Uri> for Oid {
+
impl TryFrom<&Uri> for crate::git::raw::Oid {
    type Error = Uri;

    fn try_from(value: &Uri) -> Result<Self, Self::Error> {
@@ -368,6 +368,14 @@ impl TryFrom<&Uri> for Oid {
    }
}

+
impl TryFrom<&Uri> for crate::git::Oid {
+
    type Error = Uri;
+

+
    fn try_from(value: &Uri) -> Result<Self, Self::Error> {
+
        crate::git::raw::Oid::try_from(value).map(crate::git::Oid::from)
+
    }
+
}
+

impl std::fmt::Display for Uri {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.0)
modified crates/radicle/src/cob/external.rs
@@ -225,7 +225,7 @@ impl<R: ReadRepository> Evaluate<R> for External {
        Self::from_root(Op::try_from(entry)?, store)
    }

-
    fn apply<'a, I: Iterator<Item = (&'a radicle_git_ext::Oid, &'a radicle_cob::Entry)>>(
+
    fn apply<'a, I: Iterator<Item = (&'a crate::git::Oid, &'a radicle_cob::Entry)>>(
        &mut self,
        entry: &radicle_cob::Entry,
        concurrent: I,
modified crates/radicle/src/cob/identity.rs
@@ -4,11 +4,11 @@ use std::{fmt, ops::Deref, str::FromStr};

use crypto::{PublicKey, Signature};
use radicle_cob::{Embed, ObjectId, TypeName};
-
use radicle_git_ext as git_ext;
-
use radicle_git_ext::Oid;
use serde::{Deserialize, Serialize};
use thiserror::Error;

+
use crate::git;
+
use crate::git::Oid;
use crate::identity::doc::Doc;
use crate::node::device::Device;
use crate::node::NodeId;
@@ -126,9 +126,7 @@ pub enum ApplyError {
    #[error("document does not contain any changes to current identity")]
    DocUnchanged,
    #[error("git: {0}")]
-
    Git(#[from] crate::git::raw::Error),
-
    #[error("git: {0}")]
-
    GitExt(#[from] git_ext::Error),
+
    Git(#[from] git::raw::Error),
    #[error("identity document error: {0}")]
    Doc(#[from] DocError),
}
@@ -1323,16 +1321,15 @@ mod test {
            .unwrap();

        bob.repo.fetch(alice);
-
        let a3 = cob::git::stable::with_advanced_timestamp(|| {
+
        let a3 = cob::stable::with_advanced_timestamp(|| {
            alice_identity.redact(a2, &alice.signer).unwrap()
        });
        assert!(alice_identity.revision(&a1).is_some());
        assert_eq!(alice_identity.timeline, vec![a0, a1, a2, a3]);

        let mut bob_identity = Identity::load_mut(&*bob.repo).unwrap();
-
        let b1 = cob::git::stable::with_advanced_timestamp(|| {
-
            bob_identity.accept(&a2, &bob.signer).unwrap()
-
        });
+
        let b1 =
+
            cob::stable::with_advanced_timestamp(|| bob_identity.accept(&a2, &bob.signer).unwrap());

        assert_eq!(bob_identity.timeline, vec![a0, a1, a2, b1]);
        assert_eq!(bob_identity.revision(&a2).unwrap().state, State::Accepted);
@@ -1381,15 +1378,14 @@ mod test {
        eve.repo.fetch(bob);

        let mut bob_identity = Identity::load_mut(&*bob.repo).unwrap();
-
        let b1 = cob::git::stable::with_advanced_timestamp(|| {
-
            bob_identity.accept(&a2, &bob.signer).unwrap()
-
        });
+
        let b1 =
+
            cob::stable::with_advanced_timestamp(|| bob_identity.accept(&a2, &bob.signer).unwrap());
        assert_eq!(bob_identity.current, a2);

        let mut eve_identity = Identity::load_mut(&*eve.repo).unwrap();
        let mut eve_doc = eve_identity.doc().clone().edit();
        eve_doc.visibility = Visibility::private([eve.signer.public_key().into()]);
-
        let e1 = cob::git::stable::with_advanced_timestamp(|| {
+
        let e1 = cob::stable::with_advanced_timestamp(|| {
            eve_identity
                .update(
                    cob::Title::new("Change visibility").unwrap(),
@@ -1458,15 +1454,13 @@ mod test {

        // Bob accepts alice's revision.
        let mut bob_identity = Identity::load_mut(&*bob.repo).unwrap();
-
        let b1 = cob::git::stable::with_advanced_timestamp(|| {
-
            bob_identity.accept(&a2, &bob.signer).unwrap()
-
        });
+
        let b1 =
+
            cob::stable::with_advanced_timestamp(|| bob_identity.accept(&a2, &bob.signer).unwrap());

        // Eve rejects the revision, not knowing.
        let mut eve_identity = Identity::load_mut(&*eve.repo).unwrap();
-
        let e1 = cob::git::stable::with_advanced_timestamp(|| {
-
            eve_identity.reject(a2, &eve.signer).unwrap()
-
        });
+
        let e1 =
+
            cob::stable::with_advanced_timestamp(|| eve_identity.reject(a2, &eve.signer).unwrap());
        assert!(eve_identity.revision(&a2).unwrap().is_active());

        // Then she submits a new revision.
@@ -1576,7 +1570,7 @@ mod test {
        assert_eq!(eve_identity.revision(&e1).unwrap().state, State::Active);

        alice_identity.reload().unwrap();
-
        let a2 = cob::git::stable::with_advanced_timestamp(|| {
+
        let a2 = cob::stable::with_advanced_timestamp(|| {
            alice_identity.accept(&b1, &alice.signer).unwrap()
        });

modified crates/radicle/src/cob/issue.rs
@@ -1765,7 +1765,7 @@ mod test {
            .unwrap();

        // Comments require references, so adding two of them to the same transaction errors.
-
        let mut tx: Transaction<Issue, test::storage::git::Repository> =
+
        let mut tx: Transaction<Issue, crate::storage::git::Repository> =
            Transaction::<Issue, _>::default();
        tx.comment("First reply", *issue.id, vec![]).unwrap();
        let err = tx.comment("Second reply", *issue.id, vec![]).unwrap_err();
modified crates/radicle/src/cob/op.rs
@@ -8,9 +8,10 @@ use radicle_crypto::PublicKey;

use crate::cob;
use crate::cob::Timestamp;
+
use crate::git;
+
use crate::identity;
use crate::identity::DocAt;
use crate::storage::ReadRepository;
-
use crate::{git, identity};

/// The author of an [`Op`].
pub type ActorId = PublicKey;
modified crates/radicle/src/cob/patch.rs
@@ -130,7 +130,7 @@ pub enum Error {
    Payload(#[from] PayloadError),
    /// Git error.
    #[error("git: {0}")]
-
    Git(#[from] git::ext::Error),
+
    Git(#[from] git::raw::Error),
    /// Store error.
    #[error("store: {0}")]
    Store(#[from] store::Error),
@@ -601,12 +601,15 @@ impl Patch {
    }

    /// Get the merge base of this patch.
-
    pub fn merge_base<R: ReadRepository>(&self, repo: &R) -> Result<git::Oid, git::ext::Error> {
+
    pub fn merge_base<R: ReadRepository>(
+
        &self,
+
        repo: &R,
+
    ) -> Result<crate::git::Oid, crate::git::raw::Error> {
        repo.merge_base(self.base(), self.head())
    }

    /// Get the commit range of this patch.
-
    pub fn range(&self) -> Result<(git::Oid, git::Oid), git::ext::Error> {
+
    pub fn range(&self) -> Result<(crate::git::Oid, crate::git::Oid), crate::git::raw::Error> {
        Ok((*self.base(), *self.head()))
    }

@@ -3334,7 +3337,7 @@ mod test {
            },
            &alice,
        );
-
        let patch = Patch::from_history(&h0, &repo).unwrap();
+
        let patch: Patch = Patch::from_history(&h0, &repo).unwrap();
        assert_eq!(patch.revisions().count(), 2);

        let mut h1 = h0.clone();
modified crates/radicle/src/cob/store.rs
@@ -125,7 +125,7 @@ pub enum Error {
    Git(git::raw::Error),
    #[error("failed to find reference '{name}': {err}")]
    RefLookup {
-
        name: git::RefString,
+
        name: git::fmt::RefString,
        #[source]
        err: git::raw::Error,
    },
modified crates/radicle/src/cob/stream/iter.rs
@@ -3,7 +3,9 @@ use std::marker::PhantomData;
use serde::Deserialize;

use crate::cob::{Op, TypeName};
-
use crate::git::{self, Oid, PatternString};
+
use crate::git;
+
use crate::git::fmt::refspec::PatternString;
+
use crate::git::Oid;

use super::error;
use super::CobRange;
@@ -83,7 +85,7 @@ impl Walk {
        match self.until {
            Until::Tip(tip) => walk.push_range(&format!("{}..{}", self.from, tip))?,
            Until::Glob(glob) => {
-
                walk.push(*self.from)?;
+
                walk.push(self.from.into())?;
                walk.push_glob(glob.as_str())?
            }
        }
@@ -103,7 +105,7 @@ impl<'a> Iterator for WalkIter<'a> {
        // N.b. ensure that we start using the `from` commit and use the revwalk
        // after that.
        if let Some(from) = self.from.take() {
-
            return Some(self.repo.find_commit(*from));
+
            return Some(self.repo.find_commit(from.into()));
        }
        let oid = self.inner.next()?;
        Some(oid.and_then(|oid| self.repo.find_commit(oid)))
@@ -132,7 +134,7 @@ where
        let commit = self.walk.next()?;
        match commit {
            Ok(commit) => {
-
                let entry = git::Oid::from(commit.id());
+
                let entry = crate::git::Oid::from(commit.id());
                // N.b. mark this commit as seen, so that it is not walked again
                self.walk.inner.hide(commit.id()).ok();
                // Skip any Op that do not match the manifest
@@ -155,7 +157,7 @@ impl<'a, A> OpsIter<'a, A> {

    /// Load the `Op` for the given `entry`, ensuring that manifest matches with
    /// the expected manifest.
-
    fn load(&self, entry: git::Oid) -> Result<Option<Op<A>>, error::Ops>
+
    fn load(&self, entry: crate::git::Oid) -> Result<Option<Op<A>>, error::Ops>
    where
        A: for<'de> Deserialize<'de>,
    {
modified crates/radicle/src/cob/test.rs
@@ -12,10 +12,6 @@ use crate::cob::store::encoding;
use crate::cob::{patch, Title};
use crate::cob::{Entry, History, Manifest, Timestamp, Version};
use crate::crypto::Signer;
-
use crate::git;
-
use crate::git::ext::author::Author;
-
use crate::git::ext::commit::headers::Headers;
-
use crate::git::ext::commit::{trailers::OwnedTrailer, Commit};
use crate::git::Oid;
use crate::node::device::Device;
use crate::prelude::Did;
@@ -101,7 +97,7 @@ where
        self.history.merge(other.history);
    }

-
    pub fn commit<G: Signer>(&mut self, action: &T::Action, signer: &G) -> git::ext::Oid {
+
    pub fn commit<G: Signer>(&mut self, action: &T::Action, signer: &G) -> crate::git::Oid {
        let timestamp = self.time;
        let tips = self.tips();
        let revision = arbitrary::oid();
@@ -182,7 +178,8 @@ impl<G: Signer> Actor<G> {
            "nonce": fastrand::u64(..),
        }))
        .unwrap();
-
        let oid = git::raw::Oid::hash_object(git::raw::ObjectType::Blob, &data).unwrap();
+
        let oid =
+
            crate::git::raw::Oid::hash_object(crate::git::raw::ObjectType::Blob, &data).unwrap();
        let id = oid.into();
        let author = *self.signer.public_key();
        let actions = NonEmpty::from_vec(actions).unwrap();
@@ -226,8 +223,8 @@ impl<G: Signer> Actor<G> {
        &mut self,
        title: Title,
        description: impl ToString,
-
        base: git::Oid,
-
        oid: git::Oid,
+
        base: crate::git::Oid,
+
        oid: crate::git::Oid,
        repo: &R,
    ) -> Result<Patch, patch::Error> {
        Patch::from_root(
@@ -252,21 +249,26 @@ impl<G: Signer> Actor<G> {
///
/// Doesn't encode in the same way as we do in production, but attempts to include the same data
/// that feeds into the hash entropy, so that changing any input will change the resulting oid.
-
pub fn encoded<T: Cob, G: Signer>(
+
fn encoded<T: Cob, G: Signer>(
    action: &T::Action,
    timestamp: Timestamp,
    parents: impl IntoIterator<Item = Oid>,
    signer: &G,
-
) -> (Vec<u8>, git::ext::Oid) {
+
) -> (Vec<u8>, crate::git::Oid) {
+
    use radicle_git_metadata::{
+
        author::{Author, Time},
+
        commit::{headers::Headers, trailers::OwnedTrailer, CommitData},
+
    };
+

    let data = encoding::encode(action).unwrap();
-
    let oid = git::raw::Oid::hash_object(git::raw::ObjectType::Blob, &data).unwrap();
-
    let parents = parents.into_iter().map(|o| *o);
+
    let oid = crate::git::raw::Oid::hash_object(crate::git::raw::ObjectType::Blob, &data).unwrap();
+
    let parents = parents.into_iter().map(|o| o.into());
    let author = Author {
        name: "radicle".to_owned(),
        email: signer.public_key().to_human(),
-
        time: git_ext::author::Time::new(timestamp.as_secs() as i64, 0),
+
        time: Time::new(timestamp.as_secs() as i64, 0),
    };
-
    let commit = Commit::new::<_, _, OwnedTrailer>(
+
    let commit = CommitData::<git2::Oid, git2::Oid>::new::<_, _, OwnedTrailer>(
        oid,
        parents,
        author.clone(),
@@ -277,7 +279,9 @@ pub fn encoded<T: Cob, G: Signer>(
    )
    .to_string();

-
    let hash = git::raw::Oid::hash_object(git::raw::ObjectType::Commit, commit.as_bytes()).unwrap();
+
    let hash =
+
        crate::git::raw::Oid::hash_object(crate::git::raw::ObjectType::Commit, commit.as_bytes())
+
            .unwrap();

    (data, hash.into())
}
modified crates/radicle/src/git.rs
@@ -5,33 +5,24 @@ use std::io;
use std::path::Path;
use std::process::Command;
use std::str::FromStr;
-
use std::sync::LazyLock;

-
use git_ext::ref_format as format;
+
pub use radicle_oid::{str::ParseOidError, Oid};
+

+
pub extern crate radicle_git_ref_format as fmt;

use crate::collections::RandomMap;
use crate::crypto::PublicKey;
use crate::node::Alias;
use crate::rad;
-
use crate::storage;
use crate::storage::refs::Refs;
use crate::storage::RemoteId;

-
pub use ext::Error;
-
pub use ext::NotFound;
-
pub use ext::Oid;
-
pub use git_ext::ref_format as fmt;
-
pub use git_ext::ref_format::{
-
    component, lit, name, qualified, refname, refspec,
-
    refspec::{PatternStr, PatternString, Refspec},
-
    Component, Namespaced, Qualified, RefStr, RefString,
-
};
-
pub use radicle_git_ext as ext;
-
pub use storage::git::transport::local::Url;
-
pub use storage::BranchName;
+
pub use crate::storage::git::transport::local::Url;

use raw::ErrorExt as _;

+
pub type BranchName = crate::git::fmt::RefString;
+

/// Default port of the `git` transport protocol.
pub const PROTOCOL_PORT: u16 = 9418;
/// Minimum required git version.
@@ -160,16 +151,16 @@ pub enum RefError {
    #[error("ref name is not valid UTF-8")]
    InvalidName,
    #[error("unexpected unqualified ref: {0}")]
-
    Unqualified(RefString),
+
    Unqualified(fmt::RefString),
    #[error("invalid ref format: {0}")]
-
    Format(#[from] format::Error),
+
    Format(#[from] fmt::Error),
    #[error("reference has no target")]
    NoTarget,
    #[error("expected ref to begin with 'refs/namespaces' but found '{0}'")]
-
    MissingNamespace(format::RefString),
+
    MissingNamespace(fmt::RefString),
    #[error("ref name contains invalid namespace identifier '{name}'")]
    InvalidNamespace {
-
        name: format::RefString,
+
        name: fmt::RefString,
        #[source]
        err: Box<dyn std::error::Error + Send + Sync + 'static>,
    },
@@ -186,9 +177,13 @@ pub enum ListRefsError {
}

pub mod refs {
-
    use super::*;
+
    use std::sync::LazyLock;
+

    use radicle_cob as cob;

+
    use super::fmt::*;
+
    use super::*;
+

    /// Try to get a qualified reference from a generic reference.
    pub fn qualified_from<'a>(r: &'a raw::Reference) -> Result<(Qualified<'a>, Oid), RefError> {
        let name = r.name().ok_or(RefError::InvalidName)?;
@@ -214,35 +209,28 @@ pub mod refs {
    ///
    pub fn patch<'a>(object_id: &cob::ObjectId) -> Qualified<'a> {
        Qualified::from_components(
-
            name::component!("heads"),
-
            name::component!("patches"),
+
            component!("heads"),
+
            component!("patches"),
            Some(object_id.into()),
        )
    }

    pub mod storage {
-
        use format::{
-
            lit,
-
            name::component,
-
            refspec::{self, PatternString},
-
        };
-

        use super::*;

        /// Where the repo's identity document is stored.
        ///
        /// `refs/rad/id`
        ///
-
        pub static IDENTITY_BRANCH: LazyLock<Qualified> = LazyLock::new(|| {
-
            Qualified::from_components(name::component!("rad"), name::component!("id"), None)
-
        });
+
        pub static IDENTITY_BRANCH: LazyLock<Qualified> =
+
            LazyLock::new(|| Qualified::from_components(component!("rad"), component!("id"), None));

        /// Where the repo's identity root document is stored.
        ///
        /// `refs/rad/root`
        ///
        pub static IDENTITY_ROOT: LazyLock<Qualified> = LazyLock::new(|| {
-
            Qualified::from_components(name::component!("rad"), name::component!("root"), None)
+
            Qualified::from_components(component!("rad"), component!("root"), None)
        });

        /// Where the project's signed references are stored.
@@ -250,7 +238,7 @@ pub mod refs {
        /// `refs/rad/sigrefs`
        ///
        pub static SIGREFS_BRANCH: LazyLock<Qualified> = LazyLock::new(|| {
-
            Qualified::from_components(name::component!("rad"), name::component!("sigrefs"), None)
+
            Qualified::from_components(component!("rad"), component!("sigrefs"), None)
        });

        /// The set of special references used in the Heartwood protocol.
@@ -341,8 +329,8 @@ pub mod refs {
        ///
        /// `refs/namespaces/*/refs/cobs/<typename>/<object_id>`
        ///
-
        pub fn cobs(typename: &cob::TypeName, object_id: &cob::ObjectId) -> PatternString {
-
            refspec::pattern!("refs/namespaces/*")
+
        pub fn cobs(typename: &cob::TypeName, object_id: &cob::ObjectId) -> refspec::PatternString {
+
            pattern!("refs/namespaces/*")
                .join(refname!("refs/cobs"))
                .join(Component::from(typename))
                .join(Component::from(object_id))
@@ -390,8 +378,11 @@ pub mod refs {
            ///
            /// `refs/namespaces/*/refs/drafts/cobs/<typename>/<object_id>`
            ///
-
            pub fn cobs(typename: &cob::TypeName, object_id: &cob::ObjectId) -> PatternString {
-
                refspec::pattern!("refs/namespaces/*")
+
            pub fn cobs(
+
                typename: &cob::TypeName,
+
                object_id: &cob::ObjectId,
+
            ) -> refspec::PatternString {
+
                pattern!("refs/namespaces/*")
                    .join(refname!("refs/drafts/cobs"))
                    .join(Component::from(typename))
                    .join(Component::from(object_id))
@@ -423,7 +414,6 @@ pub mod refs {

    pub mod workdir {
        use super::*;
-
        use format::name::component;

        /// Create a [`RefString`] that corresponds to `refs/heads/<branch>`.
        pub fn branch(branch: &RefStr) -> RefString {
@@ -483,7 +473,7 @@ pub fn remote_refs(url: &Url) -> Result<RandomMap<RemoteId, Refs>, ListRefsError
    Ok(remotes)
}

-
/// Parse a [`format::Qualified`] reference string while expecting the reference
+
/// Parse a [`fmt::Qualified`] reference string while expecting the reference
/// to start with `refs/namespaces`. If the namespace is not present, then an
/// error will be returned.
///
@@ -498,7 +488,7 @@ pub fn remote_refs(url: &Url) -> Result<RandomMap<RemoteId, Refs>, ListRefsError
/// The `T` can be specified when calling the function. For example, if you
/// wanted to parse the namespace as a `PublicKey`, then you would the function
/// like so, `parse_ref_namespaced::<PublicKey>(s)`.
-
pub fn parse_ref_namespaced<T>(s: &str) -> Result<(T, format::Qualified), RefError>
+
pub fn parse_ref_namespaced<T>(s: &str) -> Result<(T, fmt::Qualified), RefError>
where
    T: FromStr,
    T::Err: std::error::Error + Send + Sync + 'static,
@@ -510,7 +500,7 @@ where
    }
}

-
/// Parse a [`format::Qualified`] reference string. It will optionally return
+
/// Parse a [`fmt::Qualified`] reference string. It will optionally return
/// the namespace, if present.
///
/// The qualified form could be of the form: `refs/heads/main`,
@@ -527,15 +517,15 @@ where
/// The `T` can be specified when calling the function. For example, if you
/// wanted to parse the namespace as a `PublicKey`, then you would the function
/// like so, `parse_ref::<PublicKey>(s)`.
-
pub fn parse_ref<T>(s: &str) -> Result<(Option<T>, format::Qualified), RefError>
+
pub fn parse_ref<T>(s: &str) -> Result<(Option<T>, fmt::Qualified), RefError>
where
    T: FromStr,
    T::Err: std::error::Error + Send + Sync + 'static,
{
-
    let input = format::RefStr::try_from_str(s)?;
+
    let input = fmt::RefStr::try_from_str(s)?;
    match input.to_namespaced() {
        None => {
-
            let refname = Qualified::from_refstr(input)
+
            let refname = fmt::Qualified::from_refstr(input)
                .ok_or_else(|| RefError::Unqualified(input.to_owned()))?;

            Ok((None, refname))
@@ -573,7 +563,7 @@ pub fn initial_commit<'a>(
pub fn commit<'a>(
    repo: &'a raw::Repository,
    parent: &'a raw::Commit,
-
    target: &RefStr,
+
    target: &fmt::RefStr,
    message: &str,
    sig: &raw::Signature,
    tree: &raw::Tree,
@@ -588,7 +578,7 @@ pub fn commit<'a>(
pub fn empty_commit<'a>(
    repo: &'a raw::Repository,
    parent: &'a raw::Commit,
-
    target: &RefStr,
+
    target: &fmt::RefStr,
    message: &str,
    sig: &raw::Signature,
) -> Result<raw::Commit<'a>, raw::Error> {
@@ -611,7 +601,7 @@ pub fn write_tree<'r>(
    path: &Path,
    bytes: &[u8],
    repo: &'r raw::Repository,
-
) -> Result<raw::Tree<'r>, Error> {
+
) -> Result<raw::Tree<'r>, raw::Error> {
    let blob_id = repo.blob(bytes)?;
    let mut builder = repo.treebuilder(None)?;
    builder.insert(path, blob_id, 0o100_644)?;
@@ -697,7 +687,7 @@ pub fn fetch(repo: &raw::Repository, remote: &str) -> Result<(), raw::Error> {
pub fn push<'a>(
    repo: &raw::Repository,
    remote: &str,
-
    refspecs: impl IntoIterator<Item = (&'a Qualified<'a>, &'a Qualified<'a>)>,
+
    refspecs: impl IntoIterator<Item = (&'a fmt::Qualified<'a>, &'a fmt::Qualified<'a>)>,
) -> Result<(), raw::Error> {
    let refspecs = refspecs
        .into_iter()
modified crates/radicle/src/git/canonical.rs
@@ -19,11 +19,12 @@ use std::fmt;
use std::marker::PhantomData;
use std::ops::ControlFlow;

-
use git_ext::ref_format::Namespaced;
+
use crate::git::fmt::Namespaced;

use crate::prelude::Did;

-
use super::{Oid, Qualified};
+
use super::fmt::Qualified;
+
use crate::git::Oid;

/// A marker for the initial state of [`Canonical`], after construction using
/// [`Canonical::new`].
@@ -510,20 +511,19 @@ mod tests {

    /// Test helper to construct a Canonical and get the quorum
    fn quorum(
-
        heads: &[git::raw::Oid],
+
        heads: &[crate::git::Oid],
        threshold: usize,
-
        repo: &git::raw::Repository,
+
        repo: &crate::git::raw::Repository,
    ) -> Result<Oid, QuorumError> {
-
        let refname =
-
            git::refs::branch(git_ext::ref_format::RefStr::try_from_str("master").unwrap());
+
        let refname = git::refs::branch(crate::git::fmt::RefStr::try_from_str("master").unwrap());

        let mut delegates = Vec::new();
        for (i, head) in heads.iter().enumerate() {
            let signer = Device::mock_from_seed([(i + 1) as u8; 32]);
            let did = Did::from(signer.public_key());
            delegates.push(did);
-
            let ns = git::Component::from(signer.public_key());
-
            repo.reference(refname.with_namespace(ns).as_str(), *head, true, "")
+
            let ns = git::fmt::Component::from(signer.public_key());
+
            repo.reference(refname.with_namespace(ns).as_str(), head.into(), true, "")
                .unwrap();
        }

@@ -557,18 +557,18 @@ mod tests {
    fn test_quorum_properties() {
        let tmp = tempfile::tempdir().unwrap();
        let (repo, c0) = fixtures::repository(tmp.path());
-
        let c0: git::Oid = c0.into();
-
        let a1 = fixtures::commit("A1", &[*c0], &repo);
-
        let a2 = fixtures::commit("A2", &[*a1], &repo);
-
        let d1 = fixtures::commit("D1", &[*c0], &repo);
-
        let c1 = fixtures::commit("C1", &[*c0], &repo);
-
        let c2 = fixtures::commit("C2", &[*c1], &repo);
-
        let b2 = fixtures::commit("B2", &[*c1], &repo);
-
        let a1 = fixtures::commit("A1", &[*c0], &repo);
-
        let m1 = fixtures::commit("M1", &[*c2, *b2], &repo);
-
        let m2 = fixtures::commit("M2", &[*a1, *b2], &repo);
+
        let c0: crate::git::Oid = c0.into();
+
        let a1 = fixtures::commit("A1", &[c0.into()], &repo);
+
        let a2 = fixtures::commit("A2", &[a1.into()], &repo);
+
        let d1 = fixtures::commit("D1", &[c0.into()], &repo);
+
        let c1 = fixtures::commit("C1", &[c0.into()], &repo);
+
        let c2 = fixtures::commit("C2", &[c1.into()], &repo);
+
        let b2 = fixtures::commit("B2", &[c1.into()], &repo);
+
        let a1 = fixtures::commit("A1", &[c0.into()], &repo);
+
        let m1 = fixtures::commit("M1", &[c2.into(), b2.into()], &repo);
+
        let m2 = fixtures::commit("M2", &[a1.into(), b2.into()], &repo);
        let mut rng = fastrand::Rng::new();
-
        let choices = [*c0, *c1, *c2, *b2, *a1, *a2, *d1, *m1, *m2];
+
        let choices = [c0, c1, c2, b2, a1, a2, d1, m1, m2];

        for _ in 0..100 {
            let count = rng.usize(1..=choices.len());
@@ -591,11 +591,10 @@ mod tests {
    fn test_quorum_different_types() {
        let tmp = tempfile::tempdir().unwrap();
        let (repo, c0) = fixtures::repository(tmp.path());
-
        let c0: git::Oid = c0.into();
-
        let t0 = fixtures::tag("v1", "", *c0, &repo);
+
        let t0 = fixtures::tag("v1", "", c0, &repo);

        assert_matches!(
-
            quorum(&[*c0, *t0], 1, &repo),
+
            quorum(&[c0.into(), t0], 1, &repo),
            Err(QuorumError::DifferentTypes { .. })
        );
    }
modified crates/radicle/src/git/canonical/effects.rs
@@ -1,8 +1,9 @@
use std::collections::{BTreeMap, BTreeSet};

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

use super::{FoundObjects, GraphAheadBehind, MergeBase, Object};
@@ -40,7 +41,7 @@ pub enum FindObjectsError {
    },
    #[error("failed to find reference {refname} due to: {source}")]
    FindReference {
-
        refname: git::Namespaced<'static>,
+
        refname: git::fmt::Namespaced<'static>,
        source: Box<dyn std::error::Error + Send + Sync + 'static>,
    },
    #[error("failed to find objects")]
@@ -60,7 +61,7 @@ impl FindObjectsError {
        }
    }

-
    pub fn find_reference<E>(refname: git::Namespaced<'static>, err: E) -> Self
+
    pub fn find_reference<E>(refname: git::fmt::Namespaced<'static>, err: E) -> Self
    where
        E: std::error::Error + Send + Sync + 'static,
    {
@@ -170,7 +171,7 @@ pub struct GraphDescendant {

impl FindMergeBase for git::raw::Repository {
    fn merge_base(&self, a: Oid, b: Oid) -> Result<MergeBase, MergeBaseError> {
-
        self.merge_base(*a, *b)
+
        self.merge_base(a.into(), b.into())
            .map_err(|err| MergeBaseError {
                a,
                b,
@@ -190,7 +191,7 @@ impl Ancestry for git::raw::Repository {
        commit: Oid,
        upstream: Oid,
    ) -> Result<GraphAheadBehind, GraphDescendant> {
-
        self.graph_ahead_behind(*commit, *upstream)
+
        self.graph_ahead_behind(commit.into(), upstream.into())
            .map_err(|err| GraphDescendant {
                commit,
                upstream,
@@ -228,7 +229,7 @@ impl FindObjects for git::raw::Repository {
                log::warn!(target: "radicle", "Missing target for reference `{name}`");
                continue;
            };
-
            let object = match self.find_object(*oid, None) {
+
            let object = match self.find_object(oid.into(), None) {
                Ok(object) => Object::new(&object).ok_or_else(|| {
                    FindObjectsError::invalid_object_type(
                        *did,
modified crates/radicle/src/git/canonical/rules.rs
@@ -22,9 +22,9 @@ use thiserror::Error;
use crate::git;
use crate::git::canonical;
use crate::git::canonical::Canonical;
+
use crate::git::fmt::refspec::QualifiedPattern;
+
use crate::git::fmt::Qualified;
use crate::git::fmt::{refname, RefString};
-
use crate::git::refspec::QualifiedPattern;
-
use crate::git::Qualified;
use crate::identity::{doc, Did};

const ASTERISK: char = '*';
@@ -214,7 +214,7 @@ impl Ord for Pattern {
            }
        }

-
        use git::refspec::Component;
+
        use git::fmt::refspec::Component;

        fn cmp_component(lhs: Component<'_>, rhs: Component<'_>) -> ComponentOrdering {
            let (l, r) = (lhs.as_str(), rhs.as_str());
@@ -401,7 +401,10 @@ impl ValidRule {
    /// # Errors
    ///
    /// If the `name` reference begins with `refs/rad`.
-
    pub fn default_branch(did: Did, name: &git::RefStr) -> Result<(Pattern, Self), PatternError> {
+
    pub fn default_branch(
+
        did: Did,
+
        name: &git::fmt::RefStr,
+
    ) -> Result<(Pattern, Self), PatternError> {
        let pattern = Pattern::try_from(git::refs::branch(name).to_owned())?;
        let rule = Self {
            allow: ResolvedDelegates::Delegates(doc::Delegates::from(did)),
@@ -732,9 +735,7 @@ pub enum ValidationError {
#[derive(Debug, Error)]
pub enum CanonicalError {
    #[error(transparent)]
-
    Git(#[from] git::raw::Error),
-
    #[error(transparent)]
-
    References(#[from] git::ext::Error),
+
    Git(#[from] crate::git::raw::Error),
}

#[cfg(test)]
@@ -746,8 +747,8 @@ mod tests {

    use crate::crypto::{test::signer::MockSigner, Signer};
    use crate::git;
-
    use crate::git::refspec::qualified_pattern;
-
    use crate::git::RefString;
+
    use crate::git::fmt::qualified_pattern;
+
    use crate::git::fmt::RefString;
    use crate::identity::doc::Doc;
    use crate::identity::Visibility;
    use crate::node::device::Device;
@@ -782,7 +783,7 @@ mod tests {

    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 target = repo.find_object(commit.into(), None).unwrap();
        let tagger = repo.signature().unwrap();
        repo.tag(name.as_str(), &target, &tagger, name.as_str(), false)
            .unwrap()
@@ -1118,7 +1119,7 @@ mod tests {
            &repo,
            "heartwood".try_into().unwrap(),
            "Radicle Heartwood Protocol & Stack",
-
            git::refname!("master"),
+
            git::fmt::refname!("master"),
            Visibility::default(),
            &delegate,
            &storage,
@@ -1132,20 +1133,20 @@ mod tests {
        // Create tags and keep track of their OIDs
        //
        // follows the `refs/tags/release/candidates/*` rule
-
        let failing_tag = git::refname!("release/candidates/v1.0");
+
        let failing_tag = git::fmt::refname!("release/candidates/v1.0");
        let tags = [
            // follows the `refs/tags/*` rule
-
            git::refname!("v1.0"),
+
            git::fmt::refname!("v1.0"),
            // follows the `refs/tags/release/*` rule
-
            git::refname!("release/v1.0"),
+
            git::fmt::refname!("release/v1.0"),
            failing_tag.clone(),
            // follows the `refs/tags/*` rule
-
            git::refname!("qa/v1.0"),
+
            git::fmt::refname!("qa/v1.0"),
        ]
        .into_iter()
        .map(|name| {
            (
-
                git::lit::refs_tags(name.clone()).into(),
+
                git::fmt::lit::refs_tags(name.clone()).into(),
                tag(name, head, &repo),
            )
        })
@@ -1156,20 +1157,20 @@ mod tests {
            &rad::REMOTE_NAME,
            [
                (
-
                    &git::qualified!("refs/tags/v1.0"),
-
                    &git::qualified!("refs/tags/v1.0"),
+
                    &git::fmt::qualified!("refs/tags/v1.0"),
+
                    &git::fmt::qualified!("refs/tags/v1.0"),
                ),
                (
-
                    &git::qualified!("refs/tags/release/v1.0"),
-
                    &git::qualified!("refs/tags/release/v1.0"),
+
                    &git::fmt::qualified!("refs/tags/release/v1.0"),
+
                    &git::fmt::qualified!("refs/tags/release/v1.0"),
                ),
                (
-
                    &git::qualified!("refs/tags/release/candidates/v1.0"),
-
                    &git::qualified!("refs/tags/release/candidates/v1.0"),
+
                    &git::fmt::qualified!("refs/tags/release/candidates/v1.0"),
+
                    &git::fmt::qualified!("refs/tags/release/candidates/v1.0"),
                ),
                (
-
                    &git::qualified!("refs/tags/qa/v1.0"),
-
                    &git::qualified!("refs/tags/qa/v1.0"),
+
                    &git::fmt::qualified!("refs/tags/qa/v1.0"),
+
                    &git::fmt::qualified!("refs/tags/qa/v1.0"),
                ),
            ],
        )
@@ -1200,7 +1201,7 @@ mod tests {
        // All tags should succeed at getting their canonical commit other than the
        // candidates tag.
        let stored = storage.repository(rid).unwrap();
-
        let failing = git::Qualified::from(git::lit::refs_tags(failing_tag));
+
        let failing = git::fmt::Qualified::from(git::fmt::lit::refs_tags(failing_tag));
        for (refname, oid) in tags.into_iter() {
            let canonical = rules
                .canonical(refname.clone(), &stored)
modified crates/radicle/src/git/raw.rs
@@ -49,14 +49,3 @@ impl ErrorExt for git2::Error {
        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
@@ -10,9 +10,9 @@ use std::path::Path;
use std::str::FromStr;
use std::sync::LazyLock;

+
use crate::git::Oid;
use nonempty::NonEmpty;
use radicle_cob::type_name::{TypeName, TypeNameParse};
-
use radicle_git_ext::Oid;
use serde::{de, Deserialize, Serialize};
use thiserror::Error;

@@ -53,8 +53,6 @@ pub enum DocError {
    #[error(transparent)]
    Threshold(#[from] ThresholdError),
    #[error("git: {0}")]
-
    GitExt(#[from] git::Error),
-
    #[error("git: {0}")]
    Git(#[from] git::raw::Error),
    #[error("missing identity document")]
    Missing,
@@ -72,7 +70,6 @@ impl DocError {
    /// Whether this error is caused by the document not being found.
    pub fn is_not_found(&self) -> bool {
        match self {
-
            Self::GitExt(e) => e.is_not_found(),
            Self::Git(e) => e.is_not_found(),
            _ => false,
        }
@@ -810,7 +807,7 @@ impl Doc {
        if !self.is_delegate(&key.into()) {
            return Err(*key);
        }
-
        if key.verify(blob.as_bytes(), signature).is_err() {
+
        if key.verify(AsRef::<[u8]>::as_ref(&blob), signature).is_err() {
            return Err(*key);
        }
        Ok(())
@@ -855,7 +852,7 @@ impl Doc {
        G: crypto::signature::Signer<crypto::Signature>,
    {
        let (oid, bytes) = self.encode()?;
-
        let sig = signer.sign(oid.as_bytes());
+
        let sig = signer.sign(oid.as_ref());

        Ok((oid, bytes, sig))
    }
@@ -1132,7 +1129,7 @@ mod test {
            &repo,
            "heartwood".try_into().unwrap(),
            "Radicle Heartwood Protocol & Stack",
-
            git::refname!("master"),
+
            git::fmt::refname!("master"),
            Visibility::default(),
            &delegate,
            &storage,
@@ -1182,7 +1179,7 @@ mod test {
            &working,
            "heartwood".try_into().unwrap(),
            "Radicle Heartwood Protocol & Stack",
-
            git::refname!("master"),
+
            git::fmt::refname!("master"),
            Visibility::default(),
            &delegate,
            &storage,
modified crates/radicle/src/identity/doc/id.rs
@@ -1,7 +1,7 @@
use std::ops::Deref;
use std::{ffi::OsString, fmt, str::FromStr};

-
use git_ext::ref_format::{Component, RefString};
+
use crate::git::fmt::{Component, RefString};
use thiserror::Error;

use crate::git;
@@ -12,10 +12,10 @@ pub const RAD_PREFIX: &str = "rad:";

#[derive(Error, Debug)]
pub enum IdError {
-
    #[error("invalid git object id: {0}")]
-
    InvalidOid(#[from] git::raw::Error),
    #[error(transparent)]
    Multibase(#[from] multibase::Error),
+
    #[error("invalid length: expected {expected} bytes, got {actual} bytes")]
+
    Length { expected: usize, actual: usize },
}

/// A repository identifier.
@@ -68,14 +68,18 @@ impl RepoId {
    /// Eg. `z3XncAdkZjeK9mQS5Sdc4qhw98BUX`.
    ///
    pub fn canonical(&self) -> String {
-
        multibase::encode(multibase::Base::Base58Btc, self.0.as_bytes())
+
        multibase::encode(multibase::Base::Base58Btc, AsRef::<[u8]>::as_ref(&self.0))
    }

    pub fn from_canonical(input: &str) -> Result<Self, IdError> {
+
        const EXPECTED_LEN: usize = 20;
        let (_, bytes) = multibase::decode(input)?;
-
        let array: git::Oid = bytes.as_slice().try_into()?;
-

-
        Ok(Self(array))
+
        let bytes: [u8; EXPECTED_LEN] =
+
            bytes.try_into().map_err(|bytes: Vec<u8>| IdError::Length {
+
                expected: EXPECTED_LEN,
+
                actual: bytes.len(),
+
            })?;
+
        Ok(Self(crate::git::Oid::from_sha1(bytes)))
    }
}

modified crates/radicle/src/identity/doc/update.rs
@@ -208,7 +208,8 @@ pub fn verify(raw: RawDoc) -> Result<Doc, error::DocVerification> {
        .map(|rcrefs| rcrefs.and_then(|c| project.map(|p| (c, p))))
    {
        Ok(Some((crefs, project))) => {
-
            let default = git::Qualified::from(git::lit::refs_heads(project.default_branch()));
+
            let default =
+
                git::fmt::Qualified::from(git::fmt::lit::refs_heads(project.default_branch()));
            let matches = crefs
                .raw_rules()
                .matches(&default)
@@ -300,7 +301,7 @@ mod test {
    #[test]
    fn test_cannot_include_default_branch_rule() {
        let raw = arbitrary::gen::<RawDoc>(1);
-
        let branch = git::Qualified::from(git::lit::refs_heads(
+
        let branch = git::fmt::Qualified::from(git::fmt::lit::refs_heads(
            raw.project().unwrap().default_branch(),
        ));
        let raw = super::payload(
@@ -333,7 +334,7 @@ mod test {
    #[test]
    fn test_default_branch_rule_exists_after_verification() {
        let raw = arbitrary::gen::<RawDoc>(1);
-
        let branch = git::Qualified::from(git::lit::refs_heads(
+
        let branch = git::fmt::Qualified::from(git::fmt::lit::refs_heads(
            raw.project().unwrap().default_branch(),
        ));
        let raw = super::payload(
modified crates/radicle/src/identity/doc/update/error.rs
@@ -1,7 +1,7 @@
use thiserror::Error;

use crate::git;
-
use crate::git::RefString;
+
use crate::git::fmt::RefString;
use crate::identity::{doc::PayloadId, Did, DocError};

#[derive(Debug, Error)]
@@ -31,7 +31,7 @@ pub enum DocVerification {
    #[error("incompatible payloads: The rule(s) xyz.radicle.crefs.rules.{matches:?} matches the value of xyz.radicle.project.defaultBranch ('{default}'). Possible resolutions: Change the name of the default branch or remove the rule(s).")]
    DisallowDefault {
        matches: Vec<String>,
-
        default: git::Qualified<'static>,
+
        default: git::fmt::Qualified<'static>,
    },
}

modified crates/radicle/src/identity/project.rs
@@ -7,9 +7,9 @@ use serde::{
use thiserror::Error;

use crate::crypto;
+
use crate::git::BranchName;
use crate::identity::doc;
use crate::identity::doc::Payload;
-
use crate::storage::BranchName;

pub use crypto::PublicKey;

modified crates/radicle/src/lib.rs
@@ -7,7 +7,6 @@ pub extern crate radicle_crypto as crypto;

#[macro_use]
extern crate amplify;
-
extern crate radicle_git_ext as git_ext;

mod canonical;

@@ -42,10 +41,9 @@ pub mod prelude {
    use super::*;

    pub use crypto::{PublicKey, Verified};
+
    pub use git::BranchName;
    pub use identity::{project::Project, Did, Doc, RawDoc, RepoId};
    pub use node::{Alias, NodeId, Timestamp};
    pub use profile::Profile;
-
    pub use storage::{
-
        BranchName, ReadRepository, ReadStorage, SignRepository, WriteRepository, WriteStorage,
-
    };
+
    pub use storage::{ReadRepository, ReadStorage, SignRepository, WriteRepository, WriteStorage};
}
modified crates/radicle/src/node.rs
@@ -729,7 +729,7 @@ impl FetchResult {
        }
    }

-
    pub fn find_updated(&self, name: &git::RefStr) -> Option<RefUpdate> {
+
    pub fn find_updated(&self, name: &git::fmt::RefStr) -> Option<RefUpdate> {
        let updated = match self {
            Self::Success { updated, .. } => Some(updated),
            _ => None,
modified crates/radicle/src/node/events.rs
@@ -9,7 +9,8 @@ use std::time;

use crossbeam_channel as chan;

-
use crate::git::{Oid, Qualified};
+
use crate::git::fmt::Qualified;
+
use crate::git::Oid;
use crate::node;
use crate::prelude::*;
use crate::storage::{refs, RefUpdate};
modified crates/radicle/src/node/notifications.rs
@@ -7,7 +7,7 @@ use thiserror::Error;

use crate::cob;
use crate::cob::TypedId;
-
use crate::git::{BranchName, Qualified};
+
use crate::git::{fmt::Qualified, BranchName};
use crate::prelude::RepoId;
use crate::storage::{RefUpdate, RemoteId};

@@ -73,7 +73,7 @@ pub enum NotificationKindError {
    TypedId(#[from] cob::ParseIdentifierError),
    /// Invalid Git ref format.
    #[error("invalid ref format: {0}")]
-
    RefFormat(#[from] radicle_git_ext::ref_format::Error),
+
    RefFormat(#[from] crate::git::fmt::Error),
}

impl TryFrom<Qualified<'_>> for NotificationKind {
modified crates/radicle/src/node/notifications/store.rs
@@ -10,7 +10,9 @@ use sqlite as sql;
use thiserror::Error;

use crate::git;
-
use crate::git::{Oid, RefError, RefString};
+
use crate::git::fmt::RefString;
+
use crate::git::Oid;
+
use crate::git::RefError;
use crate::prelude::RepoId;
use crate::sql::transaction;
use crate::storage::RefUpdate;
@@ -40,7 +42,7 @@ pub enum Error {
    RefName(#[from] RefError),
    /// Invalid Git ref format.
    #[error("invalid ref format: {0}")]
-
    RefFormat(#[from] git_ext::ref_format::Error),
+
    RefFormat(#[from] crate::git::fmt::Error),
    /// Invalid notification kind.
    #[error("invalid notification kind: {0}")]
    NotificationKind(#[from] NotificationKindError),
@@ -414,10 +416,10 @@ mod parse {
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod test {
-
    use radicle_git_ext::ref_format::{qualified, refname};
+
    use crate::git::fmt::{qualified, refname};
+
    use crate::{cob, node::NodeId, test::arbitrary};

    use super::*;
-
    use crate::{cob, node::NodeId, test::arbitrary};

    #[test]
    fn test_clear() {
modified crates/radicle/src/node/refs/store.rs
@@ -6,7 +6,8 @@ use localtime::LocalTime;
use sqlite as sql;
use thiserror::Error;

-
use crate::git::{Oid, Qualified};
+
use crate::git::fmt::Qualified;
+
use crate::git::Oid;
use crate::node::Database;
use crate::node::NodeId;
use crate::prelude::RepoId;
@@ -176,7 +177,7 @@ impl Store for Database {
#[allow(clippy::unwrap_used)]
mod test {
    use super::*;
-
    use crate::git::qualified;
+
    use crate::git::fmt::qualified;
    use crate::test::arbitrary;
    use localtime::{LocalDuration, LocalTime};

modified crates/radicle/src/node/seed.rs
@@ -3,7 +3,6 @@ pub use store::{Error, Store};

use localtime::LocalTime;

-
use crate::git;
use crate::node::KnownAddress;
use crate::prelude::NodeId;
use crate::storage::{refs::RefsAt, ReadRepository, RemoteId};
@@ -14,8 +13,7 @@ use crate::storage::{refs::RefsAt, ReadRepository, RemoteId};
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct SyncedAt {
    /// Head of `rad/sigrefs`.
-
    #[cfg_attr(feature = "schemars", schemars(with = "crate::schemars_ext::git::Oid"))]
-
    pub oid: git_ext::Oid,
+
    pub oid: crate::git::Oid,
    /// When these refs were synced.
    #[serde(with = "crate::serde_ext::localtime::time")]
    #[cfg_attr(
@@ -27,7 +25,10 @@ pub struct SyncedAt {

impl SyncedAt {
    /// Load a new [`SyncedAt`] for the given remote.
-
    pub fn load<S: ReadRepository>(repo: &S, remote: RemoteId) -> Result<Self, git::ext::Error> {
+
    pub fn load<S: ReadRepository>(
+
        repo: &S,
+
        remote: RemoteId,
+
    ) -> Result<Self, crate::git::raw::Error> {
        let refs = RefsAt::new(repo, remote)?;
        let oid = refs.at;

@@ -35,7 +36,10 @@ impl SyncedAt {
    }

    /// Create a new [`SyncedAt`] given an OID, by looking up the timestamp in the repo.
-
    pub fn new<S: ReadRepository>(oid: git::ext::Oid, repo: &S) -> Result<Self, git::ext::Error> {
+
    pub fn new<S: ReadRepository>(
+
        oid: crate::git::Oid,
+
        repo: &S,
+
    ) -> Result<Self, crate::git::raw::Error> {
        let timestamp = repo.commit(oid)?.time();
        let timestamp = LocalTime::from_secs(timestamp.seconds() as u64);

modified crates/radicle/src/rad.rs
@@ -9,6 +9,7 @@ use thiserror::Error;
use crate::cob::ObjectId;
use crate::crypto::Verified;
use crate::git;
+
use crate::git::BranchName;
use crate::identity::doc;
use crate::identity::doc::{DocError, RepoId, Visibility};
use crate::identity::project::{Project, ProjectName};
@@ -17,18 +18,18 @@ use crate::storage::git::transport;
use crate::storage::git::Repository;
use crate::storage::refs::SignedRefs;
use crate::storage::RepositoryError;
-
use crate::storage::{BranchName, ReadRepository as _, RemoteId, SignRepository as _};
+
use crate::storage::{ReadRepository as _, RemoteId, SignRepository as _};
use crate::storage::{WriteRepository, WriteStorage};
use crate::{identity, storage};

/// Name of the radicle storage remote.
-
pub static REMOTE_NAME: LazyLock<git::RefString> = LazyLock::new(|| git::refname!("rad"));
+
pub static REMOTE_NAME: LazyLock<git::fmt::RefString> = LazyLock::new(|| git::fmt::refname!("rad"));
/// Name of the radicle storage remote.
-
pub static REMOTE_COMPONENT: LazyLock<git::Component> =
-
    LazyLock::new(|| git::fmt::name::component!("rad"));
+
pub static REMOTE_COMPONENT: LazyLock<git::fmt::Component> =
+
    LazyLock::new(|| git::fmt::component!("rad"));
/// Refname used for pushing patches.
-
pub static PATCHES_REFNAME: LazyLock<git::RefString> =
-
    LazyLock::new(|| git::refname!("refs/patches"));
+
pub static PATCHES_REFNAME: LazyLock<git::fmt::RefString> =
+
    LazyLock::new(|| git::fmt::refname!("refs/patches"));

#[derive(Error, Debug)]
pub enum InitError {
@@ -110,16 +111,16 @@ where

    git::configure_repository(repo)?;
    git::configure_remote(repo, &REMOTE_NAME, url, &url.clone().with_namespace(*pk))?;
-
    let branch = git::Qualified::from(git::fmt::lit::refs_heads(default_branch));
+
    let branch = git::fmt::Qualified::from(git::fmt::lit::refs_heads(default_branch));

    {
        // Push branch to storage.
        let stored = dunce::canonicalize(stored.path())?.display().to_string();

        // Pushes to default branch to the namespace of the `signer`.
-
        let pushspec = git::Refspec {
+
        let pushspec = git::fmt::refspec::Refspec {
            src: branch.clone(),
-
            dst: branch.with_namespace(git::Component::from(pk)),
+
            dst: branch.with_namespace(git::fmt::Component::from(pk)),
            force: false,
        }
        .to_string();
@@ -128,8 +129,8 @@ where
    }

    // 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 rad_remote = git::fmt::Qualified::from(git::fmt::lit::refs_remotes(&*REMOTE_COMPONENT))
+
        .join(default_branch);
    let oid = repo.refname_to_id(branch.as_str())?;
    repo.reference(
        rad_remote.as_str(),
@@ -215,7 +216,7 @@ where

    raw.reference(
        &canonical_branch.with_namespace(me.into()),
-
        *canonical_head,
+
        canonical_head.into(),
        true,
        &format!("creating default branch for {me}"),
    )?;
@@ -281,10 +282,10 @@ pub fn checkout<P: AsRef<Path>, S: storage::ReadStorage>(
    {
        // Fetch remote head to working copy.

-
        let fetchspec = git::Refspec {
-
            src: git::refspec::pattern!("refs/heads/*"),
-
            dst: git::Qualified::from(git::lit::refs_remotes(&*REMOTE_NAME))
-
                .to_pattern(git::refspec::STAR)
+
        let fetchspec = git::fmt::refspec::Refspec {
+
            src: git::fmt::pattern!("refs/heads/*"),
+
            dst: git::fmt::Qualified::from(git::fmt::lit::refs_remotes(&*REMOTE_NAME))
+
                .to_pattern(git::fmt::refspec::STAR)
                .into_patternstring(),
            force: false,
        }
@@ -456,15 +457,15 @@ pub fn repo_jj_git_root() -> Result<git::raw::Repository, JujutsuGitRootError> {
/// Setup patch upstream branch such that `git push` updates the patch.
pub fn setup_patch_upstream<'a>(
    patch: &ObjectId,
-
    patch_head: git::Oid,
-
    working: &'a git::raw::Repository,
-
    remote: &git::RefString,
+
    patch_head: crate::git::Oid,
+
    working: &'a crate::git::raw::Repository,
+
    remote: &git::fmt::RefString,
    force: bool,
-
) -> Result<Option<git::raw::Branch<'a>>, git::ext::Error> {
+
) -> Result<Option<crate::git::raw::Branch<'a>>, crate::git::raw::Error> {
    let head = working.head()?;

    // Don't do anything in case we're not on the patch branch.
-
    if head.peel_to_commit()?.id() != *patch_head {
+
    if patch_head != head.peel_to_commit()?.id() {
        return Ok(None);
    }
    let Ok(r) = head.resolve() else {
@@ -476,18 +477,18 @@ pub fn setup_patch_upstream<'a>(
        return Ok(None);
    }

-
    let branch = git::raw::Branch::wrap(r);
+
    let branch = crate::git::raw::Branch::wrap(r);

    // Only set the upstream if it's missing or `force` is `true`
    if branch.upstream().is_ok() && !force {
        return Ok(None);
    }

-
    let name: Option<git::RefString> = branch.name()?.and_then(|b| b.try_into().ok());
+
    let name: Option<git::fmt::RefString> = branch.name()?.and_then(|b| b.try_into().ok());
    let remote_branch = git::refs::workdir::patch_upstream(patch);
    let remote_branch = working.reference(
        &remote_branch,
-
        *patch_head,
+
        patch_head.into(),
        true,
        "Create remote tracking branch for patch",
    )?;
@@ -498,7 +499,7 @@ pub fn setup_patch_upstream<'a>(
            git::set_upstream(working, remote, name.as_str(), git::refs::patch(patch))?;
        }
    }
-
    Ok(Some(git::raw::Branch::wrap(remote_branch)))
+
    Ok(Some(crate::git::raw::Branch::wrap(remote_branch)))
}

#[cfg(test)]
@@ -508,12 +509,12 @@ mod tests {

    use pretty_assertions::assert_eq;

-
    use crate::git::{name::component, qualified};
    use crate::identity::Did;
    use crate::storage::git::transport;
    use crate::storage::git::Storage;
    use crate::storage::{ReadStorage, RemoteRepository as _};
    use crate::test::fixtures;
+
    use git::fmt::{component, qualified};

    use super::*;

@@ -531,7 +532,7 @@ mod tests {
            &repo,
            "acme".try_into().unwrap(),
            "Acme's repo",
-
            git::refname!("master"),
+
            git::fmt::refname!("master"),
            Visibility::default(),
            &signer,
            &storage,
@@ -553,19 +554,19 @@ mod tests {

        // Test canonical refs.
        assert_eq!(refs.head(component!("master")).unwrap(), head);
-
        assert_eq!(project_repo.raw().refname_to_id("HEAD").unwrap(), *head);
+
        assert_eq!(head, project_repo.raw().refname_to_id("HEAD").unwrap());
        assert_eq!(
+
            head,
            project_repo
                .raw()
                .refname_to_id("refs/heads/master")
                .unwrap(),
-
            *head
        );

        assert_eq!(remotes[&public_key].refs, refs);
        assert_eq!(project.name(), "acme");
        assert_eq!(project.description(), "Acme's repo");
-
        assert_eq!(project.default_branch(), &git::refname!("master"));
+
        assert_eq!(project.default_branch(), &git::fmt::refname!("master"));
        assert_eq!(doc.delegates().first(), &Did::from(public_key));
    }

@@ -586,7 +587,7 @@ mod tests {
            &original,
            "acme".try_into().unwrap(),
            "Acme's repo",
-
            git::refname!("master"),
+
            git::fmt::refname!("master"),
            Visibility::default(),
            &alice,
            &storage,
@@ -622,7 +623,7 @@ mod tests {
            &original,
            "acme".try_into().unwrap(),
            "Acme's repo",
-
            git::refname!("master"),
+
            git::fmt::refname!("master"),
            Visibility::default(),
            &signer,
            &storage,
modified crates/radicle/src/schemars_ext.rs
@@ -90,23 +90,9 @@ pub(crate) mod localtime {
}

pub(crate) mod git {
-
    use super::*;
-

-
    /// See [`crate::git::Oid`]
-
    /// See [`::git_ext::Oid`]
-
    /// See [`::git::raw::Oid`]
-
    ///
-
    /// A Git Object Identifier in hexadecimal encoding.
-
    #[derive(JsonSchema)]
-
    #[schemars(
-
        remote = "git::raw::Oid",
-
        description = "A Git Object Identifier (SHA-1 or SHA-256 hash) in hexadecimal encoding."
-
    )]
-
    pub(crate) struct Oid(
-
        #[schemars(regex(pattern = r"^([0-9a-fA-F]{64}|[0-9a-fA-F]{40})$"))] String,
-
    );
-

-
    /// See [`crate::git::RefString`]
-
    #[derive(JsonSchema)]
-
    pub(crate) struct RefString(String);
+
    pub(crate) mod fmt {
+
        /// See [`crate::git::fmt::RefString`]
+
        #[derive(schemars::JsonSchema)]
+
        pub(crate) struct RefString(String);
+
    }
}
modified crates/radicle/src/storage.rs
@@ -10,15 +10,16 @@ use nonempty::NonEmpty;
use serde::{Deserialize, Serialize};
use thiserror::Error;

+
pub use crate::git::Oid;
use crypto::{PublicKey, Unverified, Verified};
pub use git::{Validation, Validations};
-
pub use radicle_git_ext::Oid;

use crate::cob;
use crate::collections::RandomMap;
+
use crate::git::canonical;
+
use crate::git::fmt::{refspec::PatternString, refspec::Refspec, Qualified, RefStr, RefString};
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::git::RefError;
use crate::identity::{doc, Did, PayloadError};
use crate::identity::{Doc, DocAt, DocError};
use crate::identity::{Identity, RepoId};
@@ -27,10 +28,8 @@ use crate::node::SyncedAt;
use crate::storage::git::NAMESPACES_GLOB;
use crate::storage::refs::Refs;

-
use self::git::UserInfo;
use self::refs::{RefsAt, SignedRefs};
-

-
pub type BranchName = git::RefString;
+
use crate::git::UserInfo;

/// Basic repository information.
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -69,7 +68,9 @@ impl Namespaces {
            Namespaces::Followed(pks) => pks
                .iter()
                .map(|pk| {
-
                    let ns = pk.to_namespace().with_pattern(git::refspec::STAR);
+
                    let ns = pk
+
                        .to_namespace()
+
                        .with_pattern(crate::git::fmt::refspec::STAR);
                    Refspec {
                        src: ns.clone(),
                        dst: ns,
@@ -114,9 +115,7 @@ pub enum RepositoryError {
    #[error(transparent)]
    Payload(#[from] PayloadError),
    #[error(transparent)]
-
    Git(#[from] git::raw::Error),
-
    #[error(transparent)]
-
    GitExt(#[from] git_ext::Error),
+
    Git(#[from] crate::git::raw::Error),
    #[error(transparent)]
    Quorum(#[from] canonical::error::QuorumError),
    #[error(transparent)]
@@ -135,7 +134,6 @@ impl RepositoryError {
    pub fn is_not_found(&self) -> bool {
        match self {
            Self::Storage(e) => e.is_not_found(),
-
            Self::GitExt(e) => e.is_not_found(),
            Self::Git(e) => e.is_not_found(),
            _ => false,
        }
@@ -154,9 +152,7 @@ pub enum Error {
    #[error(transparent)]
    Refs(#[from] refs::Error),
    #[error("git: {0}")]
-
    Git(#[from] git::raw::Error),
-
    #[error("git: {0}")]
-
    Ext(#[from] git::ext::Error),
+
    Git(#[from] crate::git::raw::Error),
    #[error("invalid repository identifier {0:?}")]
    InvalidId(std::ffi::OsString),
    #[error("i/o: {0}")]
@@ -180,7 +176,7 @@ impl Error {
#[allow(clippy::large_enum_variant)]
pub enum FetchError {
    #[error("git: {0}")]
-
    Git(#[from] git::raw::Error),
+
    Git(#[from] crate::git::raw::Error),
    #[error("i/o: {0}")]
    Io(#[from] io::Error),
    #[error(transparent)]
@@ -205,36 +201,31 @@ pub enum RefUpdate {
    Updated {
        #[cfg_attr(
            feature = "schemars",
-
            schemars(with = "crate::schemars_ext::git::RefString")
+
            schemars(with = "crate::schemars_ext::git::fmt::RefString")
        )]
        name: RefString,
-
        #[cfg_attr(feature = "schemars", schemars(with = "crate::schemars_ext::git::Oid"))]
        old: Oid,
-
        #[cfg_attr(feature = "schemars", schemars(with = "crate::schemars_ext::git::Oid"))]
        new: Oid,
    },
    Created {
        #[cfg_attr(
            feature = "schemars",
-
            schemars(with = "crate::schemars_ext::git::RefString")
+
            schemars(with = "crate::schemars_ext::git::fmt::RefString")
        )]
        name: RefString,
-
        #[cfg_attr(feature = "schemars", schemars(with = "crate::schemars_ext::git::Oid"))]
        oid: Oid,
    },
    Deleted {
        #[cfg_attr(
            feature = "schemars",
-
            schemars(with = "crate::schemars_ext::git::RefString")
+
            schemars(with = "crate::schemars_ext::git::fmt::RefString")
        )]
        name: RefString,
-
        #[cfg_attr(feature = "schemars", schemars(with = "crate::schemars_ext::git::Oid"))]
        oid: Oid,
    },
    Skipped {
        #[cfg_attr(feature = "schemars", schemars(with = "String"))]
        name: RefString,
-
        #[cfg_attr(feature = "schemars", schemars(with = "crate::schemars_ext::git::Oid"))]
        oid: Oid,
    },
}
@@ -504,7 +495,7 @@ 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, git::raw::Error>;
+
    fn is_empty(&self) -> Result<bool, crate::git::raw::Error>;

    /// The [`Path`] to the git repository.
    fn path(&self) -> &Path;
@@ -514,10 +505,10 @@ pub trait ReadRepository: Sized + ValidateRepository {
        &self,
        commit: Oid,
        path: P,
-
    ) -> Result<git::raw::Blob, git_ext::Error>;
+
    ) -> Result<crate::git::raw::Blob, crate::git::raw::Error>;

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

    /// Get the head of this repository.
    ///
@@ -541,7 +532,7 @@ pub trait ReadRepository: Sized + ValidateRepository {
    fn identity_head(&self) -> Result<Oid, RepositoryError>;

    /// Get the identity head of a specific remote.
-
    fn identity_head_of(&self, remote: &RemoteId) -> Result<Oid, git::ext::Error>;
+
    fn identity_head_of(&self, remote: &RemoteId) -> Result<Oid, crate::git::raw::Error>;

    /// Get the root commit of the canonical identity branch.
    fn identity_root(&self) -> Result<Oid, RepositoryError>;
@@ -577,28 +568,28 @@ pub trait ReadRepository: Sized + ValidateRepository {
        &self,
        remote: &RemoteId,
        reference: &Qualified,
-
    ) -> Result<git::raw::Reference, git_ext::Error>;
+
    ) -> Result<crate::git::raw::Reference, crate::git::raw::Error>;

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

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

    /// Check if the underlying ODB contains the given `oid`.
-
    fn contains(&self, oid: Oid) -> Result<bool, git::raw::Error>;
+
    fn contains(&self, oid: Oid) -> Result<bool, crate::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>;
+
    fn is_ancestor_of(&self, ancestor: Oid, head: Oid) -> Result<bool, crate::git::raw::Error>;

    /// Get the object id of a reference under the given remote.
    fn reference_oid(
        &self,
        remote: &RemoteId,
        reference: &Qualified,
-
    ) -> Result<Oid, git::raw::Error>;
+
    ) -> Result<Oid, crate::git::raw::Error>;

    /// Get all references of the given remote.
    fn references_of(&self, remote: &RemoteId) -> Result<Refs, Error>;
@@ -610,8 +601,8 @@ pub trait ReadRepository: Sized + ValidateRepository {
    /// commit pointed to by the tag is returned, and not the [`Oid`] of the tag itsself.
    fn references_glob(
        &self,
-
        pattern: &git::PatternStr,
-
    ) -> Result<Vec<(Qualified, Oid)>, git::ext::Error>;
+
        pattern: &crate::git::fmt::refspec::PatternStr,
+
    ) -> Result<Vec<(Qualified, Oid)>, crate::git::raw::Error>;

    /// Get repository delegates.
    fn delegates(&self) -> Result<NonEmpty<Did>, RepositoryError> {
@@ -632,7 +623,7 @@ pub trait ReadRepository: Sized + ValidateRepository {
    fn identity_doc_at(&self, head: Oid) -> Result<DocAt, DocError>;

    /// Get the merge base of two commits.
-
    fn merge_base(&self, left: &Oid, right: &Oid) -> Result<Oid, git::ext::Error>;
+
    fn merge_base(&self, left: &Oid, right: &Oid) -> Result<Oid, crate::git::raw::Error>;
}

/// Access the remotes of a repository.
@@ -697,7 +688,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) -> &git::raw::Repository;
+
    fn raw(&self) -> &crate::git::raw::Repository;
}

/// Allows signing refs.
modified crates/radicle/src/storage/git.rs
@@ -27,24 +27,26 @@ use crate::storage::{
    ReadRepository, ReadStorage, Remote, Remotes, RepositoryInfo, SetHead, SignRepository,
    WriteRepository, WriteStorage,
};
-
use crate::{git, node};
+
use crate::{git, git::Oid, node};

-
pub use crate::git::{
-
    ext, raw, refname, refspec, Oid, PatternStr, Qualified, RefError, RefString, UserInfo,
+
use crate::git::fmt::{
+
    refname, refspec, refspec::PatternStr, refspec::PatternString, Qualified, RefString,
};
+
use crate::git::RefError;
+
use crate::git::UserInfo;
pub use crate::storage::{Error, RepositoryError};

use super::refs::RefsAt;
use super::{RemoteId, RemoteRepository, ValidateRepository};

-
pub static NAMESPACES_GLOB: LazyLock<git::refspec::PatternString> =
-
    LazyLock::new(|| git::refspec::pattern!("refs/namespaces/*"));
+
pub static NAMESPACES_GLOB: LazyLock<PatternString> =
+
    LazyLock::new(|| git::fmt::pattern!("refs/namespaces/*"));
pub static SIGREFS_GLOB: LazyLock<refspec::PatternString> =
-
    LazyLock::new(|| git::refspec::pattern!("refs/namespaces/*/rad/sigrefs"));
-
pub static CANONICAL_IDENTITY: LazyLock<git::Qualified> = LazyLock::new(|| {
-
    git::Qualified::from_components(
-
        git::name::component!("rad"),
-
        git::name::component!("id"),
+
    LazyLock::new(|| git::fmt::pattern!("refs/namespaces/*/rad/sigrefs"));
+
pub static CANONICAL_IDENTITY: LazyLock<git::fmt::Qualified> = LazyLock::new(|| {
+
    git::fmt::Qualified::from_components(
+
        git::fmt::component!("rad"),
+
        git::fmt::component!("id"),
        None,
    )
});
@@ -52,7 +54,7 @@ pub static CANONICAL_IDENTITY: LazyLock<git::Qualified> = LazyLock::new(|| {
/// A parsed Git reference.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Ref {
-
    pub oid: git::Oid,
+
    pub oid: crate::git::Oid,
    pub name: RefString,
    pub namespace: Option<RemoteId>,
}
@@ -446,9 +448,9 @@ impl Repository {
                continue;
            }

-
            let glob = git::refname!("refs/namespaces")
-
                .join(git::Component::from(&id))
-
                .with_pattern(git::refspec::STAR);
+
            let glob = git::fmt::refname!("refs/namespaces")
+
                .join(git::fmt::Component::from(&id))
+
                .with_pattern(git::fmt::refspec::STAR);
            let refs = match self.references_glob(&glob) {
                Ok(refs) => refs,
                Err(e) => {
@@ -476,7 +478,7 @@ impl Repository {
        doc: &Doc,
        storage: &S,
        signer: &Device<G>,
-
    ) -> Result<(Self, git::Oid), RepositoryError>
+
    ) -> Result<(Self, crate::git::Oid), RepositoryError>
    where
        G: crypto::signature::Signer<crypto::Signature>,
        S: WriteStorage,
@@ -486,7 +488,7 @@ impl Repository {
        let repo = Self::create(paths::repository(storage, &id), id, storage.info())?;
        let oid = repo.backend.blob(&doc_bytes)?; // Store document blob in repository.

-
        debug_assert_eq!(oid, *doc_oid);
+
        debug_assert_eq!(doc_oid, oid);

        let commit = doc.init(&repo, signer)?;

@@ -661,48 +663,52 @@ impl ReadRepository for Repository {
        self.backend.path()
    }

-
    fn blob_at<P: AsRef<Path>>(&self, commit: Oid, path: P) -> Result<git::raw::Blob, git::Error> {
-
        let commit = self.backend.find_commit(*commit)?;
+
    fn blob_at<P: AsRef<Path>>(
+
        &self,
+
        commit_id: Oid,
+
        path: P,
+
    ) -> Result<git::raw::Blob, git::raw::Error> {
+
        let commit = self.backend.find_commit(git::raw::Oid::from(commit_id))?;
        let tree = commit.tree()?;
        let entry = tree.get_path(path.as_ref())?;
        let obj = entry.to_object(&self.backend)?;
-
        let blob = obj.into_blob().map_err(|_| {
-
            git::Error::NotFound(git::NotFound::NoSuchBlob(
-
                path.as_ref().display().to_string(),
-
            ))
-
        })?;
+
        let blob = obj.into_blob().map_err(|_|
+
            crate::git::raw::Error::new(
+
                crate::git::raw::ErrorCode::NotFound,
+
                crate::git::raw::ErrorClass::None,
+
                format!("Path '{}' in tree of commit {commit_id} was expected to be a blob, but is not.", path.as_ref().display()),
+
            )
+
        )?;

        Ok(blob)
    }

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

    fn reference(
        &self,
        remote: &RemoteId,
-
        name: &git::Qualified,
-
    ) -> Result<git::raw::Reference, git::Error> {
+
        name: &git::fmt::Qualified,
+
    ) -> Result<git::raw::Reference, git::raw::Error> {
        let name = name.with_namespace(remote.into());
-
        self.backend.find_reference(&name).map_err(git::Error::from)
+
        self.backend.find_reference(&name)
    }

    fn reference_oid(
        &self,
        remote: &RemoteId,
-
        reference: &git::Qualified,
-
    ) -> Result<Oid, git::raw::Error> {
+
        reference: &git::fmt::Qualified,
+
    ) -> Result<Oid, crate::git::raw::Error> {
        let name = reference.with_namespace(remote.into());
        let oid = self.backend.refname_to_id(&name)?;

        Ok(oid.into())
    }

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

    fn revwalk(&self, head: Oid) -> Result<git::raw::Revwalk, git::raw::Error> {
@@ -712,14 +718,13 @@ impl ReadRepository for Repository {
        Ok(revwalk)
    }

-
    fn contains(&self, oid: Oid) -> Result<bool, raw::Error> {
+
    fn contains(&self, oid: Oid) -> Result<bool, crate::git::raw::Error> {
        self.backend.odb().map(|odb| odb.exists(oid.into()))
    }

-
    fn is_ancestor_of(&self, ancestor: Oid, head: Oid) -> Result<bool, git::Error> {
+
    fn is_ancestor_of(&self, ancestor: Oid, head: Oid) -> Result<bool, crate::git::raw::Error> {
        self.backend
            .graph_descendant_of(head.into(), ancestor.into())
-
            .map_err(git::Error::from)
    }

    fn references_of(&self, remote: &RemoteId) -> Result<Refs, Error> {
@@ -735,12 +740,14 @@ impl ReadRepository for Repository {
            let oid = e.resolve()?.target().ok_or(Error::InvalidRef)?;
            let (_, category, _, _) = refname.non_empty_components();

+
            use git::fmt::{component, name};
+

            if [
-
                git::name::HEADS,
-
                git::name::TAGS,
-
                git::name::NOTES,
-
                &git::name::component!("rad"),
-
                &git::name::component!("cobs"),
+
                name::HEADS,
+
                name::TAGS,
+
                name::NOTES,
+
                &component!("rad"),
+
                &component!("cobs"),
            ]
            .contains(&category.as_ref())
            {
@@ -753,7 +760,7 @@ impl ReadRepository for Repository {
    fn references_glob(
        &self,
        pattern: &PatternStr,
-
    ) -> Result<Vec<(Qualified, Oid)>, git::ext::Error> {
+
    ) -> Result<Vec<(Qualified, Oid)>, crate::git::raw::Error> {
        let mut refs = Vec::new();

        for r in self.backend.references_glob(pattern)? {
@@ -765,8 +772,8 @@ impl ReadRepository for Repository {

            if let Some(name) = r
                .name()
-
                .and_then(|n| git::RefStr::try_from_str(n).ok())
-
                .and_then(git::Qualified::from_refstr)
+
                .and_then(|n| git::fmt::RefStr::try_from_str(n).ok())
+
                .and_then(git::fmt::Qualified::from_refstr)
            {
                refs.push((name.to_owned(), oid.into()));
            }
@@ -823,9 +830,8 @@ impl ReadRepository for Repository {
        }
    }

-
    fn identity_head_of(&self, remote: &RemoteId) -> Result<Oid, git::ext::Error> {
+
    fn identity_head_of(&self, remote: &RemoteId) -> Result<Oid, crate::git::raw::Error> {
        self.reference_oid(remote, &git::refs::storage::IDENTITY_BRANCH)
-
            .map_err(git::ext::Error::from)
    }

    fn identity_root(&self) -> Result<Oid, RepositoryError> {
@@ -864,7 +870,7 @@ impl ReadRepository for Repository {
            let blob = Doc::blob_at(root, self)?;

            // We've got an identity that goes back to the correct root.
-
            if blob.id() == **self.id {
+
            if *self.id == blob.id() {
                let identity = Identity::get(&root.into(), self)?;

                return Ok(identity.head());
@@ -873,11 +879,10 @@ impl ReadRepository for Repository {
        Err(DocError::Missing.into())
    }

-
    fn merge_base(&self, left: &Oid, right: &Oid) -> Result<Oid, git::ext::Error> {
+
    fn merge_base(&self, left: &Oid, right: &Oid) -> Result<Oid, crate::git::raw::Error> {
        self.backend
-
            .merge_base(**left, **right)
+
            .merge_base(left.into(), right.into())
            .map(Oid::from)
-
            .map_err(git::ext::Error::from)
    }
}

@@ -897,7 +902,7 @@ impl WriteRepository for Repository {
        }
        log::debug!(target: "storage", "Setting ref: {} -> {}", &branch_ref, new);
        self.raw()
-
            .reference(&branch_ref, *new, true, "set-local-branch (radicle)")?;
+
            .reference(&branch_ref, new.into(), true, "set-local-branch (radicle)")?;

        log::debug!(target: "storage", "Setting ref: {head_ref} -> {branch_ref}");
        self.raw()
@@ -910,7 +915,7 @@ impl WriteRepository for Repository {
        log::debug!(target: "storage", "Setting ref: {} -> {}", *CANONICAL_IDENTITY, commit);
        self.raw().reference(
            CANONICAL_IDENTITY.as_str(),
-
            *commit,
+
            commit.into(),
            true,
            "set-local-branch (radicle)",
        )?;
@@ -925,7 +930,7 @@ impl WriteRepository for Repository {
        let refname = git::refs::storage::id_root(remote);

        self.raw()
-
            .reference(refname.as_str(), *root, true, "set-id-root (radicle)")?;
+
            .reference(refname.as_str(), root.into(), true, "set-id-root (radicle)")?;

        Ok(())
    }
@@ -1104,7 +1109,7 @@ mod tests {
        git::commit(
            &working,
            &head,
-
            &git::RefString::try_from(format!("refs/remotes/{alice}/heads/master")).unwrap(),
+
            &git::fmt::RefString::try_from(format!("refs/remotes/{alice}/heads/master")).unwrap(),
            "Second commit",
            &sig,
            &head.tree().unwrap(),
modified crates/radicle/src/storage/git/cob.rs
@@ -12,6 +12,7 @@ use storage::SignRepository;
use storage::ValidateRepository;

use crate::git;
+
use crate::git::fmt::*;
use crate::git::*;
use crate::identity;
use crate::identity::doc::DocError;
@@ -34,8 +35,6 @@ pub enum ObjectsError {
    Convert(#[from] cob::object::storage::convert::Error),
    #[error(transparent)]
    Git(#[from] git::raw::Error),
-
    #[error(transparent)]
-
    GitExt(#[from] git_ext::Error),
}

#[derive(Error, Debug)]
@@ -290,7 +289,7 @@ impl<R: storage::ReadRepository> ReadRepository for DraftStore<'_, R> {
        self.repo.path()
    }

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

@@ -298,39 +297,39 @@ impl<R: storage::ReadRepository> ReadRepository for DraftStore<'_, R> {
        self.repo.revwalk(head)
    }

-
    fn contains(&self, oid: Oid) -> Result<bool, raw::Error> {
+
    fn contains(&self, oid: Oid) -> Result<bool, crate::git::raw::Error> {
        self.repo.contains(oid)
    }

-
    fn is_ancestor_of(&self, ancestor: Oid, head: Oid) -> Result<bool, git_ext::Error> {
+
    fn is_ancestor_of(&self, ancestor: Oid, head: Oid) -> Result<bool, crate::git::raw::Error> {
        self.repo.is_ancestor_of(ancestor, head)
    }

    fn blob_at<P: AsRef<Path>>(
        &self,
-
        oid: git_ext::Oid,
+
        oid: Oid,
        path: P,
-
    ) -> Result<git::raw::Blob, git_ext::Error> {
+
    ) -> Result<git::raw::Blob, git::raw::Error> {
        self.repo.blob_at(oid, path)
    }

-
    fn blob(&self, oid: git_ext::Oid) -> Result<raw::Blob, ext::Error> {
+
    fn blob(&self, oid: Oid) -> Result<crate::git::raw::Blob, crate::git::raw::Error> {
        self.repo.blob(oid)
    }

    fn reference(
        &self,
        remote: &RemoteId,
-
        reference: &git::Qualified,
-
    ) -> Result<git::raw::Reference, git_ext::Error> {
+
        reference: &git::fmt::Qualified,
+
    ) -> Result<git::raw::Reference, git::raw::Error> {
        self.repo.reference(remote, reference)
    }

    fn reference_oid(
        &self,
        remote: &RemoteId,
-
        reference: &git::Qualified,
-
    ) -> Result<git_ext::Oid, git::raw::Error> {
+
        reference: &git::fmt::Qualified,
+
    ) -> Result<Oid, crate::git::raw::Error> {
        self.repo.reference_oid(remote, reference)
    }

@@ -340,8 +339,8 @@ impl<R: storage::ReadRepository> ReadRepository for DraftStore<'_, R> {

    fn references_glob(
        &self,
-
        pattern: &git::PatternStr,
-
    ) -> Result<Vec<(fmt::Qualified, Oid)>, git::ext::Error> {
+
        pattern: &git::fmt::refspec::PatternStr,
+
    ) -> Result<Vec<(fmt::Qualified, Oid)>, crate::git::raw::Error> {
        self.repo.references_glob(pattern)
    }

@@ -357,7 +356,7 @@ impl<R: storage::ReadRepository> ReadRepository for DraftStore<'_, R> {
        self.repo.identity_head()
    }

-
    fn identity_head_of(&self, remote: &RemoteId) -> Result<Oid, super::ext::Error> {
+
    fn identity_head_of(&self, remote: &RemoteId) -> Result<Oid, crate::git::raw::Error> {
        self.repo.identity_head_of(remote)
    }

@@ -373,14 +372,14 @@ impl<R: storage::ReadRepository> ReadRepository for DraftStore<'_, R> {
        self.repo.canonical_identity_head()
    }

-
    fn merge_base(&self, left: &Oid, right: &Oid) -> Result<Oid, git::ext::Error> {
+
    fn merge_base(&self, left: &Oid, right: &Oid) -> Result<Oid, crate::git::raw::Error> {
        self.repo.merge_base(left, right)
    }
}

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

modified crates/radicle/src/storage/refs.rs
@@ -13,7 +13,6 @@ use serde::{Deserialize, Serialize};
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;
@@ -48,7 +47,7 @@ pub enum Error {
    #[error("invalid reference")]
    InvalidRef,
    #[error("missing identity root reference '{0}'")]
-
    MissingIdentityRoot(git::RefString),
+
    MissingIdentityRoot(git::fmt::RefString),
    #[error("missing identity object '{0}'")]
    MissingIdentity(Oid),
    #[error("mismatched identity: local {local}, remote {remote}")]
@@ -57,26 +56,23 @@ pub enum Error {
    Ref(#[from] git::RefError),
    #[error(transparent)]
    Git(#[from] git::raw::Error),
-
    #[error(transparent)]
-
    GitExt(#[from] git_ext::Error),
}

impl Error {
    /// Whether this error is caused by a reference not being found.
    pub fn is_not_found(&self) -> bool {
        match self {
-
            Self::GitExt(e) => e.is_not_found(),
            Self::Git(e) => e.is_not_found(),
            _ => false,
        }
    }
}

-
// TODO(finto): we should turn `git::RefString` to `git::Qualified`,
+
// TODO(finto): we should turn `git::fmt::RefString` to `git::fmt::Qualified`,
// since all these refs SHOULD be `Qualified`.
/// The published state of a local repository.
#[derive(Default, Clone, Debug, PartialEq, Eq, Serialize)]
-
pub struct Refs(BTreeMap<git::RefString, Oid>);
+
pub struct Refs(BTreeMap<git::fmt::RefString, Oid>);

impl Refs {
    /// Verify the given signature on these refs, and return [`SignedRefs`] on success.
@@ -102,13 +98,13 @@ impl Refs {
    }

    /// Get a particular ref.
-
    pub fn get(&self, name: &git::Qualified) -> Option<Oid> {
+
    pub fn get(&self, name: &git::fmt::Qualified) -> Option<Oid> {
        self.0.get(name.to_ref_string().as_refstr()).copied()
    }

    /// Get a particular head ref.
-
    pub fn head(&self, name: impl AsRef<git::RefStr>) -> Option<Oid> {
-
        let branch = git::refname!("refs/heads").join(name);
+
    pub fn head(&self, name: impl AsRef<git::fmt::RefStr>) -> Option<Oid> {
+
        let branch = git::fmt::refname!("refs/heads").join(name);
        self.0.get(&branch).copied()
    }

@@ -123,8 +119,8 @@ impl Refs {
                .split_once(' ')
                .ok_or(canonical::Error::InvalidFormat)?;

-
            let name = git::RefString::try_from(name)?;
-
            let oid = Oid::from_str(oid)?;
+
            let name = git::fmt::RefString::try_from(name)?;
+
            let oid = Oid::from_str(oid).map_err(|_| canonical::Error::InvalidFormat)?;

            if oid.is_zero() {
                continue;
@@ -148,15 +144,15 @@ impl Refs {
}

impl IntoIterator for Refs {
-
    type Item = (git::RefString, Oid);
-
    type IntoIter = std::collections::btree_map::IntoIter<git::RefString, Oid>;
+
    type Item = (git::fmt::RefString, Oid);
+
    type IntoIter = std::collections::btree_map::IntoIter<git::fmt::RefString, Oid>;

    fn into_iter(self) -> Self::IntoIter {
        self.0.into_iter()
    }
}

-
impl From<Refs> for BTreeMap<git::RefString, Oid> {
+
impl From<Refs> for BTreeMap<git::fmt::RefString, Oid> {
    fn from(refs: Refs) -> Self {
        refs.0
    }
@@ -168,14 +164,14 @@ impl<V> From<SignedRefs<V>> for Refs {
    }
}

-
impl From<BTreeMap<git::RefString, Oid>> for Refs {
-
    fn from(refs: BTreeMap<git::RefString, Oid>) -> Self {
+
impl From<BTreeMap<git::fmt::RefString, Oid>> for Refs {
+
    fn from(refs: BTreeMap<git::fmt::RefString, Oid>) -> Self {
        Self(refs)
    }
}

impl Deref for Refs {
-
    type Target = BTreeMap<git::RefString, Oid>;
+
    type Target = BTreeMap<git::fmt::RefString, Oid>;

    fn deref(&self) -> &Self::Target {
        &self.0
@@ -297,7 +293,7 @@ impl SignedRefs<Verified> {
            Some(SignedRefsAt { sigrefs, at }) if sigrefs.signature == self.signature => {
                return Ok(Updated::Unchanged { oid: at });
            }
-
            Some(SignedRefsAt { at, .. }) => Some(raw.find_commit(*at)?),
+
            Some(SignedRefsAt { at, .. }) => Some(raw.find_commit(at.into())?),
            None => None,
        };

@@ -386,12 +382,14 @@ pub struct RefsAt {
    )]
    pub remote: RemoteId,
    /// The commit SHA that `rad/sigrefs` points to.
-
    #[cfg_attr(feature = "schemars", schemars(with = "crate::schemars_ext::git::Oid"))]
    pub at: Oid,
}

impl RefsAt {
-
    pub fn new<S: ReadRepository>(repo: &S, remote: RemoteId) -> Result<Self, git::raw::Error> {
+
    pub fn new<S: ReadRepository>(
+
        repo: &S,
+
        remote: RemoteId,
+
    ) -> Result<Self, crate::git::raw::Error> {
        let at = repo.reference_oid(&remote, &storage::refs::SIGREFS_BRANCH)?;
        Ok(RefsAt { remote, at })
    }
@@ -400,7 +398,7 @@ impl RefsAt {
        SignedRefsAt::load_at(self.at, self.remote, repo)
    }

-
    pub fn path(&self) -> &git::Qualified {
+
    pub fn path(&self) -> &git::fmt::Qualified {
        &SIGREFS_BRANCH
    }
}
@@ -446,7 +444,7 @@ impl SignedRefsAt {
        })
    }

-
    pub fn iter(&self) -> impl Iterator<Item = (&git::RefString, &Oid)> {
+
    pub fn iter(&self) -> impl Iterator<Item = (&git::fmt::RefString, &Oid)> {
        self.sigrefs.refs.iter()
    }
}
@@ -515,7 +513,7 @@ mod tests {
            &paris_repo,
            "paris".try_into().unwrap(),
            "Paris repository",
-
            git::refname!("master"),
+
            git::fmt::refname!("master"),
            Default::default(),
            &alice,
            storage,
@@ -528,7 +526,7 @@ mod tests {
            &london_repo,
            "london".try_into().unwrap(),
            "London repository",
-
            git::refname!("master"),
+
            git::fmt::refname!("master"),
            Default::default(),
            &alice,
            storage,
@@ -585,7 +583,7 @@ mod tests {
            let bob_head = git::empty_commit(
                &bob_working,
                &paris_head,
-
                git::refname!("refs/heads/master").as_refstr(),
+
                git::fmt::refname!("refs/heads/master").as_refstr(),
                "Bob's commit",
                &bob_sig,
            )
@@ -602,9 +600,9 @@ mod tests {

            assert_eq!(
                sigrefs
-
                    .get(&git_ext::ref_format::qualified!("refs/heads/master"))
+
                    .get(&crate::git::fmt::qualified!("refs/heads/master"))
                    .unwrap(),
-
                bob_head.id().into()
+
                bob_head.id()
            );
            (sigrefs, bob_head.id())
        };
@@ -616,10 +614,10 @@ mod tests {
                .unwrap();
            assert_ne!(
                alice_paris_sigrefs
-
                    .get(&git_ext::ref_format::qualified!("refs/heads/master"))
+
                    .get(&crate::git::fmt::qualified!("refs/heads/master"))
                    .unwrap(),
                bob_paris_sigrefs
-
                    .get(&git_ext::ref_format::qualified!("refs/heads/master"))
+
                    .get(&crate::git::fmt::qualified!("refs/heads/master"))
                    .unwrap()
            );
        }
@@ -651,7 +649,8 @@ mod tests {
        london
            .raw()
            .reference(
-
                git::refs::storage::branch_of(bob.public_key(), &git::refname!("master")).as_str(),
+
                git::refs::storage::branch_of(bob.public_key(), &git::fmt::refname!("master"))
+
                    .as_str(),
                bob_head,
                false,
                "",
modified crates/radicle/src/test.rs
@@ -34,7 +34,7 @@ pub fn fetch<W: WriteRepository>(
    };

    callbacks.update_tips(|name, old, new| {
-
        if let Ok(name) = crate::git::RefString::try_from(name) {
+
        if let Ok(name) = git::fmt::RefString::try_from(name) {
            if name.to_namespaced().is_some() {
                updates.push(RefUpdate::from(name, old, new));
                // Returning `true` ensures the process is not aborted.
@@ -77,13 +77,8 @@ pub mod setup {
    use crate::crypto::test::signer::MockSigner;
    use crate::node::device::Device;
    use crate::storage::git::transport::remote;
-
    use crate::{
-
        git,
-
        profile::Home,
-
        rad::REMOTE_NAME,
-
        test::{fixtures, storage::git::Repository},
-
        Storage,
-
    };
+
    use crate::storage::git::Repository;
+
    use crate::{git, profile::Home, rad::REMOTE_NAME, test::fixtures, Storage};
    use crate::{prelude::*, rad};

    /// A node.
@@ -188,7 +183,8 @@ pub mod setup {
            &self,
            blobs: impl IntoIterator<Item = (S, T)>,
        ) -> BranchWith {
-
            let refname = git::Qualified::from(git::lit::refs_heads(git::refname!("master")));
+
            let refname =
+
                git::fmt::Qualified::from(git::fmt::lit::refs_heads(git::fmt::refname!("master")));
            let base = self.checkout.refname_to_id(refname.as_str()).unwrap();
            let parent = self.checkout.find_commit(base).unwrap();
            let oid = commit(&self.checkout, &refname, blobs, &[&parent]);
@@ -297,10 +293,10 @@ pub mod setup {

    pub fn commit<S: AsRef<Path>, T: AsRef<[u8]>>(
        repo: &git::raw::Repository,
-
        refname: &git::Qualified,
+
        refname: &git::fmt::Qualified,
        blobs: impl IntoIterator<Item = (S, T)>,
        parents: &[&git::raw::Commit<'_>],
-
    ) -> git::Oid {
+
    ) -> crate::git::Oid {
        let tree = {
            let mut tb = repo.treebuilder(None).unwrap();
            for (name, blob) in blobs.into_iter() {
modified crates/radicle/src/test/arbitrary.rs
@@ -27,14 +27,14 @@ use crate::{cob, git};

pub fn oid() -> storage::Oid {
    let oid_bytes: [u8; 20] = gen(1);
-
    storage::Oid::try_from(oid_bytes.as_slice()).unwrap()
+
    storage::Oid::from_sha1(oid_bytes)
}

pub fn entry_id() -> cob::EntryId {
    self::oid()
}

-
pub fn refstring(len: usize) -> git::RefString {
+
pub fn refstring(len: usize) -> git::fmt::RefString {
    let mut buf = Vec::<u8>::new();
    for _ in 0..len {
        buf.push(fastrand::u8(0x61..0x7a));
@@ -135,7 +135,7 @@ impl Arbitrary for Project {
        let description = iter::repeat_with(|| rng.alphanumeric())
            .take(length * 2)
            .collect();
-
        let default_branch: git::RefString = iter::repeat_with(|| rng.alphanumeric())
+
        let default_branch: git::fmt::RefString = iter::repeat_with(|| rng.alphanumeric())
            .take(length)
            .collect::<String>()
            .try_into()
@@ -207,7 +207,7 @@ impl Arbitrary for SignedRefs<Unverified> {

impl Arbitrary for Refs {
    fn arbitrary(g: &mut qcheck::Gen) -> Self {
-
        let mut refs: BTreeMap<git::RefString, storage::Oid> = BTreeMap::new();
+
        let mut refs: BTreeMap<git::fmt::RefString, storage::Oid> = BTreeMap::new();
        let mut bytes: [u8; 20] = [0; 20];
        let names = &[
            "heads/master",
@@ -225,8 +225,8 @@ impl Arbitrary for Refs {
                for byte in &mut bytes {
                    *byte = u8::arbitrary(g);
                }
-
                let oid = storage::Oid::try_from(&bytes[..]).unwrap();
-
                let name = git::RefString::try_from(*name).unwrap();
+
                let oid = storage::Oid::from_sha1(bytes);
+
                let name = git::fmt::RefString::try_from(*name).unwrap();

                refs.insert(name, oid);
            }
@@ -273,7 +273,7 @@ impl Arbitrary for storage::Remote<crypto::Unverified> {
impl Arbitrary for RepoId {
    fn arbitrary(g: &mut qcheck::Gen) -> Self {
        let bytes = <[u8; 20]>::arbitrary(g);
-
        let oid = git::Oid::try_from(bytes.as_slice()).unwrap();
+
        let oid = crate::git::Oid::from_sha1(bytes);

        RepoId::from(oid)
    }
modified crates/radicle/src/test/fixtures.rs
@@ -57,7 +57,7 @@ where
            &repo,
            name.try_into().unwrap(),
            desc,
-
            git::refname!("master"),
+
            git::fmt::refname!("master"),
            Visibility::default(),
            signer,
            &storage,
@@ -92,7 +92,7 @@ where
        &working,
        "acme".try_into().unwrap(),
        "Acme's repository",
-
        git::refname!("master"),
+
        git::fmt::refname!("master"),
        Visibility::default(),
        signer,
        storage,
@@ -131,19 +131,21 @@ fn repository_with<P: AsRef<Path>>(
        config.set_str("user.name", USER_NAME).unwrap();
        config.set_str("user.email", USER_EMAIL).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 = {
        let commit = git::commit(
            &repo,
            &head,
-
            git::refname!("refs/heads/master").as_refstr(),
+
            git::fmt::refname!("refs/heads/master").as_refstr(),
            "Second commit",
            &sig,
            &tree,
@@ -203,7 +205,7 @@ pub fn tag(
}

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

        rng.fill(&mut buffer);
@@ -244,7 +246,7 @@ pub fn populate(repo: &git::raw::Repository, scale: usize) -> Vec<git::Qualified
        )
        .unwrap();

-
        refs.push(git::Qualified::from_refstr(refstr).unwrap());
+
        refs.push(git::fmt::Qualified::from_refstr(refstr).unwrap());
    }
    refs
}
@@ -279,7 +281,7 @@ pub mod gen {
        let oid = git::commit(
            &repo,
            &head,
-
            git::refname!("refs/heads/master").as_refstr(),
+
            git::fmt::refname!("refs/heads/master").as_refstr(),
            string(16).as_str(),
            &sig,
            &tree,
modified crates/radicle/src/test/storage.rs
@@ -4,7 +4,8 @@ use std::io;
use std::path::{Path, PathBuf};
use std::str::FromStr;

-
use git_ext::ref_format as fmt;
+
pub use crate::git;
+
use crate::git::fmt;

use crate::crypto::Verified;
use crate::identity::doc::{Doc, DocAt, DocError, RawDoc, RepoId};
@@ -18,7 +19,7 @@ use super::{arbitrary, fixtures};
#[derive(Clone, Debug)]
pub struct MockStorage {
    pub path: PathBuf,
-
    pub info: git::UserInfo,
+
    pub info: crate::git::UserInfo,

    /// All refs keyed by RID.
    /// Each value is a map of refs keyed by node Id (public key).
@@ -67,7 +68,7 @@ impl MockStorage {
impl ReadStorage for MockStorage {
    type Repository = MockRepository;

-
    fn info(&self) -> &git::UserInfo {
+
    fn info(&self) -> &crate::git::UserInfo {
        &self.info
    }

@@ -215,10 +216,12 @@ impl ReadRepository for MockRepository {
        todo!()
    }

-
    fn commit(&self, oid: Oid) -> Result<git::raw::Commit, git_ext::Error> {
-
        Err(git_ext::Error::NotFound(git_ext::NotFound::NoSuchObject(
-
            *oid,
-
        )))
+
    fn commit(&self, oid: Oid) -> Result<git::raw::Commit, git::raw::Error> {
+
        Err(git::raw::Error::new(
+
            git::raw::ErrorCode::NotFound,
+
            git::raw::ErrorClass::None,
+
            format!("commit {oid} not found"),
+
        ))
    }

    fn revwalk(&self, _head: Oid) -> Result<git::raw::Revwalk, git::raw::Error> {
@@ -232,39 +235,39 @@ impl ReadRepository for MockRepository {
            .any(|sigrefs| sigrefs.at == oid || sigrefs.refs.values().any(|oid_| *oid_ == oid)))
    }

-
    fn is_ancestor_of(&self, _ancestor: Oid, _head: Oid) -> Result<bool, git_ext::Error> {
+
    fn is_ancestor_of(&self, _ancestor: Oid, _head: Oid) -> Result<bool, crate::git::raw::Error> {
        Ok(true)
    }

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

    fn blob_at<P: AsRef<std::path::Path>>(
        &self,
-
        _oid: git_ext::Oid,
+
        _oid: Oid,
        _path: P,
-
    ) -> Result<git::raw::Blob, git_ext::Error> {
+
    ) -> Result<git::raw::Blob, git::raw::Error> {
        todo!()
    }

    fn reference(
        &self,
        _remote: &RemoteId,
-
        _reference: &git::Qualified,
-
    ) -> Result<git::raw::Reference, git_ext::Error> {
+
        _reference: &git::fmt::Qualified,
+
    ) -> Result<git::raw::Reference, git::raw::Error> {
        todo!()
    }

    fn reference_oid(
        &self,
        remote: &RemoteId,
-
        reference: &git::Qualified,
-
    ) -> Result<git_ext::Oid, git::raw::Error> {
+
        reference: &crate::git::fmt::Qualified,
+
    ) -> Result<Oid, crate::git::raw::Error> {
        let not_found = || {
-
            git::raw::Error::new(
-
                git::raw::ErrorCode::NotFound,
-
                git::raw::ErrorClass::Reference,
+
            crate::git::raw::Error::new(
+
                crate::git::raw::ErrorCode::NotFound,
+
                crate::git::raw::ErrorClass::Reference,
                format!("could not find {reference} for {remote}"),
            )
        };
@@ -283,8 +286,8 @@ impl ReadRepository for MockRepository {

    fn references_glob(
        &self,
-
        _pattern: &git::PatternStr,
-
    ) -> Result<Vec<(fmt::Qualified, Oid)>, git::ext::Error> {
+
        _pattern: &crate::git::fmt::refspec::PatternStr,
+
    ) -> Result<Vec<(fmt::Qualified, Oid)>, crate::git::raw::Error> {
        todo!()
    }

@@ -300,7 +303,7 @@ impl ReadRepository for MockRepository {
        self.canonical_identity_head()
    }

-
    fn identity_head_of(&self, _remote: &RemoteId) -> Result<Oid, git::ext::Error> {
+
    fn identity_head_of(&self, _remote: &RemoteId) -> Result<Oid, crate::git::raw::Error> {
        Ok(self.doc.commit)
    }

@@ -316,7 +319,7 @@ impl ReadRepository for MockRepository {
        Ok(self.doc.commit)
    }

-
    fn merge_base(&self, _left: &Oid, _right: &Oid) -> Result<Oid, git::ext::Error> {
+
    fn merge_base(&self, _left: &Oid, _right: &Oid) -> Result<Oid, crate::git::raw::Error> {
        todo!()
    }
}
@@ -342,7 +345,7 @@ impl WriteRepository for MockRepository {
        todo!()
    }

-
    fn set_user(&self, _info: &git::UserInfo) -> Result<(), Error> {
+
    fn set_user(&self, _info: &crate::git::UserInfo) -> Result<(), Error> {
        todo!()
    }
}