Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
Improvements to Git OID Handling
Merged lorenz opened 6 days ago

With Git 3 on the horizon, which will make SHA-256 object identifiers the default, this patch implements first steps to prepare integration of SHA-256 by means of a clean up around code locations that will be affected.

13 files changed +66 -80 aa177b04 bda92b21
modified crates/radicle-cli/src/commands/watch.rs
@@ -42,7 +42,7 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
            thread::sleep(interval);
            let oid = reference(&repo, &nid, &qualified)?;
            if oid != initial {
-
                term::info!("{}", oid.unwrap_or(git::raw::Oid::zero().into()));
+
                term::info!("{}", oid.unwrap_or(git::Oid::ZERO_SHA1));
                break;
            }
            if now.elapsed()? >= timeout {
modified crates/radicle-cli/src/git/unified_diff.rs
@@ -336,7 +336,7 @@ impl Encode for FileHeader {
                w.meta(format!("new file mode {:o}", u32::from(new.mode.clone())))?;
                w.meta(format!(
                    "index {}..{}",
-
                    term::format::oid(git::Oid::sha1_zero()),
+
                    term::format::oid(git::Oid::ZERO_SHA1),
                    term::format::oid(*new.oid),
                ))?;

@@ -358,7 +358,7 @@ impl Encode for FileHeader {
                w.meta(format!(
                    "index {}..{}",
                    term::format::oid(*old.oid),
-
                    term::format::oid(git::Oid::sha1_zero())
+
                    term::format::oid(git::Oid::ZERO_SHA1)
                ))?;

                w.meta(format!("--- a/{}", path.display()))?;
modified crates/radicle-core/src/repo.rs
@@ -14,8 +14,8 @@ pub const RAD_PREFIX: &str = "rad:";
pub enum IdError {
    #[error(transparent)]
    Multibase(#[from] multibase::Error),
-
    #[error("invalid length: expected {expected} bytes, got {actual} bytes")]
-
    Length { expected: usize, actual: usize },
+
    #[error("invalid length: expected {} bytes, got {actual} bytes", Oid::LEN_SHA1)]
+
    Length { actual: usize },
    #[error(fmt = fmt_mismatched_base_encoding)]
    MismatchedBaseEncoding {
        input: String,
@@ -106,12 +106,10 @@ impl RepoId {
    ///
    /// [multibase]: https://github.com/multiformats/multibase?tab=readme-ov-file#multibase-table
    pub fn from_canonical(input: &str) -> Result<Self, IdError> {
-
        const EXPECTED_LEN: usize = 20;
        let (base, bytes) = multibase::decode(input)?;
        Self::guard_base_encoding(input, base)?;
-
        let bytes: [u8; EXPECTED_LEN] =
+
        let bytes: [u8; Oid::LEN_SHA1] =
            bytes.try_into().map_err(|bytes: Vec<u8>| IdError::Length {
-
                expected: EXPECTED_LEN,
                actual: bytes.len(),
            })?;
        Ok(Self(Oid::from_sha1(bytes)))
@@ -315,10 +313,7 @@ pub mod arbitrary {
#[cfg(feature = "qcheck")]
impl qcheck::Arbitrary for RepoId {
    fn arbitrary(g: &mut qcheck::Gen) -> Self {
-
        let bytes = <[u8; 20]>::arbitrary(g);
-
        let oid = radicle_oid::Oid::from_sha1(bytes);
-

-
        RepoId::from(oid)
+
        RepoId::from(radicle_oid::Oid::arbitrary(g))
    }
}

modified crates/radicle-fetch/src/git/mem.rs
@@ -42,10 +42,10 @@ impl Refdb {
            .fold(Applied::default(), |mut ap, update| match update {
                Update::Direct { name, target, .. } => {
                    let name = name.into_qualified().into_owned();
-
                    let prev = match self.0.insert(name.clone(), target) {
-
                        Some(prev) => prev,
-
                        None => radicle::git::raw::Oid::zero().into(),
-
                    };
+
                    let prev = self
+
                        .0
+
                        .insert(name.clone(), target)
+
                        .unwrap_or(radicle::git::Oid::ZERO_SHA1);
                    ap.updated.push(RefUpdate::Updated {
                        name: name.to_ref_string(),
                        old: prev,
modified crates/radicle-node/src/test/handle.rs
@@ -118,7 +118,7 @@ impl radicle::node::Handle for Handle {

        Ok(RefsAt {
            remote: self.nid()?,
-
            at: Oid::sha1_zero(),
+
            at: Oid::ZERO_SHA1,
        })
    }

modified crates/radicle-oid/src/lib.rs
@@ -74,12 +74,10 @@ extern crate alloc;
#[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]),
+
    Sha1([u8; Self::LEN_SHA1]),
}

/// Conversions to/from SHA-1.
@@ -87,19 +85,25 @@ pub enum Oid {
// 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 {
+
    /// The length of a SHA-1 object identifier in bytes.
+
    pub const LEN_SHA1: usize = 20;
+

+
    /// A SHA-1 object identifier with all digest bytes set to zero.
+
    /// This is sometimes used as a sentinel value to indicate the absence of
+
    /// an object.
+
    /// To compare whether an object identifier is zero, prefer the method
+
    /// [`Oid::is_zero`] over checking equality with this constant.
+
    pub const ZERO_SHA1: Self = Self::Sha1([0u8; Self::LEN_SHA1]);
+

+
    pub fn from_sha1(digest: [u8; Self::LEN_SHA1]) -> Self {
        Self::Sha1(digest)
    }

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

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

/// Interaction with zero.
@@ -130,11 +134,11 @@ impl From<Oid> for alloc::boxed::Box<[u8]> {
}

pub mod str {
-
    use super::{Oid, SHA1_DIGEST_LEN};
+
    use super::Oid;
    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;
+
    pub(super) const SHA1_DIGEST_STR_LEN: usize = Oid::LEN_SHA1 * 2;

    impl str::FromStr for Oid {
        type Err = error::ParseOidError;
@@ -147,8 +151,8 @@ pub mod str {
                return Err(Len(len));
            }

-
            let mut bytes = [0u8; SHA1_DIGEST_LEN];
-
            for i in 0..SHA1_DIGEST_LEN {
+
            let mut bytes = [0u8; Oid::LEN_SHA1];
+
            for i in 0..Oid::LEN_SHA1 {
                bytes[i] = u8::from_str_radix(&s[i * 2..=i * 2 + 1], 16)
                    .map_err(|source| At { index: i, source })?;
            }
@@ -230,7 +234,7 @@ pub mod str {
                "0000000000000000000000000000000000000000"
                    .parse::<Oid>()
                    .unwrap(),
-
                Oid::sha1_zero()
+
                Oid::ZERO_SHA1
            );
        }

@@ -319,7 +323,7 @@ mod fmt {
        #[test]
        fn zero() {
            assert_eq!(
-
                Oid::sha1_zero().to_string(),
+
                Oid::ZERO_SHA1.to_string(),
                "0000000000000000000000000000000000000000"
            );
        }
@@ -367,7 +371,7 @@ mod gix {
        fn from(other: Other) -> Self {
            match other {
                Other::Sha1(digest) => Self::Sha1(digest),
-
                _ => panic!("unexpected SHA variant was returned for `gix_hash::ObjectId`"),
+
                _ => unimplemented!("conversion from {other:?} into radicle_oid::Oid"),
            }
        }
    }
@@ -384,7 +388,7 @@ mod gix {
        fn eq(&self, other: &Other) -> bool {
            match (self, other) {
                (Oid::Sha1(a), Other::Sha1(b)) => a == b,
-
                _ => panic!("unexpected SHA variant was returned for `gix_hash::ObjectId`"),
+
                _ => unimplemented!("conversion from {other:?} into radicle_oid::Oid"),
            }
        }
    }
@@ -404,7 +408,7 @@ mod gix {

        #[test]
        fn zero() {
-
            assert!(Oid::sha1_zero() == Other::null(Kind::Sha1));
+
            assert!(Oid::ZERO_SHA1 == Other::null(Kind::Sha1));
        }
    }
}
@@ -451,7 +455,7 @@ mod git2 {

        #[test]
        fn zero() {
-
            assert!(Oid::sha1_zero() == Other::zero());
+
            assert!(Oid::ZERO_SHA1 == Other::zero());
        }
    }
}
@@ -465,9 +469,7 @@ mod test {

        impl Arbitrary for Oid {
            fn arbitrary(g: &mut Gen) -> Self {
-
                let slice = [0u8; SHA1_DIGEST_LEN];
-
                g.fill(slice);
-
                Self::Sha1(slice)
+
                Self::Sha1(<[u8; Oid::LEN_SHA1]>::arbitrary(g))
            }
        }
    }
@@ -508,10 +510,11 @@ mod serde {
                    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)"
+
                            "a Git object identifier (SHA-1 digest in hexadecimal notation; {} characters; {} bytes)",
+
                            crate::str::SHA1_DIGEST_STR_LEN,
+
                            Oid::LEN_SHA1
                        )
                    }

@@ -568,9 +571,12 @@ mod schemars {
        }

        fn json_schema(_: &mut SchemaGenerator) -> Schema {
-
            use crate::{SHA1_DIGEST_LEN, str::SHA1_DIGEST_STR_LEN};
+
            use crate::str::SHA1_DIGEST_STR_LEN;
            json_schema!({
-
                "description": format!("A Git object identifier (SHA-1 digest in hexadecimal notation; {SHA1_DIGEST_STR_LEN} characters; {SHA1_DIGEST_LEN} bytes)"),
+
                "description": format!(
+
                    "A Git object identifier (SHA-1 digest in hexadecimal notation; {SHA1_DIGEST_STR_LEN} characters; {} bytes)",
+
                    Oid::LEN_SHA1,
+
                ),
                "type": "string",
                "maxLength": SHA1_DIGEST_STR_LEN,
                "minLength": SHA1_DIGEST_STR_LEN,
modified crates/radicle-protocol/src/service/message.rs
@@ -690,7 +690,6 @@ mod tests {
    use fastrand;
    use localtime::LocalTime;
    use qcheck_macros::quickcheck;
-
    use radicle::git::raw;
    use radicle::test::arbitrary;

    use crate::wire::Decode as _;
@@ -701,7 +700,7 @@ mod tests {
    fn test_ref_remote_limit() {
        let mut refs = BoundedVec::<_, REF_REMOTE_LIMIT>::new();
        let signer = Device::mock();
-
        let at = raw::Oid::zero().into();
+
        let at = git::Oid::ZERO_SHA1;

        assert_eq!(refs.capacity(), REF_REMOTE_LIMIT);

@@ -759,7 +758,7 @@ mod tests {
    fn prop_refs_announcement_signing(rid: RepoId) {
        let signer = Device::mock_rng(&mut fastrand::Rng::new());
        let timestamp = Timestamp::EPOCH;
-
        let at = raw::Oid::zero().into();
+
        let at = git::Oid::ZERO_SHA1;
        let refs = BoundedVec::collect_from(
            &mut [RefsAt {
                remote: *signer.public_key(),
modified crates/radicle-protocol/src/wire.rs
@@ -7,7 +7,6 @@ pub use message::{AddressType, MessageType};

use std::convert::TryFrom;
use std::fmt::Debug;
-
use std::mem;
use std::ops::Deref;
use std::str::FromStr;
use std::string::FromUtf8Error;
@@ -22,7 +21,6 @@ use cypheraddr::tor;
use radicle::crypto::{PublicKey, Signature};
use radicle::git;
use radicle::git::fmt;
-
use radicle::git::raw;
use radicle::identity::RepoId;
use radicle::node;
use radicle::node::Alias;
@@ -45,8 +43,11 @@ pub type Size = u16;

#[derive(thiserror::Error, Debug)]
pub enum Invalid {
-
    #[error("invalid Git object identifier size: expected {expected}, got {actual}")]
-
    Oid { expected: usize, actual: usize },
+
    #[error(
+
        "invalid Git object identifier size: expected {}, got {actual}",
+
        git::Oid::LEN_SHA1
+
    )]
+
    Oid { actual: usize },
    #[error(transparent)]
    Bounded(#[from] crate::bounded::Error),
    #[error("invalid filter size: {actual}")]
@@ -382,23 +383,13 @@ where

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

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

-
        if len != LEN_EXPECTED {
-
            return Err(Invalid::Oid {
-
                expected: LEN_EXPECTED,
-
                actual: len,
-
            }
-
            .into());
+
        if len != git::Oid::LEN_SHA1 {
+
            return Err(Invalid::Oid { actual: len }.into());
        }

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

-
        Ok(oid)
+
        Ok(git::Oid::Sha1(Decode::decode(buf)?))
    }
}

@@ -633,8 +624,8 @@ mod tests {
    }

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

    #[test]
modified crates/radicle-protocol/src/worker/fetch/error.rs
@@ -2,7 +2,7 @@ use std::io;

use thiserror::Error;

-
use radicle::{cob, git::raw, identity, storage};
+
use radicle::{cob, 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] raw::Error),
+
    Git(#[from] radicle::git::raw::Error),
    #[error(transparent)]
    Storage(#[from] storage::Error),
    #[error(transparent)]
modified crates/radicle/src/node/notifications/store.rs
@@ -380,7 +380,7 @@ mod parse {
                    })
                })
            })
-
            .unwrap_or(Ok(git::raw::Oid::zero().into()))?;
+
            .unwrap_or(Ok(git::Oid::ZERO_SHA1))?;
        let new = row
            .try_read::<Option<&str>, _>("new")?
            .map(|oid| {
@@ -391,7 +391,7 @@ mod parse {
                    })
                })
            })
-
            .unwrap_or(Ok(git::raw::Oid::zero().into()))?;
+
            .unwrap_or(Ok(git::Oid::ZERO_SHA1))?;
        let update = RefUpdate::from(RefString::try_from(refstr)?, old, new);
        let (namespace, qualified) = git::parse_ref(refstr)?;
        let timestamp = row.try_read::<i64, _>("timestamp")?;
modified crates/radicle/src/storage/refs.rs
@@ -177,7 +177,7 @@ impl Refs {
        let mut buf = String::new();

        for (name, oid) in self.0.iter() {
-
            debug_assert_ne!(oid, &Oid::sha1_zero());
+
            debug_assert!(!oid.is_zero());
            debug_assert_ne!(name, &SIGREFS_BRANCH.to_ref_string());

            buf.push_str(&oid.to_string());
modified crates/radicle/src/storage/refs/arbitrary.rs
@@ -17,7 +17,7 @@ where

    if bool::arbitrary(g) {
        level = Some(FeatureLevel::Parent);
-
        let parent = Oid::from_sha1(Arbitrary::arbitrary(g));
+
        let parent = Oid::arbitrary(g);
        refs.insert(SIGREFS_PARENT.to_ref_string(), parent);
    }

@@ -28,13 +28,12 @@ where
        id: *signer.node_id(),
        level: level.unwrap_or_else(|| FeatureLevel::arbitrary(g)),
        parent: Arbitrary::arbitrary(g),
-
        at: Oid::from_sha1(Arbitrary::arbitrary(g)),
+
        at: Oid::arbitrary(g),
    }
}

impl Arbitrary for Refs {
    fn arbitrary(g: &mut qcheck::Gen) -> Self {
-
        let mut bytes: [u8; 20] = [0; 20];
        let names = &[
            "heads/master",
            "heads/feature/1",
@@ -49,10 +48,7 @@ impl Arbitrary for Refs {
        let mut refs = Self::new();
        for _ in 0..g.size().min(names.len()) {
            if let Some(name) = g.choose(names) {
-
                for byte in &mut bytes {
-
                    *byte = u8::arbitrary(g);
-
                }
-
                let oid = storage::Oid::from_sha1(bytes);
+
                let oid = storage::Oid::arbitrary(g);
                let name = git::fmt::RefString::try_from(*name).unwrap();

                refs.insert(name, oid);
@@ -66,7 +62,7 @@ impl Arbitrary for RefsAt {
    fn arbitrary(g: &mut qcheck::Gen) -> Self {
        Self {
            remote: PublicKey::arbitrary(g),
-
            at: Oid::from_sha1(Arbitrary::arbitrary(g)),
+
            at: Oid::arbitrary(g),
        }
    }
}
modified crates/radicle/src/test/arbitrary.rs
@@ -25,8 +25,7 @@ use crate::test::storage::{MockRepository, MockStorage};
use crate::{cob, git};

pub fn oid() -> storage::Oid {
-
    let oid_bytes: [u8; 20] = r#gen(1);
-
    storage::Oid::from_sha1(oid_bytes)
+
    r#gen(1)
}

pub fn entry_id() -> cob::EntryId {