rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 heartwoodf1b2d2d0015f1e716b0658e006669426e1352a79
{
"request": "trigger",
"version": 1,
"event_type": "patch",
"repository": {
"id": "rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5",
"name": "heartwood",
"description": "Radicle Heartwood Protocol & Stack",
"private": false,
"default_branch": "master",
"delegates": [
"did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT",
"did:key:z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW",
"did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM",
"did:key:z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz",
"did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz"
]
},
"action": "Updated",
"patch": {
"id": "890b1dd59b8417016b6b07a8f41f752022a83684",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": "lorenz"
},
"title": "SHA-256 Support",
"state": {
"status": "draft",
"conflicts": []
},
"before": "018266023a738c51b3a4bf6abef5bb16ee3b1181",
"after": "f1b2d2d0015f1e716b0658e006669426e1352a79",
"commits": [
"f1b2d2d0015f1e716b0658e006669426e1352a79",
"f4c76f97f91a7f1631186e30f0e154826933b232",
"ccc52dbf1cc4b13da2a3bbb88a25de5041d057a1",
"fd6deb719919f2b30eb6de917e358b03b43dbf84",
"c4eef1dc1219e40d6bbf73ca28baf96d379139be",
"66f93dcd9b7c00984432114d4f8f20188b3bce27",
"1bc672ea6c1402f1dc13f5549eb2f107835a28a2",
"076fe48d46ae590594f161f0f60fa0609f8d8aa0"
],
"target": "018266023a738c51b3a4bf6abef5bb16ee3b1181",
"labels": [],
"assignees": [],
"revisions": [
{
"id": "890b1dd59b8417016b6b07a8f41f752022a83684",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": "lorenz"
},
"description": "See <https://radicle.zulipchat.com/#narrow/channel/369277-heartwood/topic/SHA-256/with/596665662>.",
"base": "88bf2a9648750365d4565e32deae35b18808a391",
"oid": "71d8e288907d5af277427829a97cf741dfdac927",
"timestamp": 1782063843
},
{
"id": "35852c562dbc7b27b1a2168ad7513e4fc7ad5983",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": "lorenz"
},
"description": "Remove `Cargo.toml` override.",
"base": "018266023a738c51b3a4bf6abef5bb16ee3b1181",
"oid": "f1b2d2d0015f1e716b0658e006669426e1352a79",
"timestamp": 1782140815
}
]
}
}
{
"response": "triggered",
"run_id": {
"id": "2c11eb8c-0d06-4026-98ef-fd130662f3a8"
},
"info_url": "https://cci.rad.levitte.org//2c11eb8c-0d06-4026-98ef-fd130662f3a8.html"
}
Started at: 2026-06-22 17:07:08.715225+02:00
Commands:
$ rad clone rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 .
✓ Creating checkout in ./...
✓ Remote cloudhead@z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT added
✓ Remote-tracking branch cloudhead@z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT/master created for z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT
✓ Remote cloudhead@z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW added
✓ Remote-tracking branch cloudhead@z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW/master created for z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW
✓ Remote fintohaps@z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM added
✓ Remote-tracking branch fintohaps@z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM/master created for z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM
✓ Remote erikli@z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz added
✓ Remote-tracking branch erikli@z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz/master created for z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz
✓ Remote lorenz@z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz added
✓ Remote-tracking branch lorenz@z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz/master created for z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz
✓ Repository successfully cloned under /opt/radcis/ci.rad.levitte.org/cci/state/2c11eb8c-0d06-4026-98ef-fd130662f3a8/w/
╭────────────────────────────────────╮
│ heartwood │
│ Radicle Heartwood Protocol & Stack │
│ 186 issues · 43 patches │
╰────────────────────────────────────╯
Run `cd ./.` to go to the repository directory.
Exit code: 0
$ rad patch checkout 890b1dd59b8417016b6b07a8f41f752022a83684
✓ Switched to branch patch/890b1dd at revision 35852c5
✓ Branch patch/890b1dd setup to track rad/patches/890b1dd59b8417016b6b07a8f41f752022a83684
Exit code: 0
$ git config advice.detachedHead false
Exit code: 0
$ git checkout f1b2d2d0015f1e716b0658e006669426e1352a79
HEAD is now at f1b2d2d0 node: SHA-256
Exit code: 0
$ rad patch show 890b1dd59b8417016b6b07a8f41f752022a83684 -p
╭────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Title SHA-256 Support │
│ Patch 890b1dd59b8417016b6b07a8f41f752022a83684 │
│ Author lorenz z6MkkPv…WX5sTEz │
│ Head f1b2d2d0015f1e716b0658e006669426e1352a79 │
│ Base 018266023a738c51b3a4bf6abef5bb16ee3b1181 │
│ Branches patch/890b1dd │
│ Commits ahead 8, behind 0 │
│ Status draft │
│ │
│ See │
│ <https://radicle.zulipchat.com/#narrow/channel/369277-heartwood/topic/SHA-256/with/596665662>. │
├────────────────────────────────────────────────────────────────────────────────────────────────┤
│ f1b2d2d node: SHA-256 │
│ f4c76f9 cli: SHA-256 │
│ ccc52db protocol: SHA-256 │
│ fd6deb7 radicle: SHA-256 │
│ c4eef1d cob: SHA-256 │
│ 66f93dc core: SHA-256 │
│ 1bc672e oid: Introduce unstable support for SHA-256 │
│ 076fe48 git2: 0.20 → 0.21 │
├────────────────────────────────────────────────────────────────────────────────────────────────┤
│ ● Revision 890b1dd @ 88bf2a9..71d8e28 by lorenz z6MkkPv…WX5sTEz 21 hours ago │
│ ↑ Revision 35852c5 @ 0182660..f1b2d2d by lorenz z6MkkPv…WX5sTEz 15 seconds ago │
╰────────────────────────────────────────────────────────────────────────────────────────────────╯
commit f1b2d2d0015f1e716b0658e006669426e1352a79
Author: Lorenz Leutgeb <lorenz.leutgeb@radicle.xyz>
Date: Sat Apr 25 19:17:41 2026 +0200
node: SHA-256
diff --git a/crates/radicle-node/src/test/simulator.rs b/crates/radicle-node/src/test/simulator.rs
index aa95e7104..35fce5dca 100644
--- a/crates/radicle-node/src/test/simulator.rs
+++ b/crates/radicle-node/src/test/simulator.rs
@@ -454,6 +454,7 @@ where
/// Process a service output event from a node.
pub fn schedule(&mut self, node: &NodeId, out: Io) {
+ let g = &mut qcheck::Gen::default();
let node = *node;
match out {
@@ -661,7 +662,7 @@ where
canonical: fetch::UpdatedCanonicalRefs::default(),
namespaces: HashSet::new(),
clone: true,
- doc: arbitrary::r#gen(1),
+ doc: arbitrary::doc_at(g, rid.object_format()),
})),
),
},
diff --git a/crates/radicle-node/src/tests.rs b/crates/radicle-node/src/tests.rs
index 2a9142ec2..8e601ddf3 100644
--- a/crates/radicle-node/src/tests.rs
+++ b/crates/radicle-node/src/tests.rs
@@ -1579,7 +1579,7 @@ fn test_queued_fetch_from_ann_same_rid() {
let bob = Peer::new("bob", [198, 18, 0, 8]);
let eve = Peer::new("eve", [198, 18, 0, 9]);
let carol = Peer::new("carol", [198, 18, 0, 10]);
- let oid = arbitrary::oid();
+ let oid = arbitrary::oid(rid.object_format());
let ann = RefsAnnouncement {
rid,
refs: vec![RefsAt {
@@ -1637,7 +1637,7 @@ fn test_queued_fetch_from_ann_same_rid() {
canonical: fetch::UpdatedCanonicalRefs::default(),
namespaces: [carol.id()].into_iter().collect(),
clone: false,
- doc: arbitrary::r#gen(1),
+ doc: arbitrary::doc_at(&mut qcheck::Gen::default(), rid.object_format()),
}),
);
// Now the 1st fetch is done, but the 2nd and 3rd fetches are redundant.
@@ -1686,8 +1686,14 @@ fn test_queued_fetch_from_command_same_rid() {
// Have enough time pass that Alice sends a "ping" to Bob.
alice.elapse(KEEP_ALIVE_DELTA);
+ let g = &mut qcheck::Gen::default();
+
// Finish the 1st fetch.
- alice.fetched(rid1, nid, Ok(arbitrary::r#gen::<fetch::FetchResult>(1)));
+ alice.fetched(
+ rid1,
+ nid,
+ Ok(fetch::FetchResult::arbitrary(g, rid1.object_format())),
+ );
// Now the 1st fetch is done, the 2nd fetch is dequeued.
let (rid, nid) = alice.fetches().next().unwrap();
assert_eq!(rid, rid1);
@@ -1697,7 +1703,11 @@ fn test_queued_fetch_from_command_same_rid() {
assert_matches!(alice.fetches().next(), None);
// Finish the 2nd fetch.
- alice.fetched(rid1, nid, Ok(arbitrary::r#gen::<fetch::FetchResult>(1)));
+ alice.fetched(
+ rid1,
+ nid,
+ Ok(fetch::FetchResult::arbitrary(g, rid1.object_format())),
+ );
// Now the 2nd fetch is done, the 3rd fetch is dequeued.
assert_matches!(alice.fetches().next(), Some((rid, nid)) if rid == rid1 && peers.remove(&nid));
// All fetches were initiated.
commit f4c76f97f91a7f1631186e30f0e154826933b232
Author: Lorenz Leutgeb <lorenz.leutgeb@radicle.xyz>
Date: Sat Apr 25 19:17:41 2026 +0200
cli: SHA-256
diff --git a/crates/radicle-cli/Cargo.toml b/crates/radicle-cli/Cargo.toml
index a7874df1e..3b96a32c7 100644
--- a/crates/radicle-cli/Cargo.toml
+++ b/crates/radicle-cli/Cargo.toml
@@ -17,6 +17,7 @@ path = "src/main.rs"
default = ["backtrace", "i2p","tor"]
i2p = ["radicle/i2p"]
tor = ["radicle/tor"]
+unstable-sha256 = ["radicle/unstable-sha256"]
[dependencies]
anyhow = "1"
diff --git a/crates/radicle-cli/src/commands/inbox.rs b/crates/radicle-cli/src/commands/inbox.rs
index b92935ebb..03da975de 100644
--- a/crates/radicle-cli/src/commands/inbox.rs
+++ b/crates/radicle-cli/src/commands/inbox.rs
@@ -408,8 +408,9 @@ fn show(
storage: &Storage,
profile: &Profile,
) -> anyhow::Result<()> {
- let n = notifs.get(id)?;
- let repo = storage.repository(n.repo)?;
+ let repo = notifs.get_repo(id)?;
+ let repo = storage.repository(repo)?;
+ let n = notifs.get(id, git::Oid::zero(repo.object_format()))?;
match n.kind {
NotificationKind::Cob { typed_id } if typed_id.is_issue() => {
diff --git a/crates/radicle-cli/src/commands/patch/review/builder.rs b/crates/radicle-cli/src/commands/patch/review/builder.rs
index bd4f0e520..bf563ad10 100644
--- a/crates/radicle-cli/src/commands/patch/review/builder.rs
+++ b/crates/radicle-cli/src/commands/patch/review/builder.rs
@@ -479,7 +479,7 @@ impl FileReviewBuilder {
}
drop(writer);
- git::raw::Diff::from_buffer(&buf).map_err(Error::from)
+ git::raw::Diff::from_buffer_ext(&buf, git::raw::ObjectFormat::Sha1).map_err(Error::from)
}
}
diff --git a/crates/radicle-cli/src/git/pretty_diff.rs b/crates/radicle-cli/src/git/pretty_diff.rs
index d7eaae92a..56fd97e42 100644
--- a/crates/radicle-cli/src/git/pretty_diff.rs
+++ b/crates/radicle-cli/src/git/pretty_diff.rs
@@ -29,6 +29,8 @@ pub trait Repo {
fn blob(&self, oid: git::Oid) -> Result<Blob, git::raw::Error>;
/// Lookup a file in the workdir.
fn file(&self, path: &Path) -> Option<Blob>;
+
+ fn object_format(&self) -> radicle::git::ObjectFormat;
}
impl Repo for git::raw::Repository {
@@ -62,6 +64,10 @@ impl Repo for git::raw::Repository {
}
})
}
+
+ fn object_format(&self) -> radicle::git::ObjectFormat {
+ self.object_format().into()
+ }
}
/// Blobs passed down to the hunk renderer.
@@ -607,9 +613,12 @@ mod test {
&[],
)
.unwrap();
- let commit = repo
- .find_commit(Oid::from_str("5078396028e2ec5660aa54a00208f6e11df84aa9").unwrap())
- .unwrap();
+
+ const OID: &str = "5078396028e2ec5660aa54a00208f6e11df84aa9";
+
+ let oid = Oid::from_str_ext(OID, radicle::git::raw::ObjectFormat::Sha1);
+
+ let commit = repo.find_commit(oid.unwrap()).unwrap();
let parent = commit.parents().next().unwrap();
let old_tree = parent.tree().unwrap();
let new_tree = commit.tree().unwrap();
diff --git a/crates/radicle-cli/src/git/unified_diff.rs b/crates/radicle-cli/src/git/unified_diff.rs
index c8b91af23..81aeed0d4 100644
--- a/crates/radicle-cli/src/git/unified_diff.rs
+++ b/crates/radicle-cli/src/git/unified_diff.rs
@@ -179,6 +179,24 @@ pub trait Decode: Sized {
}
}
+/// A trait for decoding types that may contain Git object identifiers
+/// to be parsed.
+pub trait DecodeObject: Sized {
+ /// Decode, and fail if we reach the end of the stream.
+ fn decode(r: &mut impl io::BufRead, format: radicle::git::ObjectFormat) -> Result<Self, Error>;
+
+ /// Decode from a string input.
+ fn parse(s: &str, format: radicle::git::ObjectFormat) -> Result<Self, Error> {
+ Self::from_bytes(s.as_bytes(), format)
+ }
+
+ /// Decode from a string input.
+ fn from_bytes(bytes: &[u8], format: radicle::git::ObjectFormat) -> Result<Self, Error> {
+ let mut r = io::BufReader::new(bytes);
+ Self::decode(&mut r, format)
+ }
+}
+
/// Diff-related types that can be encoded intro the unified diff format.
pub trait Encode: Sized {
/// Encode type into diff writer.
@@ -196,15 +214,16 @@ pub trait Encode: Sized {
}
}
-impl Decode for Diff {
+impl DecodeObject for Diff {
/// Decode from git's unified diff format, consuming the entire input.
- fn decode(r: &mut impl io::BufRead) -> Result<Self, Error> {
+ fn decode(r: &mut impl io::BufRead, format: git::ObjectFormat) -> Result<Self, Error> {
let mut s = String::new();
r.read_to_string(&mut s)?;
- let d = git::raw::Diff::from_buffer(s.as_ref())
- .map_err(|e| Error::syntax(format!("decoding unified diff: {e}")))?;
+ let d = git::raw::Diff::from_buffer_ext(s.as_ref(), format.into());
+
+ let d = d.map_err(|e| Error::syntax(format!("decoding unified diff: {e}")))?;
let d =
Diff::try_from(d).map_err(|e| Error::syntax(format!("decoding unified diff: {e}")))?;
@@ -621,10 +640,10 @@ mod test {
#[test]
fn test_diff_encode_decode_diff() {
- let diff_a = diff::Diff::parse(include_str!(concat!(
- env!("CARGO_MANIFEST_DIR"),
- "/tests/data/diff.diff"
- )))
+ let diff_a = diff::Diff::parse(
+ include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/diff.diff")),
+ git::ObjectFormat::Sha1,
+ )
.unwrap();
assert_eq!(
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/diff.diff")),
commit ccc52dbf1cc4b13da2a3bbb88a25de5041d057a1
Author: Lorenz Leutgeb <lorenz.leutgeb@radicle.xyz>
Date: Thu Apr 23 18:19:42 2026 +0200
protocol: SHA-256
diff --git a/crates/radicle-protocol/Cargo.toml b/crates/radicle-protocol/Cargo.toml
index db7838229..76c834950 100644
--- a/crates/radicle-protocol/Cargo.toml
+++ b/crates/radicle-protocol/Cargo.toml
@@ -12,6 +12,7 @@ rust-version.workspace = true
i2p = ["cypheraddr/i2p", "radicle/i2p"]
test = ["radicle/test", "radicle-crypto/test", "radicle-crypto/cyphernet", "qcheck"]
tor = ["cypheraddr/tor", "radicle/tor"]
+unstable-sha256 = ["radicle/unstable-sha256", "radicle-core/unstable-sha256"]
[dependencies]
bloomy = "1.2"
diff --git a/crates/radicle-protocol/src/service.rs b/crates/radicle-protocol/src/service.rs
index 16d71d913..159a2ffdd 100644
--- a/crates/radicle-protocol/src/service.rs
+++ b/crates/radicle-protocol/src/service.rs
@@ -117,7 +117,7 @@ pub const TARGET_OUTBOUND_PEERS: usize = 8;
/// Maximum external address limit imposed by message size limits.
pub use message::ADDRESS_LIMIT;
/// Maximum inventory limit imposed by message size limits.
-pub use message::INVENTORY_LIMIT;
+pub use message::INVENTORY_LIMIT_SHA1;
/// Maximum number of project git references imposed by message size limits.
pub use message::REF_REMOTE_LIMIT;
diff --git a/crates/radicle-protocol/src/service/gossip.rs b/crates/radicle-protocol/src/service/gossip.rs
index 65e7a8fca..05807a1eb 100644
--- a/crates/radicle-protocol/src/service/gossip.rs
+++ b/crates/radicle-protocol/src/service/gossip.rs
@@ -35,16 +35,64 @@ pub fn inventory(
inventory: impl IntoIterator<Item = RepoId>,
) -> InventoryAnnouncement {
let inventory = inventory.into_iter().collect::<Vec<_>>();
- if inventory.len() > INVENTORY_LIMIT {
- warn!(
- target: "service",
- "inventory announcement limit ({}) exceeded, other nodes will see only some of your projects",
- inventory.len()
- );
- }
+
+ #[cfg(not(feature = "unstable-sha256"))]
+ let inventory = {
+ if inventory.len() > INVENTORY_LIMIT_SHA1 {
+ warn!(
+ target: "service",
+ "inventory announcement limit ({}) exceeded, other nodes will see only some of your projects",
+ inventory.len()
+ );
+ }
+
+ BoundedVec::truncate(inventory)
+ };
+
+ #[cfg(feature = "unstable-sha256")]
+ let inventory = {
+ use crate::service::message::INVENTORY_LIMIT_BYTES;
+
+ // Decreasing counter, which tracks the remaining capacity of the
+ // inventory announcement in bytes.
+ let mut remaining = INVENTORY_LIMIT_BYTES;
+
+ // Increasing counter which tracks the number of repositories that
+ // are included in the inventory announcement.
+ let mut count = 0;
+
+ let mut result = BoundedVec::new();
+ let mut iter = inventory.into_iter();
+
+ while let Some(rid) = iter.next() {
+ let len = 2 + rid.len();
+
+ if len <= remaining {
+ result.push(rid).expect("RID fits");
+ remaining -= len;
+ count += 1;
+ continue;
+ }
+
+ let hint = match iter.size_hint() {
+ (lower, Some(upper)) if lower == upper => lower.to_string(),
+ (lower, Some(upper)) => format!("between {} and {}", lower, upper),
+ (lower, None) if lower != 0 => format!("at least {}", lower),
+ _ => "some".to_string(),
+ };
+
+ warn!(
+ "Inventory size exceeds announcement limit. {count} repositories will be announced, while {hint} will not be."
+ );
+
+ break;
+ }
+
+ result
+ };
InventoryAnnouncement {
- inventory: BoundedVec::truncate(inventory),
timestamp,
+ inventory,
}
}
diff --git a/crates/radicle-protocol/src/service/message.rs b/crates/radicle-protocol/src/service/message.rs
index c78bb2a93..e732dd96c 100644
--- a/crates/radicle-protocol/src/service/message.rs
+++ b/crates/radicle-protocol/src/service/message.rs
@@ -23,8 +23,22 @@ use crate::wire::Encode as _;
pub const ADDRESS_LIMIT: usize = 16;
/// Maximum number of repository remotes that can be included in a [`RefsAnnouncement`] message.
pub const REF_REMOTE_LIMIT: usize = 1024;
-/// Maximum number of inventory which can be announced to other nodes.
-pub const INVENTORY_LIMIT: usize = 2973;
+/// Maximum size of an inventory consisting only of SHA-1 repositories
+/// that can be announced to other nodes.
+pub const INVENTORY_LIMIT_SHA1: usize = 2973;
+
+/// Maximum size of an inventory consisting only of SHA-256 repositories
+/// that can be announced to other nodes.
+///
+/// Calculated as `INVENTORY_LIMIT_SHA1 * (2 + git::Oid::LEN_SHA1) / (2 + git::Oid::LEN_SHA256)`
+#[cfg(feature = "unstable-sha256")]
+pub const INVENTORY_LIMIT_SHA_256: usize = 1923;
+
+/// Maximum size of an inventory in bytes,
+/// that can be announced to other nodes.
+///
+/// Calculated as `INVENTORY_LIMIT_SHA1 * (2 + git::Oid::LEN_SHA1)`
+pub const INVENTORY_LIMIT_BYTES: usize = 65406;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Subscribe {
@@ -235,7 +249,7 @@ impl RefsStatus {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InventoryAnnouncement {
/// Node inventory.
- pub inventory: BoundedVec<RepoId, INVENTORY_LIMIT>,
+ pub inventory: BoundedVec<RepoId, INVENTORY_LIMIT_SHA1>,
/// Time of announcement.
pub timestamp: Timestamp,
}
@@ -652,9 +666,10 @@ impl qcheck::Arbitrary for Message {
.into()
}
4 => {
+ let format = git::ObjectFormat::arbitrary(g);
let message = Info::RefsAlreadySynced {
- rid: RepoId::arbitrary(g),
- at: radicle::test::arbitrary::oid(),
+ rid: RepoId::from(radicle::test::arbitrary::oid(format)),
+ at: radicle::test::arbitrary::oid(format),
};
Self::Info(message)
}
@@ -729,12 +744,58 @@ mod tests {
}
#[test]
- fn test_inventory_limit() {
+ fn inventory_limit_sha1() {
+ let mut inventory = Vec::with_capacity(INVENTORY_LIMIT_SHA1);
+ for _ in 0..INVENTORY_LIMIT_SHA1 {
+ inventory.push(RepoId::from(radicle::test::arbitrary::oid(
+ git::ObjectFormat::Sha1,
+ )));
+ }
+
+ let msg = Message::inventory(
+ InventoryAnnouncement {
+ inventory: inventory.try_into().expect("size within bounds limit"),
+ timestamp: LocalTime::now().into(),
+ },
+ &Device::mock(),
+ );
+ let mut buf: Vec<u8> = Vec::new();
+ msg.encode(&mut buf);
+
+ let decoded = Message::decode_exact(buf.as_slice());
+ assert!(
+ decoded.is_ok(),
+ "INVENTORY_LIMIT is a valid limit for decoding"
+ );
+ assert_eq!(
+ msg,
+ decoded.unwrap(),
+ "encoding and decoding should be safe for message at INVENTORY_LIMIT",
+ );
+ }
+
+ #[test]
+ #[cfg(feature = "unstable-sha256")]
+ fn inventory_limit_sha_256_definition() {
+ assert_eq!(
+ INVENTORY_LIMIT_SHA_256,
+ INVENTORY_LIMIT_SHA1 * (2 + git::Oid::LEN_SHA1) / (2 + git::Oid::LEN_SHA256)
+ );
+ }
+
+ #[test]
+ #[cfg(feature = "unstable-sha256")]
+ fn inventory_limit_sha256() {
+ let mut inventory = Vec::with_capacity(INVENTORY_LIMIT_SHA_256);
+ for _ in 0..INVENTORY_LIMIT_SHA_256 {
+ inventory.push(RepoId::from(radicle::test::arbitrary::oid(
+ git::ObjectFormat::Sha256,
+ )));
+ }
+
let msg = Message::inventory(
InventoryAnnouncement {
- inventory: arbitrary::vec(INVENTORY_LIMIT)
- .try_into()
- .expect("size within bounds limit"),
+ inventory: inventory.try_into().expect("size within bounds limit"),
timestamp: LocalTime::now().into(),
},
&Device::mock(),
diff --git a/crates/radicle-protocol/src/wire.rs b/crates/radicle-protocol/src/wire.rs
index 9cd4935d1..04288274a 100644
--- a/crates/radicle-protocol/src/wire.rs
+++ b/crates/radicle-protocol/src/wire.rs
@@ -43,9 +43,20 @@ pub type Size = u16;
#[derive(thiserror::Error, Debug)]
pub enum Invalid {
- #[error(
- "invalid Git object identifier size: expected {}, got {actual}",
- git::Oid::LEN_SHA1
+ #[cfg_attr(
+ not(feature = "unstable-sha256"),
+ error(
+ "invalid Git object identifier size: expected {}, got {actual}",
+ git::Oid::LEN_SHA1
+ )
+ )]
+ #[cfg_attr(
+ feature = "unstable-sha256",
+ error(
+ "invalid Git object identifier size: expected {} or {}, got {actual}",
+ git::Oid::LEN_SHA1,
+ git::Oid::LEN_SHA256
+ )
)]
Oid { actual: usize },
#[error(transparent)]
@@ -385,11 +396,41 @@ impl Decode for git::Oid {
fn decode(buf: &mut impl Buf) -> Result<Self, Error> {
let len = Size::decode(buf)? as usize;
- if len != git::Oid::LEN_SHA1 {
- return Err(Invalid::Oid { actual: len }.into());
+ #[cfg(not(feature = "unstable-sha256"))]
+ {
+ if len != git::Oid::LEN_SHA1 {
+ return Err(Invalid::Oid { actual: len }.into());
+ }
+
+ let buf: [u8; git::Oid::LEN_SHA1] = Decode::decode(buf)?;
+ let oid = git::Oid::Sha1(
+ buf.try_into()
+ .expect("the buffer is exactly the right size"),
+ );
+
+ Ok(oid)
}
- Ok(git::Oid::Sha1(Decode::decode(buf)?))
+ #[cfg(feature = "unstable-sha256")]
+ {
+ if len == git::Oid::LEN_SHA1 {
+ let buf: [u8; git::Oid::LEN_SHA1] = Decode::decode(buf)?;
+ let oid = git::Oid::Sha1(
+ buf.try_into()
+ .expect("the buffer is exactly the right size"),
+ );
+ Ok(oid)
+ } else if len == git::Oid::LEN_SHA256 {
+ let buf: [u8; git::Oid::LEN_SHA256] = Decode::decode(buf)?;
+ let oid = git::Oid::Sha256(
+ buf.try_into()
+ .expect("the buffer is exactly the right size"),
+ );
+ Ok(oid)
+ } else {
+ Err(Invalid::Oid { actual: len }.into())
+ }
+ }
}
}
diff --git a/crates/radicle-protocol/src/wire/message.rs b/crates/radicle-protocol/src/wire/message.rs
index 72878404e..a85fd7b07 100644
--- a/crates/radicle-protocol/src/wire/message.rs
+++ b/crates/radicle-protocol/src/wire/message.rs
@@ -485,7 +485,15 @@ mod tests {
#[test]
fn test_inv_ann_max_size() {
let signer = Device::mock();
- let inv: [RepoId; INVENTORY_LIMIT] = arbitrary::r#gen(1);
+ let mut inv: [RepoId; INVENTORY_LIMIT_SHA1] = [Oid::ZERO_SHA1.into(); INVENTORY_LIMIT_SHA1];
+ for item in inv.iter_mut() {
+ loop {
+ *item = arbitrary::r#gen(1);
+ if item.object_format() == radicle::git::ObjectFormat::Sha1 {
+ break;
+ }
+ }
+ }
let ann = AnnouncementMessage::Inventory(InventoryAnnouncement {
inventory: BoundedVec::collect_from(&mut inv.into_iter()),
timestamp: arbitrary::r#gen(1),
diff --git a/crates/radicle-protocol/src/worker/fetch.rs b/crates/radicle-protocol/src/worker/fetch.rs
index f91c54bb2..def6a6390 100644
--- a/crates/radicle-protocol/src/worker/fetch.rs
+++ b/crates/radicle-protocol/src/worker/fetch.rs
@@ -62,14 +62,16 @@ impl UpdatedCanonicalRefs {
}
#[cfg(any(test, feature = "test"))]
-impl qcheck::Arbitrary for FetchResult {
- fn arbitrary(g: &mut qcheck::Gen) -> Self {
+impl FetchResult {
+ pub fn arbitrary(g: &mut qcheck::Gen, format: radicle::git::ObjectFormat) -> Self {
+ use qcheck::Arbitrary;
+
FetchResult {
updated: vec![],
canonical: UpdatedCanonicalRefs::default(),
namespaces: HashSet::arbitrary(g),
clone: bool::arbitrary(g),
- doc: DocAt::arbitrary(g),
+ doc: radicle::test::arbitrary::doc_at(g, format),
}
}
}
commit fd6deb719919f2b30eb6de917e358b03b43dbf84
Author: Lorenz Leutgeb <lorenz.leutgeb@radicle.xyz>
Date: Thu Apr 23 17:31:35 2026 +0200
radicle: SHA-256
diff --git a/crates/radicle/Cargo.toml b/crates/radicle/Cargo.toml
index a5afce019..fd4f96cd8 100644
--- a/crates/radicle/Cargo.toml
+++ b/crates/radicle/Cargo.toml
@@ -27,6 +27,12 @@ schemars = [
"radicle-localtime/schemars",
"dep:schemars"
]
+unstable-sha256 = [
+ "git2/unstable-sha256",
+ "radicle-cob/unstable-sha256",
+ "radicle-core/unstable-sha256",
+ "radicle-oid/unstable-sha256"
+]
tor = ["cyphernet/tor"]
[dependencies]
diff --git a/crates/radicle/src/cob/common.rs b/crates/radicle/src/cob/common.rs
index 56356bc06..b0cb62a97 100644
--- a/crates/radicle/src/cob/common.rs
+++ b/crates/radicle/src/cob/common.rs
@@ -374,6 +374,7 @@ impl From<Oid> for Uri {
}
}
+/*
impl TryFrom<&Uri> for crate::git::raw::Oid {
type Error = Uri;
@@ -386,12 +387,18 @@ impl TryFrom<&Uri> for crate::git::raw::Oid {
Err(value.clone())
}
}
+*/
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)
+ if let Some(oid) = value.as_str().strip_prefix("git:") {
+ let oid = oid.parse().map_err(|_| value.clone())?;
+
+ return Ok(oid);
+ }
+ Err(value.clone())
}
}
diff --git a/crates/radicle/src/cob/identity.rs b/crates/radicle/src/cob/identity.rs
index 31d8aab28..b477f3e9a 100644
--- a/crates/radicle/src/cob/identity.rs
+++ b/crates/radicle/src/cob/identity.rs
@@ -5,6 +5,7 @@ use std::{fmt, ops::Deref, str::FromStr};
use crypto::{PublicKey, Signature};
use nonempty::NonEmpty;
use radicle_cob::{Embed, ObjectId, TypeName};
+use radicle_oid::ObjectFormat;
use serde::{Deserialize, Serialize};
use thiserror::Error;
@@ -790,8 +791,9 @@ impl Revision {
pub fn sign<G: crypto::signature::Signer<crypto::Signature>>(
&self,
signer: &G,
+ format: ObjectFormat,
) -> Result<Signature, DocError> {
- self.doc.signature_of(signer)
+ self.doc.signature_of(signer, format)
}
}
@@ -907,7 +909,9 @@ impl<R: WriteRepository> store::Transaction<Identity, R> {
) -> Result<Self, store::Error> {
let mut tx = Transaction::default();
- let (blob, bytes, signature) = doc.sign(signer).map_err(store::Error::Identity)?;
+ let (blob, bytes, signature) = doc
+ .sign(signer, repo.object_format())
+ .map_err(store::Error::Identity)?;
// Store document blob in repository.
let embed =
Embed::<Uri>::store("radicle.json", &bytes, repo.raw()).map_err(store::Error::Git)?;
@@ -1006,7 +1010,7 @@ where
let revision = self.revision(revision).ok_or(Error::NotFound(id))?;
#[allow(deprecated)]
- let signature = revision.sign(self.store.signer())?;
+ let signature = revision.sign(self.store.signer(), self.store.object_format())?;
self.transaction("Accept revision", |tx, _| tx.accept(id, signature))
}
diff --git a/crates/radicle/src/cob/issue.rs b/crates/radicle/src/cob/issue.rs
index d7214e52d..012cdc28d 100644
--- a/crates/radicle/src/cob/issue.rs
+++ b/crates/radicle/src/cob/issue.rs
@@ -1594,7 +1594,7 @@ mod test {
[],
)
.unwrap();
- let missing = arbitrary::oid();
+ let missing = arbitrary::oid(repo.object_format());
issue.comment("Invalid", missing, []).unwrap_err();
assert_eq!(issue.comments().count(), 1);
@@ -1625,7 +1625,7 @@ mod test {
[],
)
.unwrap();
- let missing = arbitrary::oid();
+ let missing = arbitrary::oid(repo.object_format());
// An invalid comment which points to a missing parent.
// Even creating it via a transaction will trigger an error.
@@ -1669,7 +1669,7 @@ mod test {
let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
let eve = Device::mock();
let identity = repo.identity().unwrap().head();
- let missing = arbitrary::oid();
+ let missing = arbitrary::oid(repo.object_format());
let type_name = Issue::type_name().clone();
let mut issues = Cache::no_cache(&*repo, &node.signer).unwrap();
let mut issue = issues
diff --git a/crates/radicle/src/cob/issue/cache.rs b/crates/radicle/src/cob/issue/cache.rs
index 94617ad9f..8766fb1f9 100644
--- a/crates/radicle/src/cob/issue/cache.rs
+++ b/crates/radicle/src/cob/issue/cache.rs
@@ -632,7 +632,7 @@ mod tests {
use crate::cob::store::access::ReadOnly;
use crate::cob::thread::Thread;
use crate::issue::{CloseReason, Issue, IssueCounts, IssueId, State};
- use crate::storage::HasRepoId as _;
+ use crate::storage::{HasRepoId as _, ReadRepository};
use crate::test::arbitrary;
use crate::test::storage::MockRepository;
@@ -676,10 +676,10 @@ mod tests {
let n_open = arbitrary::r#gen::<u8>(0);
let n_closed = arbitrary::r#gen::<u8>(1);
let open_ids = (0..n_open)
- .map(|_| IssueId::from(arbitrary::oid()))
+ .map(|_| IssueId::from(arbitrary::oid(repo.object_format())))
.collect::<BTreeSet<IssueId>>();
let closed_ids = (0..n_closed)
- .map(|_| IssueId::from(arbitrary::oid()))
+ .map(|_| IssueId::from(arbitrary::oid(repo.object_format())))
.collect::<BTreeSet<IssueId>>();
for id in open_ids.iter() {
@@ -715,11 +715,11 @@ mod tests {
let repo = arbitrary::r#gen::<MockRepository>(1);
let mut cache = memory(&repo);
let ids = (0..arbitrary::r#gen::<u8>(1))
- .map(|_| IssueId::from(arbitrary::oid()))
+ .map(|_| IssueId::from(arbitrary::oid(repo.object_format())))
.collect::<BTreeSet<IssueId>>();
let missing = (0..arbitrary::r#gen::<u8>(2))
.filter_map(|_| {
- let id = IssueId::from(arbitrary::oid());
+ let id = IssueId::from(arbitrary::oid(repo.object_format()));
(!ids.contains(&id)).then_some(id)
})
.collect::<BTreeSet<IssueId>>();
@@ -748,9 +748,10 @@ mod tests {
#[test]
fn test_list() {
let repo = arbitrary::r#gen::<MockRepository>(1);
+ let format = repo.object_format();
let mut cache = memory(&repo);
let ids = (0..arbitrary::r#gen::<u8>(1))
- .map(|_| IssueId::from(arbitrary::oid()))
+ .map(|_| IssueId::from(arbitrary::oid(format)))
.collect::<BTreeSet<IssueId>>();
let mut issues = Vec::with_capacity(ids.len());
@@ -778,9 +779,10 @@ mod tests {
#[test]
fn test_list_by_status() {
let repo = arbitrary::r#gen::<MockRepository>(1);
+ let format = repo.object_format();
let mut cache = memory(&repo);
let ids = (0..arbitrary::r#gen::<u8>(1))
- .map(|_| IssueId::from(arbitrary::oid()))
+ .map(|_| IssueId::from(arbitrary::oid(format)))
.collect::<BTreeSet<IssueId>>();
let mut issues = Vec::with_capacity(ids.len());
@@ -810,7 +812,7 @@ mod tests {
let repo = arbitrary::r#gen::<MockRepository>(1);
let mut cache = memory(&repo);
let ids = (0..arbitrary::r#gen::<u8>(1))
- .map(|_| IssueId::from(arbitrary::oid()))
+ .map(|_| IssueId::from(arbitrary::oid(repo.object_format())))
.collect::<BTreeSet<IssueId>>();
for id in ids.iter() {
diff --git a/crates/radicle/src/cob/patch.rs b/crates/radicle/src/cob/patch.rs
index 62f78fcf0..2232d5d40 100644
--- a/crates/radicle/src/cob/patch.rs
+++ b/crates/radicle/src/cob/patch.rs
@@ -2994,6 +2994,8 @@ mod test {
use pretty_assertions::assert_eq;
+ use radicle_oid::ObjectFormat;
+
use super::*;
use crate::cob::common::CodeRange;
use crate::cob::test::Actor;
@@ -3011,14 +3013,14 @@ mod test {
use cob::migrate;
- fn revision() -> (RevisionId, Revision) {
+ fn revision(format: ObjectFormat) -> (RevisionId, Revision) {
let author = arbitrary::r#gen::<Did>(1);
let description = arbitrary::r#gen::<String>(1);
- let base = arbitrary::oid();
- let oid = arbitrary::oid();
+ let base = arbitrary::oid(format);
+ let oid = arbitrary::oid(format);
let timestamp = env::local_time();
let resolves = BTreeSet::new();
- let id = RevisionId::from(arbitrary::oid());
+ let id = RevisionId::from(arbitrary::oid(format));
let mut revision = Revision::new(
id,
Author { id: author },
@@ -3036,7 +3038,7 @@ mod test {
vec![],
timestamp.into(),
);
- let thread = Thread::new(arbitrary::oid(), comment);
+ let thread = Thread::new(arbitrary::oid(format), comment);
revision.discussion = thread;
(id, revision)
}
@@ -3106,10 +3108,12 @@ mod test {
.verified()
.unwrap();
+ let format = ObjectFormat::Sha1;
+
let patch_none = Patch::new(
cob::Title::new("My patch").unwrap(),
MergeTarget::Delegates,
- revision(),
+ revision(format),
);
assert_eq!(
patch_none.merge_target_branch(&doc).unwrap().as_str(),
@@ -3119,7 +3123,7 @@ mod test {
let patch_unqualified = Patch::new(
cob::Title::new("My patch").unwrap(),
MergeTarget::Branch(TargetBranch::try_from("accepted").unwrap()),
- revision(),
+ revision(format),
);
assert_eq!(
patch_unqualified
@@ -3132,7 +3136,7 @@ mod test {
let patch_qualified_branch = Patch::new(
cob::Title::new("My patch").unwrap(),
MergeTarget::Branch(TargetBranch::try_from("refs/heads/accepted").unwrap()),
- revision(),
+ revision(format),
);
assert_eq!(
patch_qualified_branch
@@ -3174,8 +3178,10 @@ mod test {
let doc = raw_doc.verified().unwrap();
+ let format = ObjectFormat::Sha1;
+
let merge_action = Action::Merge {
- revision: RevisionId(arbitrary::entry_id()),
+ revision: RevisionId(arbitrary::entry_id(format)),
commit: oid,
};
@@ -3183,9 +3189,9 @@ mod test {
cob::Title::new("My Patch").unwrap(),
MergeTarget::Branch(TargetBranch::try_from("accepted").unwrap()),
(
- RevisionId(arbitrary::entry_id()),
+ RevisionId(arbitrary::entry_id(format)),
Revision::new(
- RevisionId(arbitrary::entry_id()),
+ RevisionId(arbitrary::entry_id(format)),
Author::new(alice.did()),
String::new(),
base,
@@ -3206,9 +3212,9 @@ mod test {
cob::Title::new("My Patch").unwrap(),
MergeTarget::Branch(TargetBranch::try_from("refs/heads/accepted").unwrap()),
(
- RevisionId(arbitrary::entry_id()),
+ RevisionId(arbitrary::entry_id(format)),
Revision::new(
- RevisionId(arbitrary::entry_id()),
+ RevisionId(arbitrary::entry_id(format)),
Author::new(alice.did()),
String::new(),
base,
@@ -3254,14 +3260,16 @@ mod test {
identity::doc::Payload::from(crefs),
);
+ let format = ObjectFormat::Sha1;
+
let doc = raw_doc.verified().unwrap();
let patch = Patch::new(
cob::Title::new("My Patch").unwrap(),
MergeTarget::Branch(TargetBranch::try_from("accepted").unwrap()),
(
- RevisionId(arbitrary::entry_id()),
+ RevisionId(arbitrary::entry_id(format)),
Revision::new(
- RevisionId(arbitrary::entry_id()),
+ RevisionId(arbitrary::entry_id(format)),
Author::new(bob.did()),
String::new(),
base,
@@ -3273,7 +3281,7 @@ mod test {
);
let merge_action = Action::Merge {
- revision: RevisionId(arbitrary::entry_id()),
+ revision: RevisionId(arbitrary::entry_id(format)),
commit: oid,
};
@@ -3313,14 +3321,16 @@ mod test {
identity::doc::Payload::from(crefs),
);
+ let format = ObjectFormat::Sha1;
+
let doc = raw_doc.verified().unwrap();
let patch = Patch::new(
cob::Title::new("My Patch").unwrap(),
MergeTarget::Branch(TargetBranch::try_from("accepted").unwrap()),
(
- RevisionId(arbitrary::entry_id()),
+ RevisionId(arbitrary::entry_id(format)),
Revision::new(
- RevisionId(arbitrary::entry_id()),
+ RevisionId(arbitrary::entry_id(format)),
Author::new(alice.did()),
String::new(),
base,
@@ -3332,7 +3342,7 @@ mod test {
);
let merge_action = Action::Merge {
- revision: RevisionId(arbitrary::entry_id()),
+ revision: RevisionId(arbitrary::entry_id(format)),
commit: oid,
};
@@ -3503,6 +3513,7 @@ mod test {
#[test]
fn test_patch_review() {
let alice = test::setup::NodeWithRepo::default();
+ let format = alice.repo.object_format();
let checkout = alice.repo.checkout();
let branch = checkout.branch_with([("README", b"Hello World!")]);
let mut patches = Cache::no_cache(&*alice.repo, &alice.signer).unwrap();
@@ -3546,7 +3557,7 @@ mod test {
patch.redact_review(review_id).unwrap();
// If the review never existed, it's an error.
patch
- .redact_review(ReviewId(arbitrary::entry_id()))
+ .redact_review(ReviewId(arbitrary::entry_id(format)))
.unwrap_err();
}
@@ -3596,37 +3607,52 @@ mod test {
.unwrap();
let repo = MockRepository::new(rid, doc);
- let a1 = alice.op::<Patch>([
- Action::Revision {
- description: String::new(),
+ let a1 = alice.op::<Patch>(
+ [
+ Action::Revision {
+ description: String::new(),
+ base,
+ oid,
+ resolves: Default::default(),
+ },
+ Action::Edit {
+ title: cob::Title::new("My patch").unwrap(),
+ target: MergeTarget::Delegates,
+ },
+ ],
+ ObjectFormat::Sha1,
+ );
+ let a2 = alice.op::<Patch>(
+ [Action::Revision {
+ description: String::from("Second revision"),
base,
oid,
resolves: Default::default(),
- },
- Action::Edit {
- title: cob::Title::new("My patch").unwrap(),
- target: MergeTarget::Delegates,
- },
- ]);
- let a2 = alice.op::<Patch>([Action::Revision {
- description: String::from("Second revision"),
- base,
- oid,
- resolves: Default::default(),
- }]);
- let a3 = alice.op::<Patch>([Action::RevisionRedact {
- revision: RevisionId(a2.id()),
- }]);
- let a4 = alice.op::<Patch>([Action::Review {
- revision: RevisionId(a2.id()),
- summary: None,
- verdict: Some(Verdict::Accept),
- labels: vec![],
- }]);
- let a5 = alice.op::<Patch>([Action::Merge {
- revision: RevisionId(a2.id()),
- commit: oid,
- }]);
+ }],
+ ObjectFormat::Sha1,
+ );
+ let a3 = alice.op::<Patch>(
+ [Action::RevisionRedact {
+ revision: RevisionId(a2.id()),
+ }],
+ ObjectFormat::Sha1,
+ );
+ let a4 = alice.op::<Patch>(
+ [Action::Review {
+ revision: RevisionId(a2.id()),
+ summary: None,
+ verdict: Some(Verdict::Accept),
+ labels: vec![],
+ }],
+ ObjectFormat::Sha1,
+ );
+ let a5 = alice.op::<Patch>(
+ [Action::Merge {
+ revision: RevisionId(a2.id()),
+ commit: oid,
+ }],
+ ObjectFormat::Sha1,
+ );
let mut patch = Patch::from_ops([a1, a2], &repo).unwrap();
assert_eq!(patch.revisions().count(), 2);
@@ -3640,9 +3666,10 @@ mod test {
#[test]
fn test_revision_edit_redact() {
- let base = arbitrary::oid();
- let oid = arbitrary::oid();
let repo = r#gen::<MockRepository>(1);
+ let format = repo.object_format();
+ let base = arbitrary::oid(format);
+ let oid = arbitrary::oid(format);
let time = env::local_time();
let alice = MockSigner::default();
let bob = MockSigner::default();
@@ -3661,6 +3688,7 @@ mod test {
],
time.into(),
&alice,
+ format,
);
let r1 = h0.commit(
&Action::Revision {
@@ -3707,24 +3735,30 @@ mod test {
let repo = r#gen::<MockRepository>(1);
let reaction = Reaction::new('👍').expect("failed to create a reaction");
- let a1 = alice.op::<Patch>([
- Action::Revision {
- description: String::new(),
- base,
- oid,
- resolves: Default::default(),
- },
- Action::Edit {
- title: cob::Title::new("My patch").unwrap(),
- target: MergeTarget::Delegates,
- },
- ]);
- let a2 = alice.op::<Patch>([Action::RevisionReact {
- revision: RevisionId(a1.id()),
- location: None,
- reaction,
- active: true,
- }]);
+ let a1 = alice.op::<Patch>(
+ [
+ Action::Revision {
+ description: String::new(),
+ base,
+ oid,
+ resolves: Default::default(),
+ },
+ Action::Edit {
+ title: cob::Title::new("My patch").unwrap(),
+ target: MergeTarget::Delegates,
+ },
+ ],
+ ObjectFormat::Sha1,
+ );
+ let a2 = alice.op::<Patch>(
+ [Action::RevisionReact {
+ revision: RevisionId(a1.id()),
+ location: None,
+ reaction,
+ active: true,
+ }],
+ ObjectFormat::Sha1,
+ );
let patch = Patch::from_ops([a1, a2], &repo).unwrap();
let (_, r1) = patch.revisions().next().unwrap();
@@ -4044,7 +4078,7 @@ mod test {
})
);
- let revision = RevisionId(arbitrary::entry_id());
+ let revision = RevisionId(arbitrary::entry_id(ObjectFormat::Sha1));
assert_eq!(
serde_json::to_value(Action::Review {
revision,
diff --git a/crates/radicle/src/cob/patch/cache.rs b/crates/radicle/src/cob/patch/cache.rs
index 58532e329..f74381130 100644
--- a/crates/radicle/src/cob/patch/cache.rs
+++ b/crates/radicle/src/cob/patch/cache.rs
@@ -717,6 +717,7 @@ mod tests {
use amplify::Wrapper;
use radicle_cob::ObjectId;
+ use radicle_oid::ObjectFormat;
use crate::cob::cache::{Store, Update, Write};
use crate::cob::store::access::ReadOnly;
@@ -727,7 +728,7 @@ mod tests {
};
use crate::prelude::Did;
use crate::profile::env;
- use crate::storage::HasRepoId as _;
+ use crate::storage::{HasRepoId as _, ReadRepository};
use crate::test::arbitrary;
use crate::test::storage::MockRepository;
@@ -742,14 +743,14 @@ mod tests {
Cache { store, cache }
}
- fn revision() -> (RevisionId, Revision) {
+ fn revision(format: ObjectFormat) -> (RevisionId, Revision) {
let author = arbitrary::r#gen::<Did>(1);
let description = arbitrary::r#gen::<String>(1);
- let base = arbitrary::oid();
- let oid = arbitrary::oid();
+ let base = arbitrary::oid(format);
+ let oid = arbitrary::oid(format);
let timestamp = env::local_time();
let resolves = BTreeSet::new();
- let id = RevisionId::from(arbitrary::oid());
+ let id = RevisionId::from(arbitrary::oid(format));
let mut revision = Revision::new(
id,
Author { id: author },
@@ -767,7 +768,7 @@ mod tests {
vec![],
timestamp.into(),
);
- let thread = Thread::new(arbitrary::oid(), comment);
+ let thread = Thread::new(arbitrary::oid(format), comment);
revision.discussion = thread;
(id, revision)
}
@@ -775,13 +776,14 @@ mod tests {
#[test]
fn test_is_empty() {
let repo = arbitrary::r#gen::<MockRepository>(1);
+ let format = repo.object_format();
let mut cache = memory(&repo);
assert!(cache.is_empty().unwrap());
let patch = Patch::new(
Title::new("Patch #1").unwrap(),
MergeTarget::Delegates,
- revision(),
+ revision(format),
);
let id = ObjectId::from_str("47799cbab2eca047b6520b9fce805da42b49ecab").unwrap();
cache.update(&cache.rid(), &id, &patch).unwrap();
@@ -791,7 +793,7 @@ mod tests {
..Patch::new(
Title::new("Patch #2").unwrap(),
MergeTarget::Delegates,
- revision(),
+ revision(format),
)
};
let id = ObjectId::from_str("ae981ded6ed2ed2cdba34c8603714782667f18a3").unwrap();
@@ -803,29 +805,30 @@ mod tests {
#[test]
fn test_counts() {
let repo = arbitrary::r#gen::<MockRepository>(1);
+ let format = repo.object_format();
let mut cache = memory(&repo);
let n_open = arbitrary::r#gen::<u8>(0);
let n_draft = arbitrary::r#gen::<u8>(1);
let n_archived = arbitrary::r#gen::<u8>(1);
let n_merged = arbitrary::r#gen::<u8>(1);
let open_ids = (0..n_open)
- .map(|_| PatchId::from(arbitrary::oid()))
+ .map(|_| PatchId::from(arbitrary::oid(format)))
.collect::<BTreeSet<PatchId>>();
let draft_ids = (0..n_draft)
- .map(|_| PatchId::from(arbitrary::oid()))
+ .map(|_| PatchId::from(arbitrary::oid(format)))
.collect::<BTreeSet<PatchId>>();
let archived_ids = (0..n_archived)
- .map(|_| PatchId::from(arbitrary::oid()))
+ .map(|_| PatchId::from(arbitrary::oid(format)))
.collect::<BTreeSet<PatchId>>();
let merged_ids = (0..n_merged)
- .map(|_| PatchId::from(arbitrary::oid()))
+ .map(|_| PatchId::from(arbitrary::oid(format)))
.collect::<BTreeSet<PatchId>>();
for id in open_ids.iter() {
let patch = Patch::new(
Title::new(&id.to_string()).unwrap(),
MergeTarget::Delegates,
- revision(),
+ revision(format),
);
cache
.update(&cache.rid(), &PatchId::from(*id), &patch)
@@ -838,7 +841,7 @@ mod tests {
..Patch::new(
Title::new(&id.to_string()).unwrap(),
MergeTarget::Delegates,
- revision(),
+ revision(format),
)
};
cache
@@ -852,7 +855,7 @@ mod tests {
..Patch::new(
Title::new(&id.to_string()).unwrap(),
MergeTarget::Delegates,
- revision(),
+ revision(format),
)
};
cache
@@ -863,13 +866,13 @@ mod tests {
for id in merged_ids.iter() {
let patch = Patch {
state: State::Merged {
- revision: arbitrary::oid().into(),
- commit: arbitrary::oid(),
+ revision: arbitrary::oid(format).into(),
+ commit: arbitrary::oid(format),
},
..Patch::new(
Title::new(&id.to_string()).unwrap(),
MergeTarget::Delegates,
- revision(),
+ revision(format),
)
};
cache
@@ -891,13 +894,14 @@ mod tests {
#[test]
fn test_get() {
let repo = arbitrary::r#gen::<MockRepository>(1);
+ let format = repo.object_format();
let mut cache = memory(&repo);
let ids = (0..arbitrary::r#gen::<u8>(1))
- .map(|_| PatchId::from(arbitrary::oid()))
+ .map(|_| PatchId::from(arbitrary::oid(format)))
.collect::<BTreeSet<PatchId>>();
let missing = (0..arbitrary::r#gen::<u8>(2))
.filter_map(|_| {
- let id = PatchId::from(arbitrary::oid());
+ let id = PatchId::from(arbitrary::oid(format));
(!ids.contains(&id)).then_some(id)
})
.collect::<BTreeSet<PatchId>>();
@@ -907,7 +911,7 @@ mod tests {
let patch = Patch::new(
Title::new(&id.to_string()).unwrap(),
MergeTarget::Delegates,
- revision(),
+ revision(format),
);
cache
.update(&cache.rid(), &PatchId::from(*id), &patch)
@@ -927,10 +931,11 @@ mod tests {
#[test]
fn test_find_by_revision() {
let repo = arbitrary::r#gen::<MockRepository>(1);
+ let format = repo.object_format();
let mut cache = memory(&repo);
- let patch_id = PatchId::from(arbitrary::oid());
+ let patch_id = PatchId::from(arbitrary::oid(format));
let revisions = (0..arbitrary::r#gen::<NonZeroU8>(1).into())
- .map(|_| revision())
+ .map(|_| revision(format))
.collect::<BTreeMap<RevisionId, Revision>>();
let (rev_id, rev) = revisions
.iter()
@@ -969,9 +974,10 @@ mod tests {
#[test]
fn test_list() {
let repo = arbitrary::r#gen::<MockRepository>(1);
+ let format = repo.object_format();
let mut cache = memory(&repo);
let ids = (0..arbitrary::r#gen::<u8>(1))
- .map(|_| PatchId::from(arbitrary::oid()))
+ .map(|_| PatchId::from(arbitrary::oid(format)))
.collect::<BTreeSet<PatchId>>();
let mut patches = Vec::with_capacity(ids.len());
@@ -979,7 +985,7 @@ mod tests {
let patch = Patch::new(
Title::new(&id.to_string()).unwrap(),
MergeTarget::Delegates,
- revision(),
+ revision(format),
);
cache
.update(&cache.rid(), &PatchId::from(*id), &patch)
@@ -1000,9 +1006,10 @@ mod tests {
#[test]
fn test_list_by_status() {
let repo = arbitrary::r#gen::<MockRepository>(1);
+ let format = repo.object_format();
let mut cache = memory(&repo);
let ids = (0..arbitrary::r#gen::<u8>(1))
- .map(|_| PatchId::from(arbitrary::oid()))
+ .map(|_| PatchId::from(arbitrary::oid(format)))
.collect::<BTreeSet<PatchId>>();
let mut patches = Vec::with_capacity(ids.len());
@@ -1010,7 +1017,7 @@ mod tests {
let patch = Patch::new(
Title::new(&id.to_string()).unwrap(),
MergeTarget::Delegates,
- revision(),
+ revision(format),
);
cache
.update(&cache.rid(), &PatchId::from(*id), &patch)
@@ -1031,16 +1038,17 @@ mod tests {
#[test]
fn test_remove() {
let repo = arbitrary::r#gen::<MockRepository>(1);
+ let format = repo.object_format();
let mut cache = memory(&repo);
let ids = (0..arbitrary::r#gen::<u8>(1))
- .map(|_| PatchId::from(arbitrary::oid()))
+ .map(|_| PatchId::from(arbitrary::oid(format)))
.collect::<BTreeSet<PatchId>>();
for id in ids.iter() {
let patch = Patch::new(
Title::new(&id.to_string()).unwrap(),
MergeTarget::Delegates,
- revision(),
+ revision(format),
);
cache
.update(&cache.rid(), &PatchId::from(*id), &patch)
diff --git a/crates/radicle/src/cob/store.rs b/crates/radicle/src/cob/store.rs
index 41b5941f2..0443d5dea 100644
--- a/crates/radicle/src/cob/store.rs
+++ b/crates/radicle/src/cob/store.rs
@@ -4,6 +4,7 @@ use std::marker::PhantomData;
use nonempty::NonEmpty;
use radicle_cob::CollaborativeObject;
+use radicle_oid::ObjectFormat;
use serde::{Deserialize, Serialize};
use crate::cob::op::Op;
@@ -200,6 +201,16 @@ where
}
}
+impl<'a, T, Repo, Access> Store<'a, T, Repo, Access>
+where
+ Repo: ReadRepository,
+{
+ /// Return a new store with the attached identity.
+ pub fn object_format(&self) -> ObjectFormat {
+ self.repo.object_format()
+ }
+}
+
impl<'a, T, Repo, Access> Store<'a, T, Repo, Access>
where
Repo: ReadRepository + cob::Store<Namespace = NodeId>,
diff --git a/crates/radicle/src/cob/test.rs b/crates/radicle/src/cob/test.rs
index e976cbb0a..b194797c1 100644
--- a/crates/radicle/src/cob/test.rs
+++ b/crates/radicle/src/cob/test.rs
@@ -3,6 +3,7 @@ use std::ops::Deref;
use nonempty::NonEmpty;
use radicle_crypto::ssh::ExtendedSignature;
+use radicle_oid::ObjectFormat;
use serde::{Deserialize, Serialize};
use crate::cob::op::Op;
@@ -28,6 +29,7 @@ pub struct HistoryBuilder<T> {
resource: Option<Oid>,
time: Timestamp,
witness: PhantomData<T>,
+ format: ObjectFormat,
}
impl<T> AsRef<History> for HistoryBuilder<T> {
@@ -56,12 +58,17 @@ where
T: CobWithType,
T::Action: for<'de> Deserialize<'de> + Serialize + Eq + 'static,
{
- pub fn new<G: Signer>(actions: &[T::Action], time: Timestamp, signer: &G) -> HistoryBuilder<T> {
- let resource = Some(arbitrary::oid());
- let revision = arbitrary::oid();
+ pub fn new<G: Signer>(
+ actions: &[T::Action],
+ time: Timestamp,
+ signer: &G,
+ format: ObjectFormat,
+ ) -> HistoryBuilder<T> {
+ let resource = Some(arbitrary::oid(format));
+ let revision = arbitrary::oid(format);
let (contents, oids): (Vec<Vec<u8>>, Vec<Oid>) = actions
.iter()
- .map(|a| encoded::<T, _>(a, time, [], signer))
+ .map(|a| encoded::<T, _>(a, time, [], signer, format))
.unzip();
let contents = NonEmpty::from_vec(contents).unwrap();
let root = oids.first().unwrap();
@@ -85,6 +92,7 @@ where
time,
resource,
witness: PhantomData,
+ format,
}
}
@@ -99,8 +107,8 @@ where
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();
- let (data, oid) = encoded::<T, _>(action, timestamp, tips, signer);
+ let revision = arbitrary::oid(self.format);
+ let (data, oid) = encoded::<T, _>(action, timestamp, tips, signer, ObjectFormat::Sha1);
let manifest = Manifest::new(T::type_name().clone(), Version::default());
let signature = signer.sign(data.as_slice());
let signature = ExtendedSignature::new(*signer.public_key(), signature);
@@ -134,12 +142,13 @@ pub fn history<T, G: Signer>(
actions: &[T::Action],
time: Timestamp,
signer: &G,
+ format: ObjectFormat,
) -> HistoryBuilder<T>
where
T: Cob + CobWithType,
T::Action: Serialize + Eq + 'static,
{
- HistoryBuilder::new(actions, time, signer)
+ HistoryBuilder::new(actions, time, signer, format)
}
/// An object that can be used to create and sign operations.
@@ -166,6 +175,7 @@ impl<G: Signer> Actor<G> {
actions: impl IntoIterator<Item = T::Action>,
identity: Option<Oid>,
timestamp: Timestamp,
+ format: ObjectFormat,
) -> Op<T::Action>
where
T: Cob + CobWithType,
@@ -177,8 +187,14 @@ impl<G: Signer> Actor<G> {
"nonce": fastrand::u64(..),
}))
.unwrap();
- let oid =
- crate::git::raw::Oid::hash_object(crate::git::raw::ObjectType::Blob, &data).unwrap();
+
+ let oid = crate::git::raw::Oid::hash_object_ext(
+ crate::git::raw::ObjectType::Blob,
+ &data,
+ format.into(),
+ )
+ .unwrap();
+
let id = oid.into();
let author = *self.signer.public_key();
let actions = NonEmpty::from_vec(actions).unwrap();
@@ -199,15 +215,19 @@ impl<G: Signer> Actor<G> {
}
/// Create a new operation.
- pub fn op<T>(&mut self, actions: impl IntoIterator<Item = T::Action>) -> Op<T::Action>
+ pub fn op<T>(
+ &mut self,
+ actions: impl IntoIterator<Item = T::Action>,
+ format: ObjectFormat,
+ ) -> Op<T::Action>
where
T: Cob + CobWithType,
T::Action: Clone + Serialize,
{
- let identity = arbitrary::oid();
+ let identity = arbitrary::oid(format);
let timestamp = env::commit_time();
- self.op_with::<T>(actions, Some(identity), timestamp.into())
+ self.op_with::<T>(actions, Some(identity), timestamp.into(), format)
}
/// Get the actor's DID.
@@ -227,18 +247,21 @@ impl<G: Signer> Actor<G> {
repo: &R,
) -> Result<Patch, patch::Error> {
Patch::from_root(
- self.op::<Patch>([
- patch::Action::Revision {
- description: description.to_string(),
- base,
- oid,
- resolves: Default::default(),
- },
- patch::Action::Edit {
- title,
- target: patch::MergeTarget::default(),
- },
- ]),
+ self.op::<Patch>(
+ [
+ patch::Action::Revision {
+ description: description.to_string(),
+ base,
+ oid,
+ resolves: Default::default(),
+ },
+ patch::Action::Edit {
+ title,
+ target: patch::MergeTarget::default(),
+ },
+ ],
+ ObjectFormat::Sha1,
+ ),
repo,
)
}
@@ -253,6 +276,7 @@ fn encoded<T: Cob, G: Signer>(
timestamp: Timestamp,
parents: impl IntoIterator<Item = Oid>,
signer: &G,
+ format: ObjectFormat,
) -> (Vec<u8>, crate::git::Oid) {
use radicle_git_metadata::{
author::{Author, Time},
@@ -260,7 +284,14 @@ fn encoded<T: Cob, G: Signer>(
};
let data = encoding::encode(action).unwrap();
- let oid = crate::git::raw::Oid::hash_object(crate::git::raw::ObjectType::Blob, &data).unwrap();
+
+ let oid = crate::git::raw::Oid::hash_object_ext(
+ crate::git::raw::ObjectType::Blob,
+ &data,
+ format.into(),
+ )
+ .unwrap();
+
let parents = parents.into_iter().map(|o| o.into());
let author = Author {
name: "radicle".to_owned(),
@@ -278,9 +309,12 @@ fn encoded<T: Cob, G: Signer>(
)
.to_string();
- let hash =
- crate::git::raw::Oid::hash_object(crate::git::raw::ObjectType::Commit, commit.as_bytes())
- .unwrap();
+ let hash = crate::git::raw::Oid::hash_object_ext(
+ crate::git::raw::ObjectType::Commit,
+ commit.as_bytes(),
+ format.into(),
+ )
+ .unwrap();
(data, hash.into())
}
diff --git a/crates/radicle/src/cob/thread.rs b/crates/radicle/src/cob/thread.rs
index 6b96d808c..b5ea61835 100644
--- a/crates/radicle/src/cob/thread.rs
+++ b/crates/radicle/src/cob/thread.rs
@@ -628,6 +628,7 @@ mod tests {
use pretty_assertions::assert_eq;
use qcheck_macros::quickcheck;
+ use radicle_oid::ObjectFormat;
use super::*;
use crate as radicle;
@@ -663,23 +664,29 @@ mod tests {
/// Create a new comment.
pub fn comment(&mut self, body: &str, reply_to: Option<CommentId>) -> Op<Action> {
- self.op::<Thread>([Action::Comment {
- body: String::from(body),
- reply_to,
- }])
+ self.op::<Thread>(
+ [Action::Comment {
+ body: String::from(body),
+ reply_to,
+ }],
+ ObjectFormat::Sha1,
+ )
}
/// Create a new redaction.
pub fn redact(&mut self, id: CommentId) -> Op<Action> {
- self.op::<Thread>([Action::Redact { id }])
+ self.op::<Thread>([Action::Redact { id }], ObjectFormat::Sha1)
}
/// Edit a comment.
pub fn edit(&mut self, id: CommentId, body: &str) -> Op<Action> {
- self.op::<Thread>([Action::Edit {
- id,
- body: body.to_owned(),
- }])
+ self.op::<Thread>(
+ [Action::Edit {
+ id,
+ body: body.to_owned(),
+ }],
+ ObjectFormat::Sha1,
+ )
}
}
@@ -748,6 +755,7 @@ mod tests {
let bob = MockSigner::default();
let eve = MockSigner::default();
let repo = r#gen::<MockRepository>(1);
+ let format = repo.object_format();
let time = env::local_time();
let mut a = test::history::<Thread, _>(
@@ -757,6 +765,7 @@ mod tests {
}],
time.into(),
&alice,
+ format,
);
a.comment("Alice comment", Some(*a.root().id()), &alice);
@@ -813,6 +822,7 @@ mod tests {
#[test]
fn test_duplicate_comments() {
let repo = r#gen::<MockRepository>(1);
+ let format = repo.object_format();
let alice = MockSigner::default();
let bob = MockSigner::default();
let time = env::local_time();
@@ -824,6 +834,7 @@ mod tests {
}],
time.into(),
&alice,
+ format,
);
let mut b = a.clone();
@@ -857,6 +868,7 @@ mod tests {
#[quickcheck]
fn prop_ordering(timestamp: u64) {
let repo = r#gen::<MockRepository>(1);
+ let format = repo.object_format();
let alice = MockSigner::default();
let bob = MockSigner::default();
let timestamp = Timestamp::from_secs(timestamp);
@@ -868,6 +880,7 @@ mod tests {
}],
timestamp,
&alice,
+ format,
);
let mut h1 = h0.clone();
let mut h2 = h0.clone();
@@ -916,9 +929,10 @@ mod tests {
#[test]
fn test_comment_redact_missing() {
let repo = r#gen::<MockRepository>(1);
+ let format = repo.object_format();
let mut alice = Actor::<MockSigner>::default();
let mut t = Thread::default();
- let id = arbitrary::entry_id();
+ let id = arbitrary::entry_id(format);
t.op(alice.redact(id), [], &repo).unwrap_err();
}
@@ -926,9 +940,10 @@ mod tests {
#[test]
fn test_comment_edit_missing() {
let repo = r#gen::<MockRepository>(1);
+ let format = repo.object_format();
let mut alice = Actor::<MockSigner>::default();
let mut t = Thread::default();
- let id = arbitrary::entry_id();
+ let id = arbitrary::entry_id(format);
t.op(alice.edit(id, "Edited"), [], &repo).unwrap_err();
}
diff --git a/crates/radicle/src/git.rs b/crates/radicle/src/git.rs
index e593d9e13..2cbf576b9 100644
--- a/crates/radicle/src/git.rs
+++ b/crates/radicle/src/git.rs
@@ -6,7 +6,7 @@ use std::path::Path;
use std::process::Command;
use std::str::FromStr;
-pub use radicle_oid::{Oid, str::ParseOidError};
+pub use radicle_oid::{ObjectFormat, Oid, str::ParseOidError};
pub extern crate radicle_git_ref_format as fmt;
diff --git a/crates/radicle/src/git/canonical/rules.rs b/crates/radicle/src/git/canonical/rules.rs
index 1df5e4e03..4c090d7db 100644
--- a/crates/radicle/src/git/canonical/rules.rs
+++ b/crates/radicle/src/git/canonical/rules.rs
@@ -636,3 +636,509 @@ pub enum CanonicalError {
#[error(transparent)]
Git(#[from] crate::git::raw::Error),
}
+
+#[cfg(test)]
+#[allow(clippy::unwrap_used)]
+mod tests {
+ use std::collections::BTreeMap;
+
+ use nonempty::nonempty;
+
+ use crate::Storage;
+ use crate::crypto::{Signer, test::signer::MockSigner};
+ use crate::git;
+ use crate::git::fmt::RefString;
+ use crate::git::fmt::qualified_pattern;
+ use crate::identity::Visibility;
+ use crate::identity::doc::Doc;
+ use crate::node::device::Device;
+ use crate::rad;
+ use crate::storage::refs::{IDENTITY_BRANCH, IDENTITY_ROOT, SIGREFS_BRANCH, SIGREFS_PARENT};
+ use crate::storage::{ReadStorage, git::transport};
+ use crate::test::{arbitrary, fixtures};
+
+ use super::*;
+
+ fn roundtrip(rule: &Rule<Allowed, usize>) {
+ let json = serde_json::to_string(rule).unwrap();
+ assert_eq!(
+ *rule,
+ serde_json::from_str(&json).unwrap(),
+ "failed to roundtrip: {json}"
+ )
+ }
+
+ fn did(s: &str) -> Did {
+ s.parse().unwrap()
+ }
+
+ fn raw_pattern(qp: QualifiedPattern<'static>) -> RawPattern {
+ qp
+ }
+
+ fn pattern(qp: QualifiedPattern<'static>) -> Pattern {
+ Pattern::new(qp).unwrap()
+ }
+
+ fn resolve_from_doc(doc: &Doc) -> doc::Delegates {
+ doc.delegates().clone()
+ }
+
+ 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.into(), None).unwrap();
+ let tagger = repo.signature().unwrap();
+ repo.tag(name.as_str(), &target, &tagger, name.as_str(), false)
+ .unwrap()
+ .into()
+ }
+
+ #[test]
+ fn test_roundtrip() {
+ let rule1 = Rule::new(Allowed::Delegates, 1);
+ let rule2 = Rule::new(Allowed::Delegates, 1);
+ let rule3 = Rule::new(Allowed::Delegates, 1);
+ let mut rule4 = Rule::new(
+ Allowed::Set(nonempty![
+ did("did:key:z6MkpQTLwr8QyADGmBGAMsGttvWzP4PojUMs4hREZW5T5E3K"),
+ did("did:key:z6MknG1nYDftMYUQ7eTBSGgqB2PL1xK5Pif33J3sRym3e8ye"),
+ ]),
+ 2,
+ );
+ rule4.add_extensions(
+ serde_json::json!({
+ "foo": "bar",
+ "quux": 5,
+ })
+ .as_object()
+ .cloned()
+ .unwrap(),
+ );
+ roundtrip(&rule1);
+ roundtrip(&rule2);
+ roundtrip(&rule3);
+ roundtrip(&rule4);
+ }
+
+ #[test]
+ fn test_deserialization() {
+ let examples = r#"
+ {
+ "refs/heads/main": {
+ "threshold": 2,
+ "allow": [
+ "did:key:z6MkpQTLwr8QyADGmBGAMsGttvWzP4PojUMs4hREZW5T5E3K",
+ "did:key:z6MknG1nYDftMYUQ7eTBSGgqB2PL1xK5Pif33J3sRym3e8ye"
+ ]
+ },
+ "refs/tags/releases/*": {
+ "threshold": 2,
+ "allow": [
+ "did:key:z6MknLWe8A7UJxvTfY36JcB8XrP1KTLb5HFTX38hEmdY3b56",
+ "did:key:z6Mkq2E5Se5H9gk1DsL1EMwR2t4CqSg3GFkNN2UeG4FNqXoP",
+ "did:key:z6MkqRmXW5fbP9hJ1Y8j2N4CgVdJ2XJ6TsyXYf3FQ2NJgXax"
+ ]
+ },
+ "refs/heads/development": {
+ "threshold": 1,
+ "allow": [
+ "did:key:z6MkhH7ENYE62JAjTiRZPU71MGZ6xCwnbyHHWfrBu3fr6PVG"
+ ]
+ },
+ "refs/heads/release/*": {
+ "threshold": 1,
+ "allow": "delegates"
+ }
+ }
+ "#;
+ let expected = [
+ (
+ raw_pattern(qualified_pattern!("refs/heads/main")),
+ Rule::new(
+ Allowed::Set(nonempty![
+ did("did:key:z6MkpQTLwr8QyADGmBGAMsGttvWzP4PojUMs4hREZW5T5E3K"),
+ did("did:key:z6MknG1nYDftMYUQ7eTBSGgqB2PL1xK5Pif33J3sRym3e8ye"),
+ ]),
+ 2,
+ ),
+ ),
+ (
+ raw_pattern(qualified_pattern!("refs/tags/releases/*")),
+ Rule::new(
+ Allowed::Set(nonempty![
+ did("did:key:z6MknLWe8A7UJxvTfY36JcB8XrP1KTLb5HFTX38hEmdY3b56"),
+ did("did:key:z6Mkq2E5Se5H9gk1DsL1EMwR2t4CqSg3GFkNN2UeG4FNqXoP"),
+ did("did:key:z6MkqRmXW5fbP9hJ1Y8j2N4CgVdJ2XJ6TsyXYf3FQ2NJgXax")
+ ]),
+ 2,
+ ),
+ ),
+ (
+ raw_pattern(qualified_pattern!("refs/heads/development")),
+ Rule::new(
+ Allowed::Set(nonempty![did(
+ "did:key:z6MkhH7ENYE62JAjTiRZPU71MGZ6xCwnbyHHWfrBu3fr6PVG"
+ )]),
+ 1,
+ ),
+ ),
+ (
+ raw_pattern(qualified_pattern!("refs/heads/release/*")),
+ Rule::new(Allowed::Delegates, 1),
+ ),
+ ]
+ .into_iter()
+ .collect::<RawRules>();
+ let rules = serde_json::from_str::<BTreeMap<RawPattern, RawRule>>(examples)
+ .unwrap()
+ .into();
+ assert_eq!(expected, rules)
+ }
+
+ #[test]
+ fn test_order() {
+ assert!(
+ pattern(qualified_pattern!("refs/heads/a/b/c/d/*"))
+ < pattern(qualified_pattern!("refs/heads/*/x")),
+ "example 1"
+ );
+ assert!(
+ pattern(qualified_pattern!("refs/heads/a"))
+ < pattern(qualified_pattern!("refs/heads/*")),
+ "example 2.a"
+ );
+ assert!(
+ pattern(qualified_pattern!("refs/heads/abc"))
+ < pattern(qualified_pattern!("refs/heads/a*")),
+ "example 2.a"
+ );
+ assert!(
+ pattern(qualified_pattern!("refs/heads/a/b/*"))
+ < pattern(qualified_pattern!("refs/heads/a/*/c")),
+ "example 2.a"
+ );
+ assert!(
+ pattern(qualified_pattern!("refs/heads/aa*"))
+ < pattern(qualified_pattern!("refs/heads/a*")),
+ "example 2.b.A"
+ );
+ assert!(
+ pattern(qualified_pattern!("refs/heads/a*b"))
+ < pattern(qualified_pattern!("refs/heads/a*")),
+ "example 2.b.B"
+ );
+
+ let pattern01 = pattern(qualified_pattern!("refs/tags/*"));
+ let pattern02 = pattern(qualified_pattern!("refs/tags/v1"));
+ let pattern04 = pattern(qualified_pattern!("refs/tags/v1.0.0"));
+ let pattern05 = pattern(qualified_pattern!("refs/tags/release/v1.0.0"));
+ let pattern03 = pattern(qualified_pattern!("refs/heads/main"));
+ let pattern06 = pattern(qualified_pattern!("refs/tags/*/v1.0.0"));
+
+ let pattern07 = pattern(qualified_pattern!("refs/tags/x*"));
+ let pattern08 = pattern(qualified_pattern!("refs/tags/xx*"));
+
+ let pattern09 = pattern(qualified_pattern!("refs/foos/*"));
+
+ let pattern10 = pattern(qualified_pattern!("refs/heads/a"));
+ let pattern11 = pattern(qualified_pattern!("refs/heads/b"));
+
+ let pattern12 = pattern(qualified_pattern!("refs/heads/a/*"));
+ let pattern13 = pattern(qualified_pattern!("refs/heads/b/*"));
+
+ let pattern14 = pattern(qualified_pattern!("refs/heads/a/*/ab"));
+ let pattern15 = pattern(qualified_pattern!("refs/heads/a/*/a"));
+
+ let pattern16 = pattern(qualified_pattern!("refs/heads/a/*/b"));
+ let pattern17 = pattern(qualified_pattern!("refs/heads/a/*/a"));
+
+ // Test priority for path specificity
+ assert!(
+ pattern06 < pattern02,
+ "match for 06 is always more specific since it has more components"
+ );
+ assert!(pattern02 < pattern01, "match for 02 is also match for 01");
+ assert!(pattern08 < pattern07, "match for 08 is also match for 07");
+ // Test equality
+ assert!(pattern02 == pattern02);
+ // Test lexicographical fallback when paths are equally specific
+ assert!(pattern02 < pattern04);
+ assert!(pattern03 < pattern01);
+ assert!(pattern09 < pattern01);
+ assert!(pattern10 < pattern11);
+ assert!(pattern12 < pattern13);
+ assert!(pattern15 < pattern14);
+ assert!(
+ pattern17 < pattern16,
+ "matches have same length, but lexicographically, 'a' < 'b'"
+ );
+
+ // Test example from docs
+ let pattern18 = pattern(qualified_pattern!("refs/tags/release/candidates/*"));
+ let pattern19 = pattern(qualified_pattern!("refs/tags/release/*"));
+ let pattern20 = pattern(qualified_pattern!("refs/tags/*"));
+
+ assert!(pattern18 < pattern19);
+ assert!(pattern19 < pattern20);
+
+ let pattern21 = pattern(qualified_pattern!("refs/heads/dev"));
+
+ assert!(pattern21 < pattern03);
+
+ let mut patterns = [
+ pattern01.clone(),
+ pattern02.clone(),
+ pattern03.clone(),
+ pattern04.clone(),
+ pattern05.clone(),
+ pattern06.clone(),
+ ];
+ patterns.sort();
+
+ assert_eq!(
+ patterns,
+ [
+ pattern05, pattern06, pattern03, pattern02, pattern04, pattern01
+ ]
+ );
+ }
+
+ #[test]
+ fn test_deserialize_extensions() {
+ let example = r#"
+ {
+ "threshold": 2,
+ "allow": [
+ "did:key:z6MkpQTLwr8QyADGmBGAMsGttvWzP4PojUMs4hREZW5T5E3K",
+ "did:key:z6MknG1nYDftMYUQ7eTBSGgqB2PL1xK5Pif33J3sRym3e8ye"
+ ],
+ "foo": "bar",
+ "quux": 5
+ }
+ "#;
+ let rule = serde_json::from_str::<Rule<Allowed, usize>>(example).unwrap();
+ assert!(!rule.extensions().is_empty());
+ let extensions = rule.extensions();
+ assert_eq!(
+ extensions.get("foo"),
+ Some(serde_json::Value::String("bar".to_string())).as_ref()
+ );
+ assert_eq!(
+ extensions.get("quux"),
+ Some(serde_json::Value::Number(5.into())).as_ref()
+ );
+ }
+
+ #[test]
+ fn test_rule_validate_success() {
+ let doc = arbitrary::r#gen::<Doc>(1);
+ let delegates = Allowed::Set(doc.delegates().as_ref().clone());
+ let threshold = doc.majority();
+
+ let rule = Rule::new(delegates, threshold);
+ let result = rule.validate(&mut || resolve_from_doc(&doc));
+ assert!(result.is_ok(), "failed to validate doc: {result:?}");
+
+ let rule = Rule::new(Allowed::Delegates, 1);
+ let result = rule.validate(&mut || resolve_from_doc(&doc));
+ assert!(result.is_ok(), "failed to validate doc: {result:?}");
+ }
+
+ #[test]
+ fn test_rule_validate_failures() {
+ let doc = arbitrary::r#gen::<Doc>(1);
+ let pattern = pattern(qualified_pattern!("refs/heads/main"));
+
+ assert!(matches!(
+ Rule::new(Allowed::Delegates, 256).validate(&mut || resolve_from_doc(&doc)),
+ Err(ValidationError::Threshold(_))
+ ));
+
+ let threshold = doc.delegates().len().saturating_add(1);
+ assert!(matches!(
+ Rule::new(Allowed::Delegates, threshold).validate(&mut || resolve_from_doc(&doc)),
+ Err(ValidationError::Threshold(_))
+ ));
+
+ let delegates = NonEmpty::from_vec(arbitrary::vec::<Did>(256)).unwrap();
+ assert!(matches!(
+ Rule::new(delegates.into(), 1).validate(&mut || resolve_from_doc(&doc)),
+ Err(ValidationError::Delegates(_))
+ ));
+
+ let delegates = nonempty![
+ did("did:key:z6MknLWe8A7UJxvTfY36JcB8XrP1KTLb5HFTX38hEmdY3b56"),
+ did("did:key:z6MknLWe8A7UJxvTfY36JcB8XrP1KTLb5HFTX38hEmdY3b56")
+ ];
+ let expected = Rule {
+ allow: ResolvedDelegates::Set(
+ doc::Delegates::new(nonempty![did(
+ "did:key:z6MknLWe8A7UJxvTfY36JcB8XrP1KTLb5HFTX38hEmdY3b56"
+ )])
+ .unwrap(),
+ ),
+ threshold: doc::Threshold::MIN,
+ extensions: json::Map::new(),
+ };
+ assert_eq!(
+ Rule::new(delegates.into(), 1)
+ .validate(&mut || resolve_from_doc(&doc))
+ .unwrap(),
+ expected,
+ );
+
+ // Duplicate rules are overwritten
+ let rules = vec![
+ (
+ pattern.clone().into_inner(),
+ Rule::new(Allowed::Delegates, 1),
+ ),
+ (
+ pattern.clone().into_inner(),
+ Rule::new(doc.delegates().as_ref().clone().into(), 1),
+ ),
+ ];
+ let expected = [(
+ pattern,
+ Rule::new(
+ ResolvedDelegates::Set(doc.delegates().clone()),
+ doc::Threshold::MIN,
+ ),
+ )]
+ .into_iter()
+ .collect::<Rules>();
+ assert_eq!(
+ Rules::from_raw(rules, &mut || resolve_from_doc(&doc)).unwrap(),
+ expected
+ );
+ }
+
+ #[test]
+ fn test_canonical() {
+ let tempdir = tempfile::tempdir().unwrap();
+ let storage = Storage::open(tempdir.path().join("storage"), fixtures::user()).unwrap();
+
+ transport::local::register(storage.clone());
+
+ let delegate = Device::mock_from_seed([0xff; 32]);
+ let contributor = MockSigner::from_seed([0xfe; 32]);
+ let (repo, head) = fixtures::repository(tempdir.path().join("working"));
+ let (rid, doc, _) = rad::init(
+ &repo,
+ "heartwood".try_into().unwrap(),
+ "Radicle Heartwood Protocol & Stack",
+ git::fmt::refname!("master"),
+ Visibility::default(),
+ &delegate,
+ &storage,
+ )
+ .unwrap();
+
+ let mut doc = doc.edit();
+ // Ensure there is a second delegate for testing overlapping rules
+ doc.delegate(contributor.public_key().into());
+
+ // Create tags and keep track of their OIDs
+ //
+ // follows the `refs/tags/release/candidates/*` rule
+ let failing_tag = git::fmt::refname!("release/candidates/v1.0");
+ let tags = [
+ // follows the `refs/tags/*` rule
+ git::fmt::refname!("v1.0"),
+ // follows the `refs/tags/release/*` rule
+ git::fmt::refname!("release/v1.0"),
+ failing_tag.clone(),
+ // follows the `refs/tags/*` rule
+ git::fmt::refname!("qa/v1.0"),
+ ]
+ .into_iter()
+ .map(|name| {
+ (
+ git::fmt::lit::refs_tags(name.clone()).into(),
+ tag(name, head, &repo),
+ )
+ })
+ .collect::<BTreeMap<Qualified, _>>();
+
+ git::push(
+ &repo,
+ &rad::REMOTE_NAME,
+ [
+ (
+ &git::fmt::qualified!("refs/tags/v1.0"),
+ &git::fmt::qualified!("refs/tags/v1.0"),
+ ),
+ (
+ &git::fmt::qualified!("refs/tags/release/v1.0"),
+ &git::fmt::qualified!("refs/tags/release/v1.0"),
+ ),
+ (
+ &git::fmt::qualified!("refs/tags/release/candidates/v1.0"),
+ &git::fmt::qualified!("refs/tags/release/candidates/v1.0"),
+ ),
+ (
+ &git::fmt::qualified!("refs/tags/qa/v1.0"),
+ &git::fmt::qualified!("refs/tags/qa/v1.0"),
+ ),
+ ],
+ )
+ .unwrap();
+
+ let rules = Rules::from_raw(
+ [
+ (
+ raw_pattern(qualified_pattern!("refs/tags/*")),
+ Rule::new(Allowed::Delegates, 1),
+ ),
+ (
+ raw_pattern(qualified_pattern!("refs/tags/release/*")),
+ Rule::new(Allowed::Delegates, 1),
+ ),
+ // Ensure that none of the other rules apply by ensuring we need
+ // both delegates to get the quorum of the
+ // `refs/tags/release/candidates/v1.0` reference
+ (
+ raw_pattern(qualified_pattern!("refs/tags/release/candidates/*")),
+ Rule::new(Allowed::Delegates, 2),
+ ),
+ ],
+ &mut || resolve_from_doc(&doc.clone().verified().unwrap()),
+ )
+ .unwrap();
+
+ // All tags should succeed at getting their canonical commit other than the
+ // candidates tag.
+ let stored = storage.repository(rid).unwrap();
+ 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)
+ .unwrap_or_else(|| {
+ panic!("there should be a matching rule for {refname}, rules: {rules:#?}")
+ });
+ if refname == failing {
+ assert!(canonical.find_objects().unwrap().quorum().is_err());
+ } else {
+ assert_eq!(
+ canonical
+ .find_objects()
+ .unwrap()
+ .quorum()
+ .unwrap_or_else(|e| panic!("quorum error for {refname}: {e}")),
+ canonical::Quorum {
+ refname,
+ object: canonical::Object::Tag { id: oid },
+ }
+ )
+ }
+ }
+ }
+
+ #[test]
+ fn test_special_branches() {
+ assert!(Pattern::new((*IDENTITY_BRANCH).clone().into()).is_err());
+ assert!(Pattern::new((*SIGREFS_BRANCH).clone().into()).is_err());
+ assert!(Pattern::new((*SIGREFS_PARENT).clone().into()).is_err());
+ assert!(Pattern::new((*IDENTITY_ROOT).clone().into()).is_err());
+ }
+}
diff --git a/crates/radicle/src/git/raw.rs b/crates/radicle/src/git/raw.rs
index 68f589ed0..ede52b79f 100644
--- a/crates/radicle/src/git/raw.rs
+++ b/crates/radicle/src/git/raw.rs
@@ -18,9 +18,9 @@ pub(crate) use git2::RemoteCallbacks;
// Re-exports that are used by other crates in the workspace, including this crate.
pub use git2::{
- Branch, BranchType, Commit, Direction, Error, ErrorClass, ErrorCode, FileMode, ObjectType, Oid,
- Reference, Remote, Repository, RepositoryInitOptions, RepositoryOpenFlags, Signature, Time,
- Tree,
+ Branch, BranchType, Commit, Direction, Error, ErrorClass, ErrorCode, FileMode, ObjectFormat,
+ ObjectType, Oid, Reference, Remote, Repository, RepositoryInitOptions, RepositoryOpenFlags,
+ Signature, Time, Tree,
};
// Re-exports that are used by other crates in the workspace, but *not* this crate.
diff --git a/crates/radicle/src/identity/doc.rs b/crates/radicle/src/identity/doc.rs
index ad9e39ae2..bf9d69a7d 100644
--- a/crates/radicle/src/identity/doc.rs
+++ b/crates/radicle/src/identity/doc.rs
@@ -11,6 +11,7 @@ use std::sync::LazyLock;
use crate::git::Oid;
use nonempty::NonEmpty;
use radicle_cob::type_name::{TypeName, TypeNameParse};
+use radicle_oid::ObjectFormat;
use serde::{Deserialize, Serialize, de};
use thiserror::Error;
@@ -983,35 +984,40 @@ impl Doc {
/// Encode the [`Doc`] as canonical JSON, returning the set of bytes and its
/// corresponding Git [`Oid`].
- pub fn encode(&self) -> Result<(git::Oid, Vec<u8>), DocError> {
+ pub fn encode(&self, format: ObjectFormat) -> Result<(git::Oid, Vec<u8>), DocError> {
let mut buf = Vec::new();
let mut serializer =
serde_json::Serializer::with_formatter(&mut buf, CanonicalFormatter::new());
self.serialize(&mut serializer)?;
- let oid = git::raw::Oid::hash_object(git::raw::ObjectType::Blob, &buf)?;
+
+ let oid = git::raw::Oid::hash_object_ext(git::raw::ObjectType::Blob, &buf, format.into())?;
Ok((oid.into(), buf))
}
/// [`Doc::encode`] and sign the [`Doc`], returning the set of bytes, its
/// corresponding Git [`Oid`] and the [`Signature`] over the [`Oid`].
- pub fn sign<G>(&self, signer: &G) -> Result<(git::Oid, Vec<u8>, Signature), DocError>
+ pub fn sign<G>(
+ &self,
+ signer: &G,
+ format: ObjectFormat,
+ ) -> Result<(git::Oid, Vec<u8>, Signature), DocError>
where
G: crypto::signature::Signer<crypto::Signature>,
{
- let (oid, bytes) = self.encode()?;
+ let (oid, bytes) = self.encode(format)?;
let sig = signer.sign(oid.as_ref());
Ok((oid, bytes, sig))
}
/// Similar to [`Doc::sign`], but only returning the [`Signature`].
- pub fn signature_of<G>(&self, signer: &G) -> Result<Signature, DocError>
+ pub fn signature_of<G>(&self, signer: &G, format: ObjectFormat) -> Result<Signature, DocError>
where
G: crypto::signature::Signer<crypto::Signature>,
{
- let (_, _, sig) = self.sign(signer)?;
+ let (_, _, sig) = self.sign(signer, format)?;
Ok(sig)
}
@@ -1305,7 +1311,12 @@ mod test {
let remote = arbitrary::r#gen::<RemoteId>(1);
let proj = arbitrary::r#gen::<RepoId>(1);
let repo = storage.create(proj).unwrap();
- let oid = git::raw::Oid::from_str("2d52a53ce5e4f141148a5f770cfd3ead2d6a45b8").unwrap();
+
+ let oid = git::raw::Oid::from_str_ext(
+ "2d52a53ce5e4f141148a5f770cfd3ead2d6a45b8",
+ git::raw::ObjectFormat::Sha1,
+ )
+ .unwrap();
let err = repo.identity_head_of(&remote).unwrap_err();
{
@@ -1343,7 +1354,7 @@ mod test {
#[quickcheck]
fn prop_encode_decode(doc: Doc) {
- let (_, bytes) = doc.encode().unwrap();
+ let (_, bytes) = doc.encode(ObjectFormat::Sha1).unwrap();
assert_eq!(RawDoc::from_json(&bytes).unwrap().verified().unwrap(), doc);
}
diff --git a/crates/radicle/src/node/notifications/store.rs b/crates/radicle/src/node/notifications/store.rs
index cda113428..5b5d13a3b 100644
--- a/crates/radicle/src/node/notifications/store.rs
+++ b/crates/radicle/src/node/notifications/store.rs
@@ -236,7 +236,7 @@ impl Store<Write> {
/// `Store<Write>` can access these functions as well.
impl<T> Store<T> {
/// Get a specific notification.
- pub fn get(&self, id: NotificationId) -> Result<Notification, Error> {
+ pub fn get(&self, id: NotificationId, zero: git::Oid) -> Result<Notification, Error> {
let mut stmt = self.db.prepare(
"SELECT rowid, repo, ref, old, new, status, timestamp
FROM `repository-notifications`
@@ -245,13 +245,32 @@ impl<T> Store<T> {
stmt.bind((1, id as i64))?;
if let Some(Ok(row)) = stmt.into_iter().next() {
- return parse::notification(row);
+ return parse::notification(row, zero);
+ }
+ Err(Error::NotificationNotFound(id))
+ }
+
+ /// Get the repo that a specific notification is associated with.
+ pub fn get_repo(&self, id: NotificationId) -> Result<RepoId, Error> {
+ let mut stmt = self.db.prepare(
+ "SELECT repo
+ FROM `repository-notifications`
+ WHERE rowid = ?",
+ )?;
+ stmt.bind((1, id as i64))?;
+
+ if let Some(Ok(row)) = stmt.into_iter().next() {
+ let repo = row.try_read::<RepoId, _>("repo")?;
+ return Ok(repo);
}
Err(Error::NotificationNotFound(id))
}
/// Get all notifications.
- pub fn all(&self) -> Result<impl Iterator<Item = Result<Notification, Error>> + '_, Error> {
+ pub fn all(
+ &self,
+ zero: git::Oid,
+ ) -> Result<impl Iterator<Item = Result<Notification, Error>> + '_, Error> {
let stmt = self.db.prepare(
"SELECT rowid, repo, ref, old, new, status, timestamp
FROM `repository-notifications`
@@ -260,7 +279,7 @@ impl<T> Store<T> {
Ok(stmt.into_iter().map(move |row| {
let row = row?;
- parse::notification(row)
+ parse::notification(row, zero)
}))
}
@@ -269,6 +288,7 @@ impl<T> Store<T> {
&self,
since: LocalTime,
until: LocalTime,
+ zero: git::Oid,
) -> Result<impl Iterator<Item = Result<Notification, Error>> + '_, Error> {
let mut stmt = self.db.prepare(
"SELECT rowid, repo, ref, old, new, status, timestamp
@@ -284,7 +304,7 @@ impl<T> Store<T> {
Ok(stmt.into_iter().map(move |row| {
let row = row?;
- parse::notification(row)
+ parse::notification(row, zero)
}))
}
@@ -302,9 +322,11 @@ impl<T> Store<T> {
))?;
stmt.bind((1, repo))?;
+ let zero = Oid::zero(repo.object_format());
+
Ok(stmt.into_iter().map(move |row| {
let row = row?;
- parse::notification(row)
+ parse::notification(row, zero)
}))
}
@@ -365,7 +387,7 @@ impl<T> Store<T> {
mod parse {
use super::*;
- pub fn notification(row: sql::Row) -> Result<Notification, Error> {
+ pub fn notification(row: sql::Row, zero: git::Oid) -> Result<Notification, Error> {
let id = row.try_read::<i64, _>("rowid")? as NotificationId;
let repo = row.try_read::<RepoId, _>("repo")?;
let refstr = row.try_read::<&str, _>("ref")?;
@@ -380,7 +402,8 @@ mod parse {
})
})
})
- .unwrap_or(Ok(git::Oid::ZERO_SHA1))?;
+ .unwrap_or(Ok(zero))?;
+
let new = row
.try_read::<Option<&str>, _>("new")?
.map(|oid| {
@@ -391,7 +414,8 @@ mod parse {
})
})
})
- .unwrap_or(Ok(git::Oid::ZERO_SHA1))?;
+ .unwrap_or(Ok(zero))?;
+
let update = RefUpdate::from(RefString::try_from(refstr)?, old, new);
let (namespace, qualified) = git::parse_ref(refstr)?;
let timestamp = row.try_read::<i64, _>("timestamp")?;
@@ -415,6 +439,7 @@ mod parse {
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod test {
+ use crate::git::ObjectFormat;
use crate::git::fmt::{qualified, refname};
use crate::{cob, node::NodeId, test::arbitrary};
@@ -424,9 +449,10 @@ mod test {
fn test_clear() {
let mut db = Store::open(":memory:").unwrap();
let repo = arbitrary::r#gen::<RepoId>(1);
- let old = arbitrary::oid();
+ let format = repo.object_format();
+ let old = arbitrary::oid(format);
let time = LocalTime::from_millis(32188142);
- let master = arbitrary::oid();
+ let master = arbitrary::oid(format);
for i in 0..3 {
let update = RefUpdate::Updated {
@@ -446,9 +472,10 @@ mod test {
#[test]
fn test_counts_by_repo() {
let mut db = Store::open(":memory:").unwrap();
- let repo1 = arbitrary::r#gen::<RepoId>(1);
- let repo2 = arbitrary::r#gen::<RepoId>(1);
- let oid = arbitrary::oid();
+ let format = arbitrary::r#gen::<ObjectFormat>(1);
+ let repo1 = RepoId::from(arbitrary::oid(format));
+ let repo2 = RepoId::from(arbitrary::oid(format));
+ let oid = arbitrary::oid(format);
let time = LocalTime::from_millis(32188142);
let update1 = RefUpdate::Created {
@@ -480,9 +507,10 @@ mod test {
#[test]
fn test_branch_notifications() {
let repo = arbitrary::r#gen::<RepoId>(1);
- let old = arbitrary::oid();
- let master = arbitrary::oid();
- let other = arbitrary::oid();
+ let format = repo.object_format();
+ let old = arbitrary::oid(format);
+ let master = arbitrary::oid(format);
+ let other = arbitrary::oid(format);
let time1 = LocalTime::from_millis(32188142);
let time2 = LocalTime::from_millis(32189874);
let time3 = LocalTime::from_millis(32189879);
@@ -558,7 +586,8 @@ mod test {
#[test]
fn test_notification_status() {
let repo = arbitrary::r#gen::<RepoId>(1);
- let oid = arbitrary::oid();
+ let format = repo.object_format();
+ let oid = arbitrary::oid(format);
let time = LocalTime::from_millis(32188142);
let mut db = Store::open(":memory:").unwrap();
@@ -601,9 +630,10 @@ mod test {
#[test]
fn test_duplicate_notifications() {
let repo = arbitrary::r#gen::<RepoId>(1);
- let old = arbitrary::oid();
- let master1 = arbitrary::oid();
- let master2 = arbitrary::oid();
+ let format = repo.object_format();
+ let old = arbitrary::oid(format);
+ let master1 = arbitrary::oid(format);
+ let master2 = arbitrary::oid(format);
let time1 = LocalTime::from_millis(32188142);
let time2 = LocalTime::from_millis(32189874);
let mut db = Store::open(":memory:").unwrap();
@@ -649,8 +679,9 @@ mod test {
#[test]
fn test_cob_notifications() {
let repo = arbitrary::r#gen::<RepoId>(1);
- let old = arbitrary::oid();
- let new = arbitrary::oid();
+ let format = repo.object_format();
+ let old = arbitrary::oid(format);
+ let new = arbitrary::oid(format);
let timestamp = LocalTime::from_millis(32189874);
let nid: NodeId = "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
.parse()
diff --git a/crates/radicle/src/node/refs/store.rs b/crates/radicle/src/node/refs/store.rs
index 614a7c5b2..34b15274f 100644
--- a/crates/radicle/src/node/refs/store.rs
+++ b/crates/radicle/src/node/refs/store.rs
@@ -180,12 +180,15 @@ mod test {
use crate::test::arbitrary;
use localtime::{LocalDuration, LocalTime};
+ use radicle_oid::ObjectFormat;
+
#[test]
fn test_count() {
let mut db = Database::memory().unwrap();
- let oid = arbitrary::oid();
+ let format = arbitrary::r#gen::<ObjectFormat>(1);
+ let oid = arbitrary::oid(format);
- let repo = arbitrary::r#gen::<RepoId>(1);
+ let repo = RepoId::from(arbitrary::oid(format));
let namespace = arbitrary::r#gen::<NodeId>(1);
let refname1 = qualified!("refs/heads/master");
let refname2 = qualified!("refs/heads/main");
@@ -211,9 +214,10 @@ mod test {
#[test]
fn test_set_and_delete() {
let mut db = Database::memory().unwrap();
- let oid = arbitrary::oid();
+ let format = arbitrary::r#gen::<ObjectFormat>(1);
+ let oid = arbitrary::oid(format);
- let repo = arbitrary::r#gen::<RepoId>(1);
+ let repo = RepoId::from(arbitrary::oid(format));
let namespace = arbitrary::r#gen::<NodeId>(1);
let refname = qualified!("refs/heads/master");
let timestamp = LocalTime::now();
@@ -228,12 +232,17 @@ mod test {
#[test]
fn test_set_and_get() {
let mut db = Database::memory().unwrap();
- let oid1 = arbitrary::oid();
- let oid2 = arbitrary::oid();
- assert_ne!(oid1, oid2);
+ let format = arbitrary::r#gen::<ObjectFormat>(1);
+
+ let oid1 = arbitrary::oid(format);
+ let mut oid2 = arbitrary::oid(format);
+
+ while oid1 == oid2 {
+ oid2 = arbitrary::oid(format);
+ }
- let repo = arbitrary::r#gen::<RepoId>(1);
+ let repo = RepoId::from(arbitrary::oid(format));
let namespace = arbitrary::r#gen::<NodeId>(1);
let refname = qualified!("refs/heads/master");
let mut timestamp = LocalTime::now();
diff --git a/crates/radicle/src/node/routing.rs b/crates/radicle/src/node/routing.rs
index 008477263..ba8f4a621 100644
--- a/crates/radicle/src/node/routing.rs
+++ b/crates/radicle/src/node/routing.rs
@@ -460,6 +460,7 @@ mod test {
fn test_len() {
let mut db = database(":memory:");
let ids = arbitrary::vec::<RepoId>(10);
+ eprintln!("IDs: {ids:?}");
let node = arbitrary::r#gen(1);
db.add_inventory(&ids, node, LocalTime::now().into())
diff --git a/crates/radicle/src/rad.rs b/crates/radicle/src/rad.rs
index 74d89fd8f..5584fc677 100644
--- a/crates/radicle/src/rad.rs
+++ b/crates/radicle/src/rad.rs
@@ -76,7 +76,8 @@ where
})?;
let doc = identity::Doc::initial(proj, delegate, visibility);
- let (project, identity) = Repository::init(&doc, &storage, signer)?;
+ let (project, identity) =
+ Repository::init(&doc, &storage, signer, repo.object_format().into())?;
let url = git::Url::from(project.id);
match init_configure(repo, &project, &default_branch, &url, identity, signer) {
diff --git a/crates/radicle/src/storage.rs b/crates/radicle/src/storage.rs
index 99c99a69c..e36c1fe59 100644
--- a/crates/radicle/src/storage.rs
+++ b/crates/radicle/src/storage.rs
@@ -7,6 +7,7 @@ use std::path::{Path, PathBuf};
use std::{fmt, io};
use nonempty::NonEmpty;
+use radicle_oid::ObjectFormat;
use serde::{Deserialize, Serialize};
use thiserror::Error;
@@ -632,6 +633,8 @@ pub trait ReadRepository: Sized + ValidateRepository {
/// Get the merge base of two commits.
fn merge_base(&self, left: &Oid, right: &Oid) -> Result<Oid, crate::git::raw::Error>;
+
+ fn object_format(&self) -> ObjectFormat;
}
/// Access the remotes of a repository.
diff --git a/crates/radicle/src/storage/git.rs b/crates/radicle/src/storage/git.rs
index 2a93d4345..9f16ea5f7 100644
--- a/crates/radicle/src/storage/git.rs
+++ b/crates/radicle/src/storage/git.rs
@@ -3,6 +3,7 @@ pub mod cob;
pub mod transport;
pub mod temp;
+use radicle_oid::ObjectFormat;
pub use temp::TempRepository;
use std::collections::{BTreeMap, BTreeSet, HashMap};
@@ -429,13 +430,18 @@ impl Repository {
/// Create a new repository.
pub fn create<P: AsRef<Path>>(path: P, id: RepoId, info: &UserInfo) -> Result<Self, Error> {
- let backend = git::raw::Repository::init_opts(
- &path,
- git::raw::RepositoryInitOptions::new()
- .bare(true)
- .no_reinit(true)
- .external_template(false),
- )?;
+ let opts = {
+ let mut opts = git::raw::RepositoryInitOptions::new();
+
+ opts.bare(true).no_reinit(true).external_template(false);
+
+ #[cfg(feature = "unstable-sha256")]
+ opts.object_format(id.object_format().into());
+
+ opts
+ };
+
+ let backend = git::raw::Repository::init_opts(&path, &opts)?;
{
// Even though `external_template(false)` is called above,
@@ -526,12 +532,13 @@ impl Repository {
doc: &Doc,
storage: &S,
signer: &Device<G>,
+ format: ObjectFormat,
) -> Result<(Self, crate::git::Oid), RepositoryError>
where
G: crypto::signature::Signer<crypto::Signature>,
S: WriteStorage,
{
- let (doc_oid, doc_bytes) = doc.encode()?;
+ let (doc_oid, doc_bytes) = doc.encode(format)?;
let id = RepoId::from(doc_oid);
let repo = Self::create(paths::repository(storage, &id), id, storage.info())?;
let oid = repo.backend.blob(&doc_bytes)?; // Store document blob in repository.
@@ -936,6 +943,10 @@ impl ReadRepository for Repository {
.merge_base(left.into(), right.into())
.map(Oid::from)
}
+
+ fn object_format(&self) -> radicle_oid::ObjectFormat {
+ self.backend.object_format().into()
+ }
}
impl WriteRepository for Repository {
diff --git a/crates/radicle/src/storage/git/cob.rs b/crates/radicle/src/storage/git/cob.rs
index 2ca9eef7c..a0b4b6c6b 100644
--- a/crates/radicle/src/storage/git/cob.rs
+++ b/crates/radicle/src/storage/git/cob.rs
@@ -391,6 +391,10 @@ impl<R: storage::ReadRepository> ReadRepository for DraftStore<'_, R> {
fn merge_base(&self, left: &Oid, right: &Oid) -> Result<Oid, crate::git::raw::Error> {
self.repo.merge_base(left, right)
}
+
+ fn object_format(&self) -> ObjectFormat {
+ self.repo.object_format()
+ }
}
impl<R: storage::WriteRepository> cob::object::Storage for DraftStore<'_, R> {
diff --git a/crates/radicle/src/test/arbitrary.rs b/crates/radicle/src/test/arbitrary.rs
index a5c2fbe79..718e90d35 100644
--- a/crates/radicle/src/test/arbitrary.rs
+++ b/crates/radicle/src/test/arbitrary.rs
@@ -1,4 +1,4 @@
-use std::collections::{BTreeSet, HashMap, HashSet};
+use std::collections::{BTreeSet, HashSet};
use std::hash::Hash;
use std::ops::RangeBounds;
use std::str::FromStr;
@@ -10,6 +10,7 @@ use cyphernet::addr::i2p::I2pAddr;
#[cfg(feature = "tor")]
use cyphernet::{EcPk, addr::tor::OnionAddrV3};
use qcheck::Arbitrary;
+use radicle_oid::ObjectFormat;
use crate::identity::doc::Visibility;
use crate::identity::project::ProjectName;
@@ -24,12 +25,18 @@ use crate::storage;
use crate::test::storage::{MockRepository, MockStorage};
use crate::{cob, git};
-pub fn oid() -> storage::Oid {
- r#gen(1)
+pub fn oid(format: git::ObjectFormat) -> storage::Oid {
+ let mut r#gen = qcheck::Gen::default();
+ loop {
+ let oid = git::Oid::arbitrary(&mut r#gen);
+ if oid.object_format() == format {
+ return oid;
+ }
+ }
}
-pub fn entry_id() -> cob::EntryId {
- self::oid()
+pub fn entry_id(format: git::ObjectFormat) -> cob::EntryId {
+ self::oid(format)
}
pub fn refstring(len: usize) -> git::fmt::RefString {
@@ -65,21 +72,24 @@ pub fn vec<T: Eq + Arbitrary>(size: usize) -> Vec<T> {
vec
}
+pub fn doc_at(g: &mut qcheck::Gen, format: ObjectFormat) -> DocAt {
+ DocAt {
+ commit: oid(format),
+ blob: oid(format),
+ doc: Doc::arbitrary(g),
+ }
+}
+
pub fn nonempty_storage(size: usize) -> MockStorage {
- let mut storage = r#gen::<MockStorage>(size);
+ let g = &mut qcheck::Gen::new(size);
+
+ let mut inventory = Vec::with_capacity(size);
for _ in 0..size {
- let doc = r#gen::<DocAt>(1);
- let id = RepoId::from(doc.blob);
- storage.repos.insert(
- id,
- MockRepository {
- id,
- doc,
- remotes: HashMap::new(),
- },
- );
+ let format = ObjectFormat::arbitrary(g);
+ let rid = RepoId::from(oid(format));
+ inventory.push((rid, doc_at(g, format)));
}
- storage
+ MockStorage::new(inventory)
}
/// Generate a `String` of length `size`, only containing alphanumeric
@@ -179,22 +189,9 @@ impl Arbitrary for Doc {
}
}
-impl Arbitrary for DocAt {
- fn arbitrary(g: &mut qcheck::Gen) -> Self {
- let doc = Doc::arbitrary(g);
-
- DocAt {
- commit: self::oid(),
- blob: self::oid(),
- doc,
- }
- }
-}
-
impl Arbitrary for MockStorage {
fn arbitrary(g: &mut qcheck::Gen) -> Self {
- let inventory = Arbitrary::arbitrary(g);
- MockStorage::new(inventory)
+ nonempty_storage(usize::arbitrary(g).min(5))
}
}
diff --git a/crates/radicle/src/test/storage.rs b/crates/radicle/src/test/storage.rs
index fb5ee4978..1f519a3ac 100644
--- a/crates/radicle/src/test/storage.rs
+++ b/crates/radicle/src/test/storage.rs
@@ -138,7 +138,7 @@ pub struct MockRepository {
impl MockRepository {
pub fn new(id: RepoId, doc: Doc) -> Self {
- let (blob, _) = doc.encode().unwrap();
+ let (blob, _) = doc.encode(id.object_format()).unwrap();
Self {
id,
@@ -220,7 +220,10 @@ impl ReadRepository for MockRepository {
}
fn head(&self) -> Result<(fmt::Qualified<'_>, Oid), RepositoryError> {
- Ok((fmt::qualified!("refs/heads/master"), arbitrary::oid()))
+ Ok((
+ fmt::qualified!("refs/heads/master"),
+ arbitrary::oid(self.object_format()),
+ ))
}
fn canonical_head(&self) -> Result<(fmt::Qualified<'_>, Oid), RepositoryError> {
@@ -337,6 +340,10 @@ impl ReadRepository for MockRepository {
fn merge_base(&self, _left: &Oid, _right: &Oid) -> Result<Oid, crate::git::raw::Error> {
todo!()
}
+
+ fn object_format(&self) -> radicle_oid::ObjectFormat {
+ self.id.object_format()
+ }
}
impl WriteRepository for MockRepository {
commit c4eef1dc1219e40d6bbf73ca28baf96d379139be
Author: Lorenz Leutgeb <lorenz.leutgeb@radicle.xyz>
Date: Sat Apr 25 18:25:34 2026 +0200
cob: SHA-256
diff --git a/crates/radicle-cob/Cargo.toml b/crates/radicle-cob/Cargo.toml
index 89fe314aa..b13e44892 100644
--- a/crates/radicle-cob/Cargo.toml
+++ b/crates/radicle-cob/Cargo.toml
@@ -15,6 +15,7 @@ rust-version.workspace = true
[features]
default = []
+unstable-sha256 = ["git2/unstable-sha256", "radicle-oid/unstable-sha256"]
# Only used for testing. Ensures that commit ids are stable.
stable-commit-ids = []
test = []
diff --git a/crates/radicle-cob/src/backend/git/change.rs b/crates/radicle-cob/src/backend/git/change.rs
index 836e07573..fb55f8804 100644
--- a/crates/radicle-cob/src/backend/git/change.rs
+++ b/crates/radicle-cob/src/backend/git/change.rs
@@ -124,13 +124,9 @@ impl change::Storage for git2::Repository {
let (id, timestamp) = write_commit(
self,
- resource.map(|o| o.into()),
+ resource,
// Commit to tips, extra parents and resource.
- tips.iter()
- .cloned()
- .chain(related.clone())
- .chain(resource)
- .map(git2::Oid::from),
+ tips.iter().cloned().chain(related.clone()).chain(resource),
message,
signature.clone(),
related
@@ -167,7 +163,7 @@ impl change::Storage for git2::Repository {
}
fn load(&self, id: Self::ObjectId) -> Result<Entry, Self::LoadError> {
- let commit = super::commit::Commit::read(self, id.into())?;
+ let commit = super::commit::Commit::read(self, id)?;
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 {
@@ -178,7 +174,6 @@ impl change::Storage for git2::Repository {
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)?
@@ -194,7 +189,7 @@ impl change::Storage for git2::Repository {
return Err(error::Load::TooManyResources(id));
};
- let tree = self.find_tree(*commit.tree())?;
+ let tree = self.find_tree(commit.tree().into())?;
let manifest = load_manifest(self, &tree)?;
let contents = load_contents(self, &tree)?;
@@ -271,8 +266,8 @@ fn load_contents(repo: &git2::Repository, tree: &git2::Tree) -> Result<Contents,
fn write_commit(
repo: &git2::Repository,
- resource: Option<git2::Oid>,
- parents: impl IntoIterator<Item = git2::Oid>,
+ resource: Option<oid::Oid>,
+ parents: impl IntoIterator<Item = oid::Oid>,
message: String,
signature: ExtendedSignature,
trailers: impl IntoIterator<Item = OwnedTrailer>,
@@ -280,7 +275,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()).into()))
+ .chain(resource.map(|r| trailers::CommitTrailer::Resource(r).into()))
.collect();
let author = repo.signature()?;
#[allow(unused_variables)]
@@ -333,7 +328,7 @@ fn write_commit(
};
let oid = Commit::new(
- tree.id(),
+ tree.id().into(),
parents,
author.clone(),
author,
@@ -343,7 +338,7 @@ fn write_commit(
)
.write(repo)?;
- Ok((Oid::from(oid), timestamp as u64))
+ Ok((oid, timestamp as u64))
}
fn write_manifest(
diff --git a/crates/radicle-cob/src/backend/git/commit.rs b/crates/radicle-cob/src/backend/git/commit.rs
index 1d7f7f99a..7148f08fb 100644
--- a/crates/radicle-cob/src/backend/git/commit.rs
+++ b/crates/radicle-cob/src/backend/git/commit.rs
@@ -1,9 +1,10 @@
mod trailers;
use std::fmt;
-use std::str::{self, FromStr};
+use std::str;
-use git2::{ObjectType, Oid};
+use git2::ObjectType;
+use oid::Oid;
use metadata::author::Author;
use metadata::commit::CommitData;
@@ -17,7 +18,7 @@ pub(super) struct Commit(metadata::commit::CommitData<Oid, Oid>);
impl Commit {
pub fn new<P, I, T>(
- tree: git2::Oid,
+ tree: Oid,
parents: P,
author: Author,
committer: Author,
@@ -41,7 +42,7 @@ impl Commit {
/// `oid`.
pub fn read(repo: &git2::Repository, oid: Oid) -> Result<Self, error::Read> {
let odb = repo.odb()?;
- let object = odb.read(oid)?;
+ let object = odb.read(oid.into())?;
Ok(Commit::try_from(object.data())?)
}
@@ -50,7 +51,9 @@ impl 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())?)
+ Ok(odb
+ .write(ObjectType::Commit, self.to_string().as_bytes())?
+ .into())
}
fn verify_for_write(&self, odb: &git2::Odb) -> Result<(), error::Write> {
@@ -67,11 +70,14 @@ fn verify_object(odb: &git2::Odb, oid: &Oid, expected: ObjectType) -> Result<(),
use git2::{Error, ErrorClass, ErrorCode};
let (_, kind) = odb
- .read_header(*oid)
- .map_err(|err| error::Write::OdbRead { oid: *oid, err })?;
+ .read_header(oid.into())
+ .map_err(|err| error::Write::OdbRead {
+ oid: oid.into(),
+ err,
+ })?;
if kind != expected {
Err(error::Write::NotCommit {
- oid: *oid,
+ oid: oid.into(),
err: Error::new(
ErrorCode::NotFound,
ErrorClass::Object,
@@ -135,10 +141,8 @@ impl TryFrom<&[u8]> for Commit {
}
}
-impl FromStr for Commit {
- type Err = error::Parse;
-
- fn from_str(buffer: &str) -> Result<Self, Self::Err> {
+impl Commit {
+ fn from_str(buffer: &str) -> Result<Self, error::Parse> {
let (header, message) = buffer
.split_once("\n\n")
.ok_or(metadata::commit::headers::ParseError::InvalidFormat)?;
@@ -172,7 +176,7 @@ impl fmt::Display for Commit {
}
impl std::ops::Deref for Commit {
- type Target = CommitData<git2::Oid, git2::Oid>;
+ type Target = CommitData<oid::Oid, oid::Oid>;
fn deref(&self) -> &Self::Target {
&self.0
diff --git a/crates/radicle-cob/src/change/store.rs b/crates/radicle-cob/src/change/store.rs
index b4e9485cd..52c23c990 100644
--- a/crates/radicle-cob/src/change/store.rs
+++ b/crates/radicle-cob/src/change/store.rs
@@ -215,18 +215,18 @@ 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 {
+ pub fn oid(&self, format: oid::ObjectFormat) -> Oid {
// SAFETY: This should not fail since we are using a valid object type.
- git2::Oid::hash_object(git2::ObjectType::Blob, &self.content)
+ git2::Oid::hash_object_ext(git2::ObjectType::Blob, &self.content, format.into())
.expect("Embed::oid: invalid object")
.into()
}
/// Return an embed where the content is replaced by a content hash.
- pub fn hashed<T: From<Oid>>(&self) -> Embed<T> {
+ pub fn hashed<T: From<Oid>>(&self, format: oid::ObjectFormat) -> Embed<T> {
Embed {
name: self.name.clone(),
- content: T::from(self.oid()),
+ content: T::from(self.oid(format)),
}
}
}
commit 66f93dcd9b7c00984432114d4f8f20188b3bce27
Author: Lorenz Leutgeb <lorenz.leutgeb@radicle.xyz>
Date: Fri Apr 24 00:33:02 2026 +0200
core: SHA-256
diff --git a/crates/radicle-core/Cargo.toml b/crates/radicle-core/Cargo.toml
index e47bdfabf..74d739fc6 100644
--- a/crates/radicle-core/Cargo.toml
+++ b/crates/radicle-core/Cargo.toml
@@ -14,6 +14,7 @@ rust-version.workspace = true
default = ["std"]
git2 = ["dep:git2", "radicle-oid/git2"]
gix = ["dep:gix-hash", "radicle-oid/gix"]
+unstable-sha256 = []
std = ["radicle-oid/std", "thiserror/std", "schemars/std", "serde/std"]
qcheck = ["dep:qcheck", "radicle-oid/qcheck"]
diff --git a/crates/radicle-core/src/repo.rs b/crates/radicle-core/src/repo.rs
index 7e9bb1c77..a273a3940 100644
--- a/crates/radicle-core/src/repo.rs
+++ b/crates/radicle-core/src/repo.rs
@@ -14,8 +14,21 @@ pub const RAD_PREFIX: &str = "rad:";
pub enum IdError {
#[error(transparent)]
Multibase(#[from] multibase::Error),
- #[error("invalid length: expected {} bytes, got {actual} bytes", Oid::LEN_SHA1)]
+
+ #[cfg_attr(
+ not(feature = "unstable-sha256"),
+ error("invalid length: expected {} bytes, got {actual} bytes", Oid::LEN_SHA1)
+ )]
+ #[cfg_attr(
+ feature = "unstable-sha256",
+ error(
+ "invalid length: expected {} or {} bytes, got {actual} bytes",
+ Oid::LEN_SHA1,
+ Oid::LEN_SHA256
+ )
+ )]
Length { actual: usize },
+
#[error(fmt = fmt_mismatched_base_encoding)]
MismatchedBaseEncoding {
input: String,
@@ -108,11 +121,27 @@ impl RepoId {
pub fn from_canonical(input: &str) -> Result<Self, IdError> {
let (base, bytes) = multibase::decode(input)?;
Self::guard_base_encoding(input, base)?;
- let bytes: [u8; Oid::LEN_SHA1] =
- bytes.try_into().map_err(|bytes: Vec<u8>| IdError::Length {
- actual: bytes.len(),
- })?;
- Ok(Self(Oid::from_sha1(bytes)))
+
+ if bytes.len() == Oid::LEN_SHA1 {
+ let bytes: [u8; Oid::LEN_SHA1] =
+ bytes.try_into().map_err(|bytes: Vec<u8>| IdError::Length {
+ actual: bytes.len(),
+ })?;
+ return Ok(Self(Oid::from_sha1(bytes)));
+ }
+
+ #[cfg(feature = "unstable-sha256")]
+ if bytes.len() == Oid::LEN_SHA256 {
+ let bytes: [u8; Oid::LEN_SHA256] =
+ bytes.try_into().map_err(|bytes: Vec<u8>| IdError::Length {
+ actual: bytes.len(),
+ })?;
+ return Ok(Self(Oid::from_sha256(bytes)));
+ }
+
+ Err(IdError::Length {
+ actual: bytes.len(),
+ })
}
fn guard_base_encoding(input: &str, base: multibase::Base) -> Result<(), IdError> {
commit 1bc672ea6c1402f1dc13f5549eb2f107835a28a2
Author: Lorenz Leutgeb <lorenz.leutgeb@radicle.xyz>
Date: Thu Apr 23 17:43:24 2026 +0200
oid: Introduce unstable support for SHA-256
diff --git a/Cargo.lock b/Cargo.lock
index a7aaf80b2..e6c981363 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1435,6 +1435,7 @@ dependencies = [
"faster-hex",
"gix-features",
"sha1-checked",
+ "sha2",
"thiserror 2.0.18",
]
diff --git a/crates/radicle-oid/Cargo.toml b/crates/radicle-oid/Cargo.toml
index f01847b28..9b318cb9c 100644
--- a/crates/radicle-oid/Cargo.toml
+++ b/crates/radicle-oid/Cargo.toml
@@ -15,6 +15,7 @@ default = ["sha1", "std"]
gix = ["dep:gix-hash"]
std = []
sha1 = []
+unstable-sha256 = ["gix-hash/sha256", "git2/unstable-sha256"]
[dependencies]
git2 = { workspace = true, optional = true, default-features = false }
diff --git a/crates/radicle-oid/src/lib.rs b/crates/radicle-oid/src/lib.rs
index ea1a181f0..8a6f464fe 100644
--- a/crates/radicle-oid/src/lib.rs
+++ b/crates/radicle-oid/src/lib.rs
@@ -74,16 +74,28 @@ extern crate alloc;
#[cfg(not(feature = "sha1"))]
compile_error!("The `sha1` feature is required.");
+#[derive(Clone, Copy, PartialEq, Eq, Debug)]
+pub enum ObjectFormat {
+ #[cfg(feature = "sha1")]
+ Sha1 = 1,
+ #[cfg(feature = "unstable-sha256")]
+ Sha256 = 2,
+}
+
#[derive(PartialEq, Eq, Ord, PartialOrd, Clone, Copy)]
#[non_exhaustive]
pub enum Oid {
+ #[cfg(feature = "sha1")]
Sha1([u8; Self::LEN_SHA1]),
+ #[cfg(feature = "unstable-sha256")]
+ Sha256([u8; Self::LEN_SHA256]),
}
/// 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?
+#[cfg(feature = "sha1")]
impl Oid {
/// The length of a SHA-1 object identifier in bytes.
pub const LEN_SHA1: usize = 20;
@@ -102,6 +114,34 @@ impl Oid {
pub fn into_sha1(&self) -> Option<[u8; Self::LEN_SHA1]> {
match self {
Oid::Sha1(digest) => Some(*digest),
+ #[cfg(feature = "unstable-sha256")]
+ _ => None,
+ }
+ }
+}
+
+#[cfg(feature = "unstable-sha256")]
+/// Conversions to/from SHA-256.
+impl Oid {
+ /// The length of a SHA-256 object identifier in bytes.
+ pub const LEN_SHA256: usize = 32;
+
+ /// A SHA-256 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_SHA256: Self = Self::Sha256([0u8; Self::LEN_SHA256]);
+
+ pub fn from_sha256(digest: [u8; Self::LEN_SHA256]) -> Self {
+ Self::Sha256(digest)
+ }
+
+ pub fn into_sha256(&self) -> Option<[u8; Self::LEN_SHA256]> {
+ match self {
+ Oid::Sha256(digest) => Some(*digest),
+ #[cfg(feature = "sha1")]
+ _ => None,
}
}
}
@@ -111,8 +151,35 @@ impl Oid {
/// Test whether all bytes in this object identifier are zero.
/// See also [`::git2::Oid::is_zero`].
pub fn is_zero(&self) -> bool {
+ <Self as AsRef<[u8]>>::as_ref(self).iter().all(|b| *b == 0)
+ }
+
+ pub fn zero(format: ObjectFormat) -> Self {
+ match format {
+ #[cfg(feature = "sha1")]
+ ObjectFormat::Sha1 => Self::ZERO_SHA1,
+ #[cfg(feature = "unstable-sha256")]
+ ObjectFormat::Sha256 => Self::ZERO_SHA256,
+ }
+ }
+}
+
+impl Oid {
+ pub fn object_format(&self) -> ObjectFormat {
match self {
- Oid::Sha1(array) => array.iter().all(|b| *b == 0),
+ #[cfg(feature = "sha1")]
+ Oid::Sha1(_) => ObjectFormat::Sha1,
+ #[cfg(feature = "unstable-sha256")]
+ Oid::Sha256(_) => ObjectFormat::Sha256,
+ }
+ }
+
+ pub fn len(&self) -> usize {
+ match self {
+ #[cfg(feature = "sha1")]
+ Oid::Sha1(_) => Self::LEN_SHA1,
+ #[cfg(feature = "unstable-sha256")]
+ Oid::Sha256(_) => Self::LEN_SHA256,
}
}
}
@@ -120,7 +187,10 @@ impl Oid {
impl AsRef<[u8]> for Oid {
fn as_ref(&self) -> &[u8] {
match self {
+ #[cfg(feature = "sha1")]
Oid::Sha1(array) => array,
+ #[cfg(feature = "unstable-sha256")]
+ Oid::Sha256(array) => array,
}
}
}
@@ -128,7 +198,10 @@ impl AsRef<[u8]> for Oid {
impl From<Oid> for alloc::boxed::Box<[u8]> {
fn from(oid: Oid) -> Self {
match oid {
+ #[cfg(feature = "sha1")]
Oid::Sha1(array) => alloc::boxed::Box::new(array),
+ #[cfg(feature = "unstable-sha256")]
+ Oid::Sha256(array) => alloc::boxed::Box::new(array),
}
}
}
@@ -138,8 +211,13 @@ pub mod str {
use core::str;
/// Length of the string representation of a SHA-1 digest in hexadecimal notation.
+ #[cfg(feature = "sha1")]
pub(super) const SHA1_DIGEST_STR_LEN: usize = Oid::LEN_SHA1 * 2;
+ /// Length of the string representation of a SHA-256 digest in hexadecimal notation.
+ #[cfg(feature = "unstable-sha256")]
+ pub(super) const SHA256_DIGEST_STR_LEN: usize = Oid::LEN_SHA256 * 2;
+
impl str::FromStr for Oid {
type Err = error::ParseOidError;
@@ -147,25 +225,42 @@ pub mod str {
use error::ParseOidError::*;
let len = s.len();
- if len != SHA1_DIGEST_STR_LEN {
- return Err(Len(len));
+
+ #[cfg(feature = "sha1")]
+ if len == SHA1_DIGEST_STR_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 })?;
+ }
+
+ return Ok(Self::Sha1(bytes));
}
- 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 })?;
+ #[cfg(feature = "unstable-sha256")]
+ if len == SHA256_DIGEST_STR_LEN {
+ let mut bytes = [0u8; Oid::LEN_SHA256];
+ for i in 0..Oid::LEN_SHA256 {
+ bytes[i] = u8::from_str_radix(&s[i * 2..=i * 2 + 1], 16)
+ .map_err(|source| At { index: i, source })?;
+ }
+
+ return Ok(Self::Sha256(bytes));
}
- Ok(Self::Sha1(bytes))
+ Err(Len(len))
}
}
pub mod error {
use core::{fmt, num};
+ #[cfg(feature = "sha1")]
use super::SHA1_DIGEST_STR_LEN;
+ #[cfg(feature = "unstable-sha256")]
+ use super::SHA256_DIGEST_STR_LEN;
+
pub enum ParseOidError {
Len(usize),
At {
@@ -178,9 +273,24 @@ pub mod str {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use ParseOidError::*;
match self {
+ #[cfg(all(feature = "sha1", not(feature = "unstable-sha256")))]
Len(len) => {
write!(f, "invalid length (have {len}, want {SHA1_DIGEST_STR_LEN})")
}
+ #[cfg(all(not(feature = "sha1"), feature = "unstable-sha256"))]
+ Len(len) => {
+ write!(
+ f,
+ "invalid length (have {len}, want {SHA256_DIGEST_STR_LEN})"
+ )
+ }
+ #[cfg(all(feature = "sha1", feature = "unstable-sha256"))]
+ Len(len) => {
+ write!(
+ f,
+ "invalid length (have {len}, want {SHA1_DIGEST_STR_LEN} or {SHA256_DIGEST_STR_LEN})"
+ )
+ }
At { index, source } => write!(
f,
"parse error at byte {index} (characters {} and {}): {source}",
@@ -215,31 +325,72 @@ pub mod str {
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,
- ])
- );
+ #[cfg(feature = "sha1")]
+ mod sha1 {
+ use super::*;
+
+ #[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::ZERO_SHA1
+ );
+ }
}
- #[test]
- fn zero() {
- assert_eq!(
- "0000000000000000000000000000000000000000"
- .parse::<Oid>()
- .unwrap(),
- Oid::ZERO_SHA1
- );
+ #[cfg(feature = "unstable-sha256")]
+ mod sha256 {
+ use super::*;
+
+ #[test]
+ fn fixture() {
+ assert_eq!(
+ "123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0"
+ .parse::<Oid>()
+ .unwrap(),
+ Oid::from_sha256([
+ 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78,
+ 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
+ 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
+ ])
+ );
+ }
+
+ #[test]
+ fn zero() {
+ assert_eq!(
+ "0000000000000000000000000000000000000000000000000000000000000000"
+ .parse::<Oid>()
+ .unwrap(),
+ Oid::ZERO_SHA256
+ );
+ }
}
#[quickcheck]
fn git2_roundtrip(oid: Oid) {
+ #[cfg(feature = "unstable-sha256")]
+ if matches!(oid, Oid::Sha256(_)) {
+ // `git2::Oid` does not support SHA-256, so skip this test for
+ // SHA-256 object identifiers.
+ return;
+ }
+
let other = git2::Oid::from(oid);
let other = other.to_string();
let other = other.parse::<Oid>().unwrap();
@@ -265,6 +416,7 @@ mod fmt {
impl fmt::Display for Oid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
+ #[cfg(feature = "sha1")]
Oid::Sha1(digest) =>
// SAFETY (for all 20 blocks below): The length of `digest` is
// known to be `SHA1_DIGEST_LEN`, which is 20.
@@ -291,7 +443,47 @@ mod fmt {
unsafe { digest.get_unchecked(17) },
unsafe { digest.get_unchecked(18) },
unsafe { digest.get_unchecked(19) },
- ).fmt(f)
+ ).fmt(f),
+ #[cfg(feature = "unstable-sha256")]
+ Oid::Sha256(digest) =>
+ // SAFETY (for all 32 blocks below): The length of `digest` is
+ // known to be `SHA256_DIGEST_LEN`, which is 32.
+ // 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}{: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) },
+ unsafe { digest.get_unchecked(20) },
+ unsafe { digest.get_unchecked(21) },
+ unsafe { digest.get_unchecked(22) },
+ unsafe { digest.get_unchecked(23) },
+ unsafe { digest.get_unchecked(24) },
+ unsafe { digest.get_unchecked(25) },
+ unsafe { digest.get_unchecked(26) },
+ unsafe { digest.get_unchecked(27) },
+ unsafe { digest.get_unchecked(28) },
+ unsafe { digest.get_unchecked(29) },
+ unsafe { digest.get_unchecked(30) },
+ unsafe { digest.get_unchecked(31) },
+ ).fmt(f),
}
}
}
@@ -308,28 +500,66 @@ mod fmt {
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"
- );
+ #[cfg(feature = "sha1")]
+ mod sha1 {
+ use super::*;
+
+ #[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::ZERO_SHA1.to_string(),
+ "0000000000000000000000000000000000000000"
+ );
+ }
}
- #[test]
- fn zero() {
- assert_eq!(
- Oid::ZERO_SHA1.to_string(),
- "0000000000000000000000000000000000000000"
- );
+ #[cfg(feature = "unstable-sha256")]
+ mod sha256 {
+ use super::*;
+
+ #[test]
+ fn fixture() {
+ assert_eq!(
+ Oid::from_sha256([
+ 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78,
+ 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
+ 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0
+ ])
+ .to_string(),
+ "123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0"
+ );
+ }
+
+ #[test]
+ fn zero() {
+ assert_eq!(
+ Oid::ZERO_SHA256.to_string(),
+ "0000000000000000000000000000000000000000000000000000000000000000"
+ );
+ }
}
#[quickcheck]
fn git2(oid: Oid) {
+ #[cfg(feature = "unstable-sha256")]
+ if matches!(oid, Oid::Sha256(_)) {
+ // `git2::Oid` does not support SHA-256, so skip this test for
+ // SHA-256 object identifiers.
+ return;
+ }
+
assert_eq!(oid.to_string(), git2::Oid::from(oid).to_string());
}
@@ -365,11 +595,13 @@ mod std {
mod gix {
use gix_hash::ObjectId as Other;
+ use super::ObjectFormat;
use super::Oid;
impl From<Other> for Oid {
fn from(other: Other) -> Self {
match other {
+ #[cfg(feature = "sha1")]
Other::Sha1(digest) => Self::Sha1(digest),
_ => unimplemented!("conversion from {other:?} into radicle_oid::Oid"),
}
@@ -379,7 +611,10 @@ mod gix {
impl From<Oid> for Other {
fn from(oid: Oid) -> Other {
match oid {
+ #[cfg(feature = "sha1")]
Oid::Sha1(digest) => Other::Sha1(digest),
+ #[cfg(feature = "unstable-sha256")]
+ Oid::Sha256(digest) => Other::Sha256(digest),
}
}
}
@@ -387,7 +622,12 @@ mod gix {
impl core::cmp::PartialEq<Other> for Oid {
fn eq(&self, other: &Other) -> bool {
match (self, other) {
+ #[cfg(feature = "sha1")]
(Oid::Sha1(a), Other::Sha1(b)) => a == b,
+ #[cfg(feature = "unstable-sha256")]
+ (Oid::Sha256(a), Other::Sha256(b)) => a == b,
+ #[cfg(all(feature = "sha1", feature = "unstable-sha256"))]
+ (Oid::Sha1(_), Other::Sha256(_)) | (Oid::Sha256(_), Other::Sha1(_)) => false,
_ => unimplemented!("conversion from {other:?} into radicle_oid::Oid"),
}
}
@@ -396,7 +636,33 @@ mod gix {
impl AsRef<gix_hash::oid> for Oid {
fn as_ref(&self) -> &gix_hash::oid {
match self {
+ #[cfg(feature = "sha1")]
Oid::Sha1(digest) => gix_hash::oid::from_bytes_unchecked(digest),
+ #[cfg(feature = "unstable-sha256")]
+ Oid::Sha256(digest) => gix_hash::oid::from_bytes_unchecked(digest),
+ }
+ }
+ }
+
+ impl From<gix_hash::Kind> for ObjectFormat {
+ fn from(kind: gix_hash::Kind) -> Self {
+ match kind {
+ #[cfg(feature = "sha1")]
+ gix_hash::Kind::Sha1 => Self::Sha1,
+ #[cfg(feature = "unstable-sha256")]
+ gix_hash::Kind::Sha256 => Self::Sha256,
+ _ => unimplemented!("conversion from {kind:?} into radicle_oid::ObjectFormat"),
+ }
+ }
+ }
+
+ impl From<ObjectFormat> for gix_hash::Kind {
+ fn from(format: ObjectFormat) -> Self {
+ match format {
+ #[cfg(feature = "sha1")]
+ ObjectFormat::Sha1 => Self::Sha1,
+ #[cfg(feature = "unstable-sha256")]
+ ObjectFormat::Sha256 => Self::Sha256,
}
}
}
@@ -406,9 +672,24 @@ mod gix {
use super::*;
use gix_hash::Kind;
- #[test]
- fn zero() {
- assert!(Oid::ZERO_SHA1 == Other::null(Kind::Sha1));
+ #[cfg(feature = "sha1")]
+ mod sha1 {
+ use super::*;
+
+ #[test]
+ fn zero() {
+ assert!(Oid::ZERO_SHA1 == Other::null(Kind::Sha1));
+ }
+ }
+
+ #[cfg(feature = "unstable-sha256")]
+ mod sha256 {
+ use super::*;
+
+ #[test]
+ fn zero() {
+ assert!(Oid::ZERO_SHA256 == Other::null(Kind::Sha256));
+ }
}
}
}
@@ -419,18 +700,41 @@ mod git2 {
use super::*;
- const EXPECT: &str = "git2::Oid must be exactly 20 bytes long";
+ #[cfg(feature = "sha1")]
+ const EXPECT_SHA1: &str = "git2::Oid must be exactly 20 bytes long";
+
+ #[cfg(feature = "unstable-sha256")]
+ const EXPECT_SHA256: &str = "git2::Oid must be exactly 20 bytes long";
+ #[cfg(feature = "sha1")]
impl From<Other> for Oid {
fn from(other: Other) -> Self {
- Self::Sha1(other.as_bytes().try_into().expect(EXPECT))
+ match other.object_format() {
+ #[cfg(feature = "sha1")]
+ ::git2::ObjectFormat::Sha1 => {
+ Self::Sha1(other.as_bytes().try_into().expect(EXPECT_SHA1))
+ }
+ #[cfg(feature = "unstable-sha256")]
+ ::git2::ObjectFormat::Sha256 => {
+ Self::Sha256(other.as_bytes().try_into().expect(EXPECT_SHA256))
+ }
+ #[cfg(all(feature = "sha1", not(feature = "unstable-sha256")))]
+ _ => {
+ unimplemented!(
+ "conversion from {other:?} into radicle_oid::Oid since object format is not equal to SHA-1",
+ )
+ }
+ }
}
}
impl From<Oid> for Other {
fn from(oid: Oid) -> Self {
match oid {
- Oid::Sha1(array) => Other::from_bytes(&array).expect(EXPECT),
+ #[cfg(feature = "sha1")]
+ Oid::Sha1(array) => Other::from_bytes(&array).expect(EXPECT_SHA1),
+ #[cfg(feature = "unstable-sha256")]
+ Oid::Sha256(array) => Other::from_bytes(&array).expect(EXPECT_SHA256),
}
}
}
@@ -438,7 +742,10 @@ mod git2 {
impl From<&Oid> for Other {
fn from(oid: &Oid) -> Self {
match oid {
- Oid::Sha1(array) => Other::from_bytes(array).expect(EXPECT),
+ #[cfg(feature = "sha1")]
+ Oid::Sha1(array) => Other::from_bytes(array).expect(EXPECT_SHA1),
+ #[cfg(feature = "unstable-sha256")]
+ Oid::Sha256(array) => Other::from_bytes(array).expect(EXPECT_SHA256),
}
}
}
@@ -449,7 +756,35 @@ mod git2 {
}
}
- #[cfg(test)]
+ impl From<ObjectFormat> for ::git2::ObjectFormat {
+ fn from(format: ObjectFormat) -> Self {
+ match format {
+ #[cfg(feature = "sha1")]
+ ObjectFormat::Sha1 => Self::Sha1,
+ #[cfg(feature = "unstable-sha256")]
+ ObjectFormat::Sha256 => Self::Sha256,
+ }
+ }
+ }
+
+ impl From<::git2::ObjectFormat> for ObjectFormat {
+ fn from(format: ::git2::ObjectFormat) -> Self {
+ match format {
+ #[cfg(feature = "sha1")]
+ ::git2::ObjectFormat::Sha1 => Self::Sha1,
+ #[cfg(feature = "unstable-sha256")]
+ ::git2::ObjectFormat::Sha256 => Self::Sha256,
+ #[cfg(all(feature = "sha1", not(feature = "unstable-sha256")))]
+ _ => {
+ unimplemented!(
+ "conversion from {format:?} into radicle_oid::ObjectFormat since it is not equal to SHA-1",
+ )
+ }
+ }
+ }
+ }
+
+ #[cfg(all(feature = "sha1", test))]
mod test {
use super::*;
@@ -468,9 +803,45 @@ mod test {
use crate::*;
impl Arbitrary for Oid {
+ #[cfg(all(feature = "sha1", not(feature = "unstable-sha256")))]
fn arbitrary(g: &mut Gen) -> Self {
Self::Sha1(<[u8; Oid::LEN_SHA1]>::arbitrary(g))
}
+
+ #[cfg(all(not(feature = "sha1"), feature = "unstable-sha256"))]
+ fn arbitrary(g: &mut Gen) -> Self {
+ Self::Sha256(<[u8; Oid::LEN_SHA256]>::arbitrary(g))
+ }
+
+ #[cfg(all(feature = "sha1", feature = "unstable-sha256"))]
+ fn arbitrary(g: &mut Gen) -> Self {
+ if bool::arbitrary(g) {
+ Self::Sha1(<[u8; Oid::LEN_SHA1]>::arbitrary(g).try_into().unwrap())
+ } else {
+ Self::Sha256(<[u8; Oid::LEN_SHA256]>::arbitrary(g).try_into().unwrap())
+ }
+ }
+ }
+
+ impl Arbitrary for ObjectFormat {
+ #[cfg(all(feature = "sha1", not(feature = "unstable-sha256")))]
+ fn arbitrary(_g: &mut Gen) -> Self {
+ Self::Sha1
+ }
+
+ #[cfg(all(not(feature = "sha1"), feature = "unstable-sha256"))]
+ fn arbitrary(g: &mut Gen) -> Self {
+ Self::Sha256
+ }
+
+ #[cfg(all(feature = "sha1", feature = "unstable-sha256"))]
+ fn arbitrary(g: &mut Gen) -> Self {
+ if bool::arbitrary(g) {
+ Self::Sha1
+ } else {
+ Self::Sha256
+ }
+ }
}
}
}
commit 076fe48d46ae590594f161f0f60fa0609f8d8aa0
Author: Lorenz Leutgeb <lorenz.leutgeb@radicle.dev>
Date: Mon Jun 22 16:53:16 2026 +0200
git2: 0.20 → 0.21
diff --git a/Cargo.lock b/Cargo.lock
index 4bc7e3d4b..a7aaf80b2 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1228,9 +1228,9 @@ checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
[[package]]
name = "git-ref-format"
-version = "0.6.0"
+version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ed6913a77cee9e231cab93577c9a5eea84a1344ab39294d91dc075b3c24499d0"
+checksum = "c56b3fd79d4f8678e804bf9e60ef31bc810889d702df1e7280d800b3c907e1af"
dependencies = [
"git-ref-format-core",
"git-ref-format-macro",
@@ -1249,27 +1249,26 @@ dependencies = [
[[package]]
name = "git-ref-format-macro"
-version = "0.6.0"
+version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4e730f09c82961c28f5465b83da0aa5c2716156ce57da33a1fa51bbd560aa5f7"
+checksum = "d38795f40abc04aed7473ce282be6a5da97ae58a1445a014193d3bf302b034cf"
dependencies = [
"git-ref-format-core",
- "proc-macro-error2",
+ "proc-macro-error3",
"quote",
"syn 2.0.117",
]
[[package]]
name = "git2"
-version = "0.20.4"
+version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b"
+checksum = "ddddbf932745a6be37109b6112d3ee09696106f848449069d3a57bba937ab82e"
dependencies = [
"bitflags",
"libc",
"libgit2-sys",
"log",
- "url",
]
[[package]]
@@ -2195,9 +2194,9 @@ checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
[[package]]
name = "libgit2-sys"
-version = "0.18.3+1.9.2"
+version = "0.18.4+1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c9b3acc4b91781bb0b3386669d325163746af5f6e4f73e6d2d630e09a35f3487"
+checksum = "9b26f66f35e1871b22efcf7191564123d2a446ca0538cde63c23adfefa9b15b7"
dependencies = [
"cc",
"libc",
@@ -2800,22 +2799,22 @@ dependencies = [
]
[[package]]
-name = "proc-macro-error-attr2"
-version = "2.0.0"
+name = "proc-macro-error-attr3"
+version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
+checksum = "34e4dd828515431dd6c4a030d26f7eaed7dd4778226e9d2bb968d65ca4ec3d4d"
dependencies = [
"proc-macro2",
"quote",
]
[[package]]
-name = "proc-macro-error2"
-version = "2.0.1"
+name = "proc-macro-error3"
+version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
+checksum = "5ee475e440453418ff1335189eddf7101ba502cd818ab7ae04209bc83aa925aa"
dependencies = [
- "proc-macro-error-attr2",
+ "proc-macro-error-attr3",
"proc-macro2",
"quote",
"syn 2.0.117",
@@ -3105,9 +3104,9 @@ dependencies = [
[[package]]
name = "radicle-git-ext"
-version = "0.12.0"
+version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db68f47aaf6b8352a733da684f7e24f89aeb03131598628f147ff1bcc633670d"
+checksum = "5a30b3153c4db6898f21ad8b0d7f2d6fe04434ec9b66764ef34e23b0b05167ba"
dependencies = [
"git-ref-format",
"git2",
@@ -3267,9 +3266,9 @@ checksum = "fb935931bdd2a2966f3b584f3031d9d54ec0713ddbc563a0193d54e62a88ec73"
[[package]]
name = "radicle-surf"
-version = "0.27.1"
+version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a2dfaa46d3e05ca10fc5841bb9cfac9fe6d3685cf2a02b16972f4cafbb888bd1"
+checksum = "ee7a801291a59d24a533fa4923721181e2ca1d588c8418cdbf619a81b726d273"
dependencies = [
"anyhow",
"base64 0.21.7",
diff --git a/Cargo.toml b/Cargo.toml
index 3c286198d..5def169f9 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -30,7 +30,7 @@ cypheraddr = "0.4.1"
cyphernet = "0.5.4"
dunce = "1.0.5"
fastrand = { version = "2.0.0", default-features = false }
-git2 = { version = "0.20.4", default-features = false, features = ["vendored-libgit2"] }
+git2 = { version = "0.21", default-features = false, features = ["vendored-libgit2"] }
gix-hash = { version = "0.25.0", default-features = false, features = ["sha1"] }
gix-packetline = { version = "0.21.1", default-features = false }
human-panic = "2.0.6"
@@ -85,7 +85,7 @@ zeroize = "1.5.7"
# `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.6.0", default-features = false }
-radicle-surf = "0.27.1"
+radicle-surf = "0.28.0"
[workspace.lints.clippy]
fallible_impl_from = "deny"
diff --git a/crates/radicle-cli/src/commands/inbox.rs b/crates/radicle-cli/src/commands/inbox.rs
index 137a28f4e..b92935ebb 100644
--- a/crates/radicle-cli/src/commands/inbox.rs
+++ b/crates/radicle-cli/src/commands/inbox.rs
@@ -273,7 +273,7 @@ impl NotificationRow {
S: ReadRepository,
{
let commit = if let Some(head) = n.update.new() {
- repo.commit(head)?.summary().unwrap_or_default().to_owned()
+ repo.commit(head)?.summary()?.unwrap_or_default().to_owned()
} else {
String::new()
};
@@ -371,7 +371,7 @@ impl NotificationRow {
S: ReadRepository,
{
let commit = if let Some(head) = n.update.new() {
- repo.commit(head)?.summary().unwrap_or_default().to_owned()
+ repo.commit(head)?.summary()?.unwrap_or_default().to_owned()
} else {
String::new()
};
diff --git a/crates/radicle-cli/src/commands/init.rs b/crates/radicle-cli/src/commands/init.rs
index d0ab135cb..6ccdaacb1 100644
--- a/crates/radicle-cli/src/commands/init.rs
+++ b/crates/radicle-cli/src/commands/init.rs
@@ -38,9 +38,9 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
}
Err(e) => return Err(e.into()),
};
- if let Ok((remote, _)) = git::rad_remote(&repo)
- && let Some(remote) = remote.url()
- {
+
+ if let Ok((remote, _)) = git::rad_remote(&repo) {
+ let remote = remote.url()?;
bail!("repository is already initialized with remote {remote}");
}
@@ -564,17 +564,17 @@ enum DefaultBranchError {
}
fn find_default_branch(repo: &raw::Repository) -> Result<String, DefaultBranchError> {
- match find_init_default_branch(repo).ok().flatten() {
+ match find_init_default_branch(repo).ok() {
Some(refname) => Ok(refname),
None => Ok(find_repository_head(repo)?),
}
}
-fn find_init_default_branch(repo: &raw::Repository) -> Result<Option<String>, raw::Error> {
+fn find_init_default_branch(repo: &raw::Repository) -> Result<String, raw::Error> {
let config = repo.config().and_then(|mut c| c.snapshot())?;
let default_branch = config.get_str("init.defaultbranch")?;
let branch = repo.find_branch(default_branch, raw::BranchType::Local)?;
- Ok(branch.into_reference().shorthand().map(ToOwned::to_owned))
+ Ok(branch.into_reference().shorthand()?.to_owned())
}
fn find_repository_head(repo: &raw::Repository) -> Result<String, DefaultBranchError> {
@@ -583,6 +583,7 @@ fn find_repository_head(repo: &raw::Repository) -> Result<String, DefaultBranchE
Err(e) => Err(DefaultBranchError::Git(e)),
Ok(head) => head
.shorthand()
+ .ok()
.filter(|refname| *refname != "HEAD")
.ok_or(DefaultBranchError::Head)
.map(|refname| refname.to_owned()),
diff --git a/crates/radicle-cli/src/commands/inspect.rs b/crates/radicle-cli/src/commands/inspect.rs
index 7f7f2e7b3..6cd54677f 100644
--- a/crates/radicle-cli/src/commands/inspect.rs
+++ b/crates/radicle-cli/src/commands/inspect.rs
@@ -189,16 +189,15 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
term::println(format_args!("date {time}"));
term::blank();
- if let Some(msg) = tip.message() {
- for line in msg.lines() {
- if line.is_empty() {
- term::blank();
- } else {
- term::indented(term::format::dim(line));
- }
+ for line in tip.message()?.lines() {
+ if line.is_empty() {
+ term::blank();
+ } else {
+ term::indented(term::format::dim(line));
}
- term::blank();
}
+ term::blank();
+
for line in json::to_pretty(&doc, Path::new("radicle.json"))? {
term::println(format_args!(" {line}"));
}
diff --git a/crates/radicle-cli/src/git.rs b/crates/radicle-cli/src/git.rs
index 1dd074f0c..e449b0fac 100644
--- a/crates/radicle-cli/src/git.rs
+++ b/crates/radicle-cli/src/git.rs
@@ -98,9 +98,13 @@ impl<'a> TryFrom<git::raw::Remote<'a>> for Remote<'a> {
})?;
let pushurl = value
.pushurl()
+ .map_err(|_| RemoteError::MissingUrl)?
.map(radicle::git::Url::from_str)
.transpose()?;
- let name = value.name().ok_or(RemoteError::MissingName)?;
+ let name = value
+ .name()
+ .map_err(|_| RemoteError::MissingName)?
+ .ok_or(RemoteError::MissingName)?;
Ok(Self {
name: name.to_owned(),
@@ -257,7 +261,7 @@ pub fn rad_remotes(repo: &Repository) -> anyhow::Result<Vec<Remote<'_>>> {
.remotes()?
.iter()
.filter_map(|name| {
- let remote = repo.find_remote(name?).ok()?;
+ let remote = repo.find_remote(name.ok()??).ok()?;
Remote::try_from(remote).ok()
})
.collect();
diff --git a/crates/radicle-cli/src/terminal/patch.rs b/crates/radicle-cli/src/terminal/patch.rs
index b8d976b2c..ce1fdb9b3 100644
--- a/crates/radicle-cli/src/terminal/patch.rs
+++ b/crates/radicle-cli/src/terminal/patch.rs
@@ -30,8 +30,6 @@ pub enum Error {
Git(#[from] git::raw::Error),
#[error("i/o error: {0}")]
Io(#[from] io::Error),
- #[error("invalid utf-8 string")]
- InvalidUtf8,
}
/// The user supplied `Patch` description.
@@ -152,7 +150,7 @@ fn message_from_commits(name: &str, commits: Vec<git::raw::Commit>) -> Result<St
let Some(commit) = commits.next() else {
return Ok(String::default());
};
- let commit_msg = commit.message().ok_or(Error::InvalidUtf8)?.to_string();
+ let commit_msg = commit.message()?.to_string();
if count == 1 {
return Ok(commit_msg);
@@ -172,7 +170,7 @@ fn message_from_commits(name: &str, commits: Vec<git::raw::Commit>) -> Result<St
writeln!(&mut msg)?;
for (i, commit) in commits.enumerate() {
- let commit_msg = commit.message().ok_or(Error::InvalidUtf8)?.trim_end();
+ let commit_msg = commit.message()?.trim_end();
let commit_num = i + 2;
writeln!(&mut msg, "<!--")?;
@@ -488,7 +486,7 @@ fn patch_commit_lines(
term::format::oid(commit.id()).into(),
)),
term::label(term::format::default(
- commit.summary().unwrap_or_default().to_owned(),
+ commit.summary()?.unwrap_or_default().to_owned(),
)),
]));
}
diff --git a/crates/radicle-cli/src/terminal/patch/common.rs b/crates/radicle-cli/src/terminal/patch/common.rs
index cf518a48b..22d9a3141 100644
--- a/crates/radicle-cli/src/terminal/patch/common.rs
+++ b/crates/radicle-cli/src/terminal/patch/common.rs
@@ -89,7 +89,7 @@ pub fn branches(target: &Oid, repo: &git::raw::Repository) -> anyhow::Result<Vec
if !r.is_branch() {
continue;
}
- if let (Some(oid), Some(name)) = (&r.target(), &r.shorthand())
+ if let (Some(oid), name) = (&r.target(), &r.shorthand()?)
&& target == oid
{
branches.push(name.to_string());
diff --git a/crates/radicle-cob/src/backend/git/change.rs b/crates/radicle-cob/src/backend/git/change.rs
index 9b13f2f01..836e07573 100644
--- a/crates/radicle-cob/src/backend/git/change.rs
+++ b/crates/radicle-cob/src/backend/git/change.rs
@@ -252,7 +252,7 @@ fn load_contents(repo: &git2::Repository, tree: &git2::Tree) -> Result<Contents,
.filter_map(|entry| {
entry.kind().and_then(|kind| match kind {
git2::ObjectType::Blob => {
- let name = entry.name()?.parse::<i8>().ok()?;
+ let name = entry.name().ok()?.parse::<i8>().ok()?;
let blob = entry
.to_object(repo)
.and_then(|object| object.peel_to_blob())
diff --git a/crates/radicle-oid/src/lib.rs b/crates/radicle-oid/src/lib.rs
index 4b99b8fb3..ea1a181f0 100644
--- a/crates/radicle-oid/src/lib.rs
+++ b/crates/radicle-oid/src/lib.rs
@@ -455,7 +455,7 @@ mod git2 {
#[test]
fn zero() {
- assert!(Oid::ZERO_SHA1 == Other::zero());
+ assert!(Oid::ZERO_SHA1 == Other::ZERO_SHA1);
}
}
}
diff --git a/crates/radicle/src/git.rs b/crates/radicle/src/git.rs
index 6c5cceb6e..e593d9e13 100644
--- a/crates/radicle/src/git.rs
+++ b/crates/radicle/src/git.rs
@@ -146,8 +146,8 @@ pub fn version() -> Result<Version, VersionError> {
#[derive(thiserror::Error, Debug)]
pub enum RefError {
- #[error("ref name is not valid UTF-8")]
- InvalidName,
+ #[error("ref name is not valid: {source}")]
+ InvalidName { source: raw::Error },
#[error("unexpected unqualified ref: {0}")]
Unqualified(fmt::RefString),
#[error("invalid ref format: {0}")]
@@ -184,7 +184,9 @@ pub mod refs {
/// 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)?;
+ let name = r
+ .name()
+ .map_err(|source| RefError::InvalidName { source })?;
let refstr = RefStr::try_from_str(name)?;
let target = r.resolve()?.target().ok_or(RefError::NoTarget)?;
let qualified = Qualified::from_refstr(refstr)
@@ -717,19 +719,18 @@ pub fn set_upstream(
Ok(())
}
-pub fn init_default_branch(repo: &raw::Repository) -> Result<Option<String>, raw::Error> {
+pub fn init_default_branch(repo: &raw::Repository) -> Result<String, raw::Error> {
let config = repo.config().and_then(|mut c| c.snapshot())?;
let default_branch = config.get_str("init.defaultbranch")?;
let branch = repo.find_branch(default_branch, raw::BranchType::Local)?;
- Ok(branch.into_reference().shorthand().map(ToOwned::to_owned))
+ Ok(branch.into_reference().shorthand()?.to_owned())
}
pub fn head_refname(repo: &raw::Repository) -> Result<Option<String>, raw::Error> {
let head = repo.head()?;
- match head.shorthand() {
- Some("HEAD") => Ok(None),
- Some(refname) => Ok(Some(refname.to_owned())),
- None => Ok(None),
+ match head.shorthand()? {
+ "HEAD" => Ok(None),
+ refname => Ok(Some(refname.to_owned())),
}
}
diff --git a/crates/radicle/src/rad.rs b/crates/radicle/src/rad.rs
index 7af413b23..74d89fd8f 100644
--- a/crates/radicle/src/rad.rs
+++ b/crates/radicle/src/rad.rs
@@ -342,8 +342,6 @@ pub enum RemoteError {
Git(#[from] git::raw::Error),
#[error("invalid remote url: {0}")]
Url(#[from] transport::local::UrlError),
- #[error("invalid utf-8 string")]
- InvalidUtf8,
#[error("remote `{0}` not found")]
NotFound(String),
#[error("expected remote for {expected} but found {found}")]
@@ -359,7 +357,7 @@ pub fn remote(repo: &git::raw::Repository) -> Result<(git::raw::Remote<'_>, Repo
RemoteError::from(e)
}
})?;
- let url = remote.url().ok_or(RemoteError::InvalidUtf8)?;
+ let url = remote.url()?;
let url = git::Url::from_str(url)?;
Ok((remote, url.repo))
diff --git a/crates/radicle/src/storage/git.rs b/crates/radicle/src/storage/git.rs
index 56b185435..2a93d4345 100644
--- a/crates/radicle/src/storage/git.rs
+++ b/crates/radicle/src/storage/git.rs
@@ -59,7 +59,9 @@ impl TryFrom<git::raw::Reference<'_>> for Ref {
type Error = RefError;
fn try_from(r: git::raw::Reference) -> Result<Self, Self::Error> {
- let name = r.name().ok_or(RefError::InvalidName)?;
+ let name = r
+ .name()
+ .map_err(|source| RefError::InvalidName { source })?;
let (namespace, name) = match git::parse_ref_namespaced::<RemoteId>(name) {
Ok((namespace, refname)) => (Some(namespace), refname.to_ref_string()),
Err(RefError::MissingNamespace(refname)) => (None, refname),
@@ -282,7 +284,7 @@ impl Storage {
for r in repo.raw().references()? {
let r = r?;
- let name = r.name().ok_or(Error::InvalidRef)?;
+ let name = r.name().map_err(|_| Error::InvalidRef)?;
let oid = r.resolve()?.target().ok_or(Error::InvalidRef)?;
println!("{} {oid} {name}", rid.urn());
@@ -544,7 +546,7 @@ impl Repository {
pub fn inspect(&self) -> Result<(), Error> {
for r in self.backend.references()? {
let r = r?;
- let name = r.name().ok_or(Error::InvalidRef)?;
+ let name = r.name().map_err(|_| Error::InvalidRef)?;
let oid = r.resolve()?.target().ok_or(Error::InvalidRef)?;
println!("{oid} {name}");
@@ -592,7 +594,7 @@ impl Repository {
let iter = self.backend.references_glob(SIGREFS_GLOB.as_str())?.map(
|reference| -> Result<RemoteId, refs::Error> {
let r = reference?;
- let name = r.name().ok_or(refs::Error::InvalidRef)?;
+ let name = r.name().map_err(|_| refs::Error::InvalidRef)?;
let (id, _) = git::parse_ref_namespaced::<RemoteId>(name)?;
Ok(id)
@@ -610,7 +612,7 @@ impl Repository {
.references_glob(SIGREFS_GLOB.as_str())?
.map(|reference| -> Result<_, _> {
let r = reference?;
- let name = r.name().ok_or(refs::Error::InvalidRef)?;
+ let name = r.name().map_err(|_| refs::Error::InvalidRef)?;
let (id, _) = git::parse_ref_namespaced::<RemoteId>(name)?;
let remote = self.remote(&id)?;
@@ -797,7 +799,7 @@ impl ReadRepository for Repository {
for e in entries {
let e = e?;
- let name = e.name().ok_or(Error::InvalidRef)?;
+ let name = e.name().map_err(|_| Error::InvalidRef)?;
let (_, refname) = git::parse_ref::<RemoteId>(name)?;
let oid = e.resolve()?.target().ok_or(Error::InvalidRef)?;
let (_, category, subcategory, _) = refname.non_empty_components();
@@ -827,6 +829,7 @@ impl ReadRepository for Repository {
if let Some(name) = r
.name()
+ .ok()
.and_then(|n| git::fmt::RefStr::try_from_str(n).ok())
.and_then(git::fmt::Qualified::from_refstr)
{
@@ -949,7 +952,7 @@ impl WriteRepository for Repository {
let name = name.as_ref();
let target = target.as_ref();
match self.raw().find_reference(name.as_str()) {
- Ok(mut existing) => match existing.symbolic_target() {
+ Ok(mut existing) => match existing.symbolic_target()? {
Some(current) if current == target.as_str() => {
// Already points to the correct target, nothing to do.
}
diff --git a/crates/radicle/src/storage/git/cob.rs b/crates/radicle/src/storage/git/cob.rs
index 17545f509..2ca9eef7c 100644
--- a/crates/radicle/src/storage/git/cob.rs
+++ b/crates/radicle/src/storage/git/cob.rs
@@ -119,7 +119,7 @@ impl cob::object::Storage for Repository {
// TODO: Use glob here.
let mut references = self.backend.references()?.filter_map(|reference| {
let reference = reference.ok()?;
- match RefStr::try_from_str(reference.name()?) {
+ match RefStr::try_from_str(reference.name().ok()?) {
Ok(name) => {
let (ty, object_id) = cob::object::parse_refstr(&name)?;
if ty == *typename {
Exit code: 0
shell: 'export RUSTDOCFLAGS=''-D warnings'' cargo --version rustc --version cargo fmt --check cargo clippy --all-targets --workspace -- --deny warnings cargo build --all-targets --workspace cargo doc --workspace --no-deps --all-features cargo test --workspace --no-fail-fast '
Commands:
$ podman run --name 2c11eb8c-0d06-4026-98ef-fd130662f3a8 -v /opt/radcis/ci.rad.levitte.org/cci/state/2c11eb8c-0d06-4026-98ef-fd130662f3a8/s:/2c11eb8c-0d06-4026-98ef-fd130662f3a8/s:ro -v /opt/radcis/ci.rad.levitte.org/cci/state/2c11eb8c-0d06-4026-98ef-fd130662f3a8/w:/2c11eb8c-0d06-4026-98ef-fd130662f3a8/w -w /2c11eb8c-0d06-4026-98ef-fd130662f3a8/w -v /opt/radcis/ci.rad.levitte.org/.radicle:/${id}/.radicle:ro -e RAD_HOME=/${id}/.radicle rust:trixie bash /2c11eb8c-0d06-4026-98ef-fd130662f3a8/s/script.sh
+ export 'RUSTDOCFLAGS=-D warnings'
+ RUSTDOCFLAGS='-D warnings'
+ cargo --version
info: syncing channel updates for '1.95-x86_64-unknown-linux-gnu'
info: latest update on 2026-04-16, rust version 1.95.0 (59807616e 2026-04-14)
info: downloading component 'cargo'
info: downloading component 'clippy'
info: downloading component 'rust-docs'
info: downloading component 'rust-src'
info: downloading component 'rust-std'
info: downloading component 'rustc'
info: downloading component 'rustfmt'
info: installing component 'cargo'
info: installing component 'clippy'
info: installing component 'rust-docs'
info: installing component 'rust-src'
info: installing component 'rust-std'
info: installing component 'rustc'
info: installing component 'rustfmt'
cargo 1.95.0 (f2d3ce0bd 2026-03-21)
+ rustc --version
rustc 1.95.0 (59807616e 2026-04-14)
+ cargo fmt --check
+ cargo clippy --all-targets --workspace -- --deny warnings
Updating crates.io index
Downloading crates ...
Downloaded form_urlencoded v1.2.2
Downloaded find-msvc-tools v0.1.9
Downloaded errno v0.3.14
Downloaded filetime v0.2.27
Downloaded gix-quote v0.7.1
Downloaded colorchoice v1.0.5
Downloaded base256emoji v1.0.2
Downloaded gix-tempfile v23.0.0
Downloaded data-encoding-macro-internal v0.1.17
Downloaded fast-glob v0.3.3
Downloaded gix-odb v0.80.0
Downloaded gix-object v0.60.0
Downloaded email_address v0.2.9
Downloaded bytes v1.11.1
Downloaded match-lookup v0.1.2
Downloaded cypheraddr v0.4.1
Downloaded num v0.4.3
Downloaded iana-time-zone v0.1.65
Downloaded num-iter v0.1.45
Downloaded inout v0.1.4
Downloaded escargot v0.5.15
Downloaded lexopt v0.3.2
Downloaded opaque-debug v0.3.1
Downloaded jsonschema v0.30.0
Downloaded percent-encoding v2.3.2
Downloaded pastey v0.2.1
Downloaded num-traits v0.2.19
Downloaded ref-cast v1.0.25
Downloaded jiff-static v0.2.23
Downloaded poly1305 v0.8.0
Downloaded heck v0.5.0
Downloaded heapless v0.8.0
Downloaded memmap2 v0.9.10
Downloaded group v0.13.0
Downloaded normalize-line-endings v0.3.0
Downloaded rand_core v0.6.4
Downloaded potential_utf v0.1.4
Downloaded multibase v0.9.2
Downloaded num-cmp v0.1.0
Downloaded litemap v0.8.1
Downloaded sec1 v0.7.3
Downloaded num-rational v0.4.2
Downloaded qcheck v1.0.0
Downloaded getrandom v0.2.17
Downloaded icu_normalizer v2.1.1
Downloaded rustc_version v0.4.1
Downloaded sha1 v0.10.6
Downloaded serde_spanned v1.0.4
Downloaded rfc6979 v0.4.0
Downloaded pkg-config v0.3.32
Downloaded radicle-git-ext v0.13.0
Downloaded jobserver v0.1.34
Downloaded signature v1.6.4
Downloaded hashbrown v0.16.1
Downloaded test-log-macros v0.2.19
Downloaded hmac v0.12.1
Downloaded signature v2.2.0
Downloaded signal-hook-mio v0.2.5
Downloaded serde_fmt v1.1.0
Downloaded referencing v0.30.0
Downloaded stable_deref_trait v1.2.1
Downloaded serde_derive_internals v0.29.1
Downloaded socks5-client v0.4.3
Downloaded thread_local v1.1.9
Downloaded sval_fmt v2.17.0
Downloaded proc-macro2 v1.0.106
Downloaded tinystr v0.8.2
Downloaded typeid v1.0.3
Downloaded test-log v0.2.19
Downloaded sval_ref v2.17.0
Downloaded tinyvec_macros v0.1.1
Downloaded sharded-slab v0.1.7
Downloaded miniz_oxide v0.8.9
Downloaded timeago v0.4.2
Downloaded siphasher v1.0.2
Downloaded unicode-display-width v0.3.0
Downloaded toml_datetime v0.7.5+spec-1.1.0
Downloaded unit-prefix v0.5.2
Downloaded vsimd v0.8.0
Downloaded rsa v0.9.10
Downloaded yoke-derive v0.8.1
Downloaded radicle-surf v0.28.0
Downloaded utf8_iter v1.0.4
Downloaded value-bag-sval2 v1.12.0
Downloaded zerofrom-derive v0.1.6
Downloaded sval_buffer v2.17.0
Downloaded zeroize v1.8.2
Downloaded crossterm v0.29.0
Downloaded yoke v0.8.1
Downloaded version_check v0.9.5
Downloaded writeable v0.6.2
Downloaded typenum v1.19.0
Downloaded serde_json v1.0.149
Downloaded zmij v1.0.21
Downloaded value-bag v1.12.0
Downloaded tree-sitter-css v0.23.2
Downloaded portable-atomic v1.13.1
Downloaded tree-sitter-go v0.23.4
Downloaded unicode-normalization v0.1.25
Downloaded zerovec v0.11.5
Downloaded uuid v1.22.0
Downloaded tree-sitter v0.24.7
Downloaded tracing-core v0.1.36
Downloaded tree-sitter-c v0.23.4
Downloaded tar v0.4.45
Downloaded zerocopy v0.8.42
Downloaded ssh-key v0.6.7
Downloaded unicode-segmentation v1.12.0
Downloaded libm v0.2.16
Downloaded sval v2.17.0
Downloaded tree-sitter-rust v0.23.3
Downloaded tree-sitter-python v0.23.6
Downloaded zlib-rs v0.6.3
Downloaded vcpkg v0.2.15
Downloaded tree-sitter-bash v0.23.3
Downloaded unicode-width v0.2.2
Downloaded tracing v0.1.44
Downloaded syn v2.0.117
Downloaded tree-sitter-ruby v0.23.1
Downloaded tree-sitter-md v0.3.2
Downloaded regex-syntax v0.8.10
Downloaded regex-automata v0.4.14
Downloaded gimli v0.32.3
Downloaded syn v1.0.109
Downloaded rustix v1.1.4
Downloaded sysinfo v0.37.2
Downloaded itertools v0.14.0
Downloaded object v0.37.3
Downloaded tracing-subscriber v0.3.23
Downloaded bstr v1.12.1
Downloaded bloomy v1.2.0
Downloaded similar v3.1.1
Downloaded unicode-ident v1.0.24
Downloaded rand v0.9.2
Downloaded mio v1.1.1
Downloaded schemars v1.2.1
Downloaded proptest v1.10.0
Downloaded tempfile v3.27.0
Downloaded regex v1.12.3
Downloaded p521 v0.13.3
Downloaded jiff v0.2.23
Downloaded sha1-checked v0.10.0
Downloaded libc v0.2.183
Downloaded p384 v0.13.1
Downloaded zerovec-derive v0.11.2
Downloaded tree-sitter-typescript v0.23.2
Downloaded sha3 v0.10.8
Downloaded tokio v1.50.0
Downloaded libz-sys v1.1.25
Downloaded zerotrie v0.2.3
Downloaded yansi v1.0.1
Downloaded url v2.5.8
Downloaded thiserror v2.0.18
Downloaded ssh-agent-lib v0.6.0
Downloaded toml v0.9.12+spec-1.1.0
Downloaded sval_nested v2.17.0
Downloaded rand v0.8.5
Downloaded aho-corasick v1.1.4
Downloaded spin v0.9.8
Downloaded walkdir v2.5.0
Downloaded tree-sitter-html v0.23.2
Downloaded serde v1.0.228
Downloaded zerofrom v0.1.6
Downloaded snapbox v1.2.2
Downloaded value-bag-serde1 v1.12.0
Downloaded utf8parse v0.2.2
Downloaded universal-hash v0.5.1
Downloaded thiserror-impl v1.0.69
Downloaded prodash v31.0.0
Downloaded xattr v1.6.1
Downloaded wait-timeout v0.2.1
Downloaded uuid-simd v0.8.0
Downloaded tree-sitter-toml-ng v0.6.0
Downloaded tree-sitter-highlight v0.24.7
Downloaded toml_writer v1.0.7+spec-1.1.0
Downloaded tinyvec v1.11.0
Downloaded systemd-journal-logger v2.2.2
Downloaded synstructure v0.13.2
Downloaded sqlite3-sys v0.18.0
Downloaded sqlite v0.37.0
Downloaded smallvec v1.15.1
Downloaded git2 v0.21.0
Downloaded tree-sitter-language v0.1.7
Downloaded tree-sitter-json v0.24.8
Downloaded structured-logger v1.0.5
Downloaded spki v0.7.3
Downloaded serde_core v1.0.228
Downloaded signal-hook v0.3.18
Downloaded unarray v0.1.4
Downloaded sval_serde v2.17.0
Downloaded signals_receipts v0.2.5
Downloaded sem_safe v0.2.1
Downloaded ryu v1.0.23
Downloaded pretty_assertions v1.4.1
Downloaded thiserror-impl v2.0.18
Downloaded thiserror v1.0.69
Downloaded libgit2-sys v0.18.4+1.9.3
Downloaded streaming-iterator v0.1.9
Downloaded num-bigint v0.4.6
Downloaded gix-transport v0.57.0
Downloaded gix-pack v0.70.0
Downloaded curve25519-dalek v4.1.3
Downloaded aes v0.8.4
Downloaded rustc-demangle v0.1.27
Downloaded gix-diff v0.63.0
Downloaded ssh-encoding v0.2.0
Downloaded socket2 v0.5.10
Downloaded clap_builder v4.6.0
Downloaded tracing-log v0.2.0
Downloaded subtle v2.6.1
Downloaded strsim v0.11.1
Downloaded signal-hook-registry v1.4.8
Downloaded serde-untagged v0.1.9
Downloaded pem-rfc7468 v0.7.0
Downloaded snapbox-macros v1.1.0
Downloaded simd-adler32 v0.3.8
Downloaded semver v1.0.27
Downloaded rand_core v0.9.5
Downloaded sval_dynamic v2.17.0
Downloaded shlex v1.3.0
Downloaded sval_json v2.17.0
Downloaded siphasher v0.3.11
Downloaded flate2 v1.1.9
Downloaded icu_normalizer_data v2.1.1
Downloaded ssh-cipher v0.2.0
Downloaded serde_derive v1.0.228
Downloaded same-file v1.0.6
Downloaded rand_chacha v0.9.0
Downloaded rand_chacha v0.3.1
Downloaded phf_shared v0.11.3
Downloaded idna v1.1.0
Downloaded quote v1.0.45
Downloaded primeorder v0.13.6
Downloaded pkcs1 v0.7.5
Downloaded emojis v0.6.4
Downloaded scrypt v0.11.0
Downloaded der v0.7.10
Downloaded shell-words v1.1.1
Downloaded scopeguard v1.2.0
Downloaded secrecy v0.10.3
Downloaded memchr v2.8.0
Downloaded icu_locale_core v2.1.1
Downloaded fluent-uri v0.3.2
Downloaded sha2 v0.10.9
Downloaded schemars_derive v1.2.1
Downloaded salsa20 v0.10.2
Downloaded rustversion v1.0.22
Downloaded quick-error v1.2.3
Downloaded p256 v0.13.2
Downloaded num-integer v0.1.46
Downloaded icu_provider v2.1.1
Downloaded derive_more v2.1.1
Downloaded radicle-std-ext v0.2.0
Downloaded proc-macro-error-attr3 v3.0.2
Downloaded linux-raw-sys v0.12.1
Downloaded parking_lot_core v0.9.12
Downloaded sqlite3-src v0.7.0
Downloaded gix-date v0.15.3
Downloaded ppv-lite86 v0.2.21
Downloaded pin-project-lite v0.2.17
Downloaded num-complex v0.4.6
Downloaded gix-utils v0.3.2
Downloaded ref-cast-impl v1.0.25
Downloaded pbkdf2 v0.12.2
Downloaded phf v0.11.3
Downloaded gix-refspec v0.41.0
Downloaded gix-actor v0.41.0
Downloaded rand_xorshift v0.4.0
Downloaded qcheck-macros v1.0.0
Downloaded proc-macro-error3 v3.0.2
Downloaded once_cell v1.21.4
Downloaded gix-validate v0.11.1
Downloaded cc v1.2.57
Downloaded base64 v0.21.7
Downloaded human-panic v2.0.6
Downloaded crossbeam-channel v0.5.15
Downloaded elliptic-curve v0.13.8
Downloaded colored v2.2.0
Downloaded ascii v1.1.0
Downloaded polyval v0.6.2
Downloaded pkcs8 v0.10.2
Downloaded itoa v1.0.17
Downloaded generic-array v0.14.7
Downloaded nu-ansi-term v0.50.3
Downloaded nonempty v0.9.0
Downloaded gix-hashtable v0.15.0
Downloaded autocfg v1.5.0
Downloaded anyhow v1.0.102
Downloaded anstream v0.6.21
Downloaded rusty-fork v0.3.1
Downloaded num-bigint-dig v0.8.6
Downloaded maybe-async v0.2.10
Downloaded matchers v0.2.0
Downloaded crc32fast v1.5.0
Downloaded bytecount v0.6.9
Downloaded anstyle v1.0.14
Downloaded noise-framework v0.4.1
Downloaded either v1.15.0
Downloaded outref v0.5.2
Downloaded nonempty v0.12.0
Downloaded base64ct v1.8.3
Downloaded is_terminal_polyfill v1.70.2
Downloaded gix-hash v0.25.0
Downloaded bytesize v2.3.1
Downloaded gix-protocol v0.61.0
Downloaded gix-prompt v0.15.0
Downloaded gix-error v0.2.3
Downloaded git-ref-format v0.7.0
Downloaded fraction v0.15.3
Downloaded keccak v0.1.6
Downloaded hash32 v0.3.1
Downloaded gix-path v0.12.0
Downloaded gix-lock v23.0.0
Downloaded gix-glob v0.26.0
Downloaded gix-features v0.48.0
Downloaded gix-config-value v0.18.0
Downloaded gix-command v0.9.0
Downloaded gix-revision v0.45.0
Downloaded gix-ref v0.63.0
Downloaded fnv v1.0.7
Downloaded chacha20poly1305 v0.10.1
Downloaded anstyle-query v1.1.5
Downloaded gix-credentials v0.38.0
Downloaded parking_lot v0.12.5
Downloaded lock_api v0.4.14
Downloaded idna_adapter v1.2.1
Downloaded gix-url v0.36.0
Downloaded gix-traverse v0.57.0
Downloaded git-ref-format-macro v0.7.0
Downloaded git-ref-format-core v0.6.0
Downloaded getrandom v0.4.2
Downloaded getrandom v0.3.4
Downloaded const-oid v0.9.6
Downloaded humantime v2.3.0
Downloaded gix-sec v0.14.0
Downloaded gix-shallow v0.12.0
Downloaded gix-revwalk v0.31.0
Downloaded gix-negotiate v0.31.0
Downloaded ed25519 v1.5.3
Downloaded anstyle-parse v0.2.7
Downloaded amplify_num v0.5.3
Downloaded ec25519 v0.1.0
Downloaded base16ct v0.2.0
Downloaded log v0.4.29
Downloaded litrs v1.0.0
Downloaded icu_collections v2.1.1
Downloaded gix-trace v0.1.19
Downloaded gix-packetline v0.21.3
Downloaded gix-fs v0.21.1
Downloaded displaydoc v0.2.5
Downloaded diff v0.1.13
Downloaded ctr v0.9.2
Downloaded convert_case v0.10.0
Downloaded clap_lex v1.1.0
Downloaded cipher v0.4.4
Downloaded data-encoding v2.10.0
Downloaded curve25519-dalek-derive v0.1.1
Downloaded chacha20 v0.9.1
Downloaded amplify_derive v4.0.1
Downloaded lazy_static v1.5.0
Downloaded gix-commitgraph v0.37.0
Downloaded ghash v0.5.1
Downloaded equivalent v1.0.2
Downloaded borrow-or-share v0.2.4
Downloaded icu_properties v2.1.2
Downloaded gix-chunk v0.7.1
Downloaded ecdsa v0.16.9
Downloaded blowfish v0.9.1
Downloaded bit-set v0.8.0
Downloaded dyn-clone v1.0.20
Downloaded dunce v1.0.5
Downloaded crypto-common v0.1.7
Downloaded block-buffer v0.10.4
Downloaded indexmap v2.13.0
Downloaded fastrand v2.3.0
Downloaded env_filter v1.0.0
Downloaded aead v0.5.2
Downloaded inquire v0.9.4
Downloaded icu_properties_data v2.1.2
Downloaded ed25519 v2.2.3
Downloaded ct-codecs v1.1.6
Downloaded crypto-bigint v0.5.5
Downloaded bit-vec v0.8.0
Downloaded ff v0.13.1
Downloaded data-encoding-macro v0.1.19
Downloaded crossbeam-utils v0.8.21
Downloaded amplify_syn v2.0.1
Downloaded fancy-regex v0.14.0
Downloaded document-features v0.2.12
Downloaded cyphergraphy v0.3.1
Downloaded clap_derive v4.6.0
Downloaded byteorder v1.5.0
Downloaded block-padding v0.3.3
Downloaded bitflags v2.11.0
Downloaded base32 v0.4.0
Downloaded cpufeatures v0.2.17
Downloaded cbc v0.1.2
Downloaded base-x v0.2.11
Downloaded aes-gcm v0.10.3
Downloaded indicatif v0.18.4
Downloaded faster-hex v0.10.0
Downloaded ed25519-dalek v2.2.0
Downloaded derive_more-impl v2.1.1
Downloaded cyphernet v0.5.4
Downloaded clap_complete v4.6.0
Downloaded clap v4.6.0
Downloaded chrono v0.4.44
Downloaded bcrypt-pbkdf v0.10.0
Downloaded base64 v0.22.1
Downloaded backtrace v0.3.76
Downloaded arc-swap v1.9.1
Downloaded console v0.16.3
Downloaded ahash v0.8.12
Downloaded addr2line v0.25.1
Downloaded erased-serde v0.4.10
Downloaded env_logger v0.11.9
Downloaded digest v0.10.7
Downloaded const-str v0.4.3
Downloaded cfg-if v1.0.4
Downloaded anstyle-parse v1.0.0
Downloaded anstream v1.0.0
Downloaded amplify v4.9.0
Downloaded adler2 v2.0.1
Compiling libc v0.2.183
Compiling proc-macro2 v1.0.106
Compiling unicode-ident v1.0.24
Compiling quote v1.0.45
Checking cfg-if v1.0.4
Checking zeroize v1.8.2
Compiling version_check v0.9.5
Compiling typenum v1.19.0
Compiling generic-array v0.14.7
Checking getrandom v0.2.17
Compiling syn v2.0.117
Checking rand_core v0.6.4
Checking memchr v2.8.0
Compiling jobserver v0.1.34
Compiling find-msvc-tools v0.1.9
Compiling shlex v1.3.0
Compiling cc v1.2.57
Checking crypto-common v0.1.7
Checking subtle v2.6.1
Compiling serde_core v1.0.228
Checking regex-syntax v0.8.10
Checking aho-corasick v1.1.4
Checking const-oid v0.9.6
Checking smallvec v1.15.1
Checking block-buffer v0.10.4
Checking digest v0.10.7
Checking cpufeatures v0.2.17
Compiling thiserror v2.0.18
Checking regex-automata v0.4.14
Checking fastrand v2.3.0
Compiling parking_lot_core v0.9.12
Checking scopeguard v1.2.0
Checking lock_api v0.4.14
Checking stable_deref_trait v1.2.1
Checking parking_lot v0.12.5
Checking bitflags v2.11.0
Compiling typeid v1.0.3
Compiling erased-serde v0.4.10
Compiling crc32fast v1.5.0
Checking gix-trace v0.1.19
Checking tinyvec_macros v0.1.1
Checking tinyvec v1.11.0
Compiling serde v1.0.228
Checking unicode-normalization v0.1.25
Checking byteorder v1.5.0
Checking itoa v1.0.17
Checking gix-utils v0.3.2
Checking serde_fmt v1.1.0
Checking hashbrown v0.16.1
Checking value-bag-serde1 v1.12.0
Checking value-bag v1.12.0
Compiling thiserror-impl v2.0.18
Checking bstr v1.12.1
Compiling serde_derive v1.0.228
Checking log v0.4.29
Checking gix-validate v0.11.1
Checking same-file v1.0.6
Checking walkdir v2.5.0
Checking prodash v31.0.0
Checking zlib-rs v0.6.3
Compiling rustix v1.1.4
Checking gix-path v0.12.0
Compiling heapless v0.8.0
Compiling pkg-config v0.3.32
Checking hash32 v0.3.1
Checking gix-features v0.48.0
Compiling autocfg v1.5.0
Compiling libm v0.2.16
Checking linux-raw-sys v0.12.1
Compiling num-traits v0.2.19
Compiling getrandom v0.4.2
Checking faster-hex v0.10.0
Checking block-padding v0.3.3
Compiling zerocopy v0.8.42
Checking inout v0.1.4
Checking sha1 v0.10.6
Checking sha2 v0.10.9
Checking sha1-checked v0.10.0
Checking cipher v0.4.4
Checking once_cell v1.21.4
Checking gix-hash v0.25.0
Compiling zmij v1.0.21
Checking der v0.7.10
Checking equivalent v1.0.2
Compiling serde_json v1.0.149
Checking indexmap v2.13.0
Compiling syn v1.0.109
Compiling vcpkg v0.2.15
Compiling ref-cast v1.0.25
Compiling thiserror v1.0.69
Checking tempfile v3.27.0
Compiling thiserror-impl v1.0.69
Compiling libz-sys v1.1.25
Compiling ref-cast-impl v1.0.25
Checking spin v0.9.8
Checking lazy_static v1.5.0
Checking num-integer v0.1.46
Checking hmac v0.12.1
Checking universal-hash v0.5.1
Checking dyn-clone v1.0.20
Compiling tree-sitter-language v0.1.7
Checking opaque-debug v0.3.1
Checking spki v0.7.3
Compiling libgit2-sys v0.18.4+1.9.3
Checking signature v2.2.0
Checking ff v0.13.1
Checking base16ct v0.2.0
Checking ppv-lite86 v0.2.21
Checking sec1 v0.7.3
Checking group v0.13.0
Checking rand_chacha v0.3.1
Compiling serde_derive_internals v0.29.1
Checking crypto-bigint v0.5.5
Compiling schemars_derive v1.2.1
Checking elliptic-curve v0.13.8
Compiling amplify_syn v2.0.1
Checking rand v0.8.5
Checking num-iter v0.1.45
Checking aead v0.5.2
Compiling semver v1.0.27
Checking signature v1.6.4
Compiling amplify_derive v4.0.1
Checking ed25519 v1.5.3
Checking schemars v1.2.1
Compiling rustc_version v0.4.1
Checking poly1305 v0.8.0
Checking rfc6979 v0.4.0
Checking chacha20 v0.9.1
Checking ct-codecs v1.1.6
Checking ascii v1.1.0
Checking amplify_num v0.5.3
Checking ec25519 v0.1.0
Checking ecdsa v0.16.9
Compiling curve25519-dalek v4.1.3
Checking git-ref-format-core v0.6.0
Checking primeorder v0.13.6
Checking amplify v4.9.0
Checking polyval v0.6.2
Compiling num-bigint-dig v0.8.6
Checking base64ct v1.8.3
Checking ghash v0.5.1
Checking cyphergraphy v0.3.1
Checking pem-rfc7468 v0.7.0
Checking pkcs8 v0.10.2
Checking pbkdf2 v0.12.2
Checking ctr v0.9.2
Checking aes v0.8.4
Compiling sqlite3-src v0.7.0
Checking gix-error v0.2.3
Compiling curve25519-dalek-derive v0.1.1
Checking keccak v0.1.6
Checking aes-gcm v0.10.3
Checking sha3 v0.10.8
Checking pkcs1 v0.7.5
Checking ssh-encoding v0.2.0
Checking ed25519 v2.2.3
Checking blowfish v0.9.1
Checking cbc v0.1.2
Checking base32 v0.4.0
Compiling data-encoding v2.10.0
Compiling crossbeam-utils v0.8.21
Checking rsa v0.9.10
Compiling data-encoding-macro-internal v0.1.17
Checking cypheraddr v0.4.1
Checking ssh-cipher v0.2.0
Checking bcrypt-pbkdf v0.10.0
Checking ed25519-dalek v2.2.0
Checking p256 v0.13.2
Checking p384 v0.13.1
Checking p521 v0.13.3
Checking chacha20poly1305 v0.10.1
Checking qcheck v1.0.0
Compiling match-lookup v0.1.2
Checking const-str v0.4.3
Checking base256emoji v1.0.2
Checking data-encoding-macro v0.1.19
Checking ssh-key v0.6.7
Checking noise-framework v0.4.1
Checking socks5-client v0.4.3
Checking secrecy v0.10.3
Checking base-x v0.2.11
Checking ssh-agent-lib v0.6.0
Checking multibase v0.9.2
Checking crossbeam-channel v0.5.15
Checking cyphernet v0.5.4
Checking anstyle-query v1.1.5
Compiling synstructure v0.13.2
Checking percent-encoding v2.3.2
Checking errno v0.3.14
Checking utf8parse v0.2.2
Checking jiff v0.2.23
Compiling zerofrom-derive v0.1.6
Checking nonempty v0.9.0
Checking siphasher v1.0.2
Checking zerofrom v0.1.6
Compiling yoke-derive v0.8.1
Checking radicle-localtime v0.1.0 (/2c11eb8c-0d06-4026-98ef-fd130662f3a8/w/crates/radicle-localtime)
Checking radicle-git-metadata v0.2.0 (/2c11eb8c-0d06-4026-98ef-fd130662f3a8/w/crates/radicle-git-metadata)
Checking gix-date v0.15.3
Checking radicle-dag v0.10.0 (/2c11eb8c-0d06-4026-98ef-fd130662f3a8/w/crates/radicle-dag)
Checking colorchoice v1.0.5
Checking is_terminal_polyfill v1.70.2
Checking anstyle v1.0.14
Checking gix-actor v0.41.0
Checking yoke v0.8.1
Checking radicle-git-ref-format v0.1.0 (/2c11eb8c-0d06-4026-98ef-fd130662f3a8/w/crates/radicle-git-ref-format)
Checking gix-hashtable v0.15.0
Compiling radicle v0.24.0 (/2c11eb8c-0d06-4026-98ef-fd130662f3a8/w/crates/radicle)
Compiling unicode-segmentation v1.12.0
Compiling signal-hook v0.3.18
Checking base64 v0.21.7
Compiling convert_case v0.10.0
Checking gix-object v0.60.0
Checking signal-hook-registry v1.4.8
Compiling zerovec-derive v0.11.2
Checking serde-untagged v0.1.9
Checking bytesize v2.3.1
Checking memmap2 v0.9.10
Checking dunce v1.0.5
Checking fast-glob v0.3.3
Checking nonempty v0.12.0
Checking zerovec v0.11.5
Compiling derive_more-impl v2.1.1
Checking gix-chunk v0.7.1
Checking mio v1.1.1
Checking regex v1.12.3
Compiling displaydoc v0.2.5
Checking sem_safe v0.2.1
Compiling portable-atomic v1.13.1
Compiling litrs v1.0.0
Checking unicode-width v0.2.2
Checking signals_receipts v0.2.5
Checking signal-hook-mio v0.2.5
Compiling document-features v0.2.12
Checking derive_more v2.1.1
Checking gix-commitgraph v0.37.0
Checking anstyle-parse v1.0.0
Checking crossterm v0.29.0
Checking anstream v1.0.0
Checking gix-revwalk v0.31.0
Checking console v0.16.3
Checking gix-fs v0.21.1
Checking unit-prefix v0.5.2
Checking indicatif v0.18.4
Checking gix-tempfile v23.0.0
Checking inquire v0.9.4
Checking unicode-display-width v0.3.0
Checking radicle-signals v0.11.0 (/2c11eb8c-0d06-4026-98ef-fd130662f3a8/w/crates/radicle-signals)
Checking tinystr v0.8.2
Checking gix-quote v0.7.1
Checking litemap v0.8.1
Checking writeable v0.6.2
Checking zerotrie v0.2.3
Checking icu_locale_core v2.1.1
Checking potential_utf v0.1.4
Compiling icu_normalizer_data v2.1.1
Checking iana-time-zone v0.1.65
Checking either v1.15.0
Compiling icu_properties_data v2.1.2
Checking shell-words v1.1.1
Checking icu_provider v2.1.1
Checking gix-command v0.9.0
Checking chrono v0.4.44
Checking icu_collections v2.1.1
Checking colored v2.2.0
Compiling rustversion v1.0.22
Compiling object v0.37.3
Checking gix-lock v23.0.0
Checking gix-url v0.36.0
Checking gix-config-value v0.18.0
Checking gix-sec v0.14.0
Checking adler2 v2.0.1
Checking gimli v0.32.3
Checking miniz_oxide v0.8.9
Checking gix-prompt v0.15.0
Checking icu_properties v2.1.2
Checking icu_normalizer v2.1.1
Checking addr2line v0.25.1
Checking gix-revision v0.45.0
Checking gix-traverse v0.57.0
Checking gix-diff v0.63.0
Checking anstyle-parse v0.2.7
Checking gix-packetline v0.21.3
Checking gix-glob v0.26.0
Compiling tree-sitter v0.24.7
Compiling anyhow v1.0.102
Checking rustc-demangle v0.1.27
Checking backtrace v0.3.76
Checking gix-refspec v0.41.0
Checking gix-transport v0.57.0
Checking anstream v0.6.21
Checking gix-pack v0.70.0
Checking arc-swap v1.9.1
Checking idna_adapter v1.2.1
Checking gix-credentials v0.38.0
Checking gix-ref v0.63.0
Checking gix-shallow v0.12.0
Checking gix-negotiate v0.31.0
Compiling maybe-async v0.2.10
Compiling proc-macro-error-attr3 v3.0.2
Checking utf8_iter v1.0.4
Compiling getrandom v0.3.4
Compiling simd-adler32 v0.3.8
Checking gix-protocol v0.61.0
Checking idna v1.1.0
Compiling proc-macro-error3 v3.0.2
Checking gix-odb v0.80.0
Compiling xattr v1.6.1
Compiling filetime v0.2.27
Checking uuid v1.22.0
Checking bytes v1.11.1
Compiling tar v0.4.45
Compiling flate2 v1.1.9
Compiling git-ref-format-macro v0.7.0
Checking snapbox-macros v1.1.0
Checking salsa20 v0.10.2
Checking strsim v0.11.1
Checking normalize-line-endings v0.3.0
Checking similar v3.1.1
Checking streaming-iterator v0.1.9
Checking siphasher v0.3.11
Compiling heck v0.5.0
Checking clap_lex v1.1.0
Compiling clap_derive v4.6.0
Checking sqlite3-sys v0.18.0
Checking clap_builder v4.6.0
Checking sqlite v0.37.0
Checking radicle-crypto v0.17.0 (/2c11eb8c-0d06-4026-98ef-fd130662f3a8/w/crates/radicle-crypto)
Checking snapbox v1.2.2
Checking bloomy v1.2.0
Compiling radicle-surf v0.28.0
Checking scrypt v0.11.0
Checking git-ref-format v0.7.0
Checking form_urlencoded v1.2.2
Checking systemd-journal-logger v2.2.2
Checking toml_datetime v0.7.5+spec-1.1.0
Checking serde_spanned v1.0.4
Compiling tree-sitter-c v0.23.4
Compiling tree-sitter-bash v0.23.3
Compiling tree-sitter-css v0.23.2
Compiling tree-sitter-typescript v0.23.2
Compiling tree-sitter-json v0.24.8
Compiling tree-sitter-ruby v0.23.1
Compiling tree-sitter-go v0.23.4
Compiling tree-sitter-toml-ng v0.6.0
Compiling tree-sitter-html v0.23.2
Compiling tree-sitter-rust v0.23.3
Compiling tree-sitter-md v0.3.2
Compiling tree-sitter-python v0.23.6
Checking radicle-std-ext v0.2.0
Checking pin-project-lite v0.2.17
Checking toml_writer v1.0.7+spec-1.1.0
Checking tokio v1.50.0
Checking toml v0.9.12+spec-1.1.0
Checking url v2.5.8
Checking clap v4.6.0
Checking sysinfo v0.37.2
Compiling radicle-cli v0.21.0 (/2c11eb8c-0d06-4026-98ef-fd130662f3a8/w/crates/radicle-cli)
Checking yansi v1.0.1
Compiling radicle-node v0.20.0 (/2c11eb8c-0d06-4026-98ef-fd130662f3a8/w/crates/radicle-node)
Checking diff v0.1.13
Checking pretty_assertions v1.4.1
Checking human-panic v2.0.6
Checking clap_complete v4.6.0
Checking structured-logger v1.0.5
Checking radicle-systemd v0.13.0 (/2c11eb8c-0d06-4026-98ef-fd130662f3a8/w/crates/radicle-systemd)
Checking tree-sitter-highlight v0.24.7
Checking itertools v0.14.0
Compiling qcheck-macros v1.0.0
Checking socket2 v0.5.10
Checking humantime v2.3.0
Compiling escargot v0.5.15
Checking timeago v0.4.2
Checking lexopt v0.3.2
Checking bit-vec v0.8.0
Checking bit-set v0.8.0
Checking rand_core v0.9.5
Checking num-bigint v0.4.6
Compiling ahash v0.8.12
Checking num-complex v0.4.6
Checking env_filter v1.0.0
Checking borrow-or-share v0.2.4
Checking fluent-uri v0.3.2
Checking env_logger v0.11.9
Checking num-rational v0.4.2
Checking phf_shared v0.11.3
Compiling test-log-macros v0.2.19
Checking wait-timeout v0.2.1
Checking outref v0.5.2
Checking num v0.4.3
Checking vsimd v0.8.0
Compiling radicle-remote-helper v0.17.0 (/2c11eb8c-0d06-4026-98ef-fd130662f3a8/w/crates/radicle-remote-helper)
Checking quick-error v1.2.3
Checking fnv v1.0.7
Checking rusty-fork v0.3.1
Checking test-log v0.2.19
Checking uuid-simd v0.8.0
Checking fraction v0.15.3
Checking phf v0.11.3
Checking referencing v0.30.0
Checking rand_xorshift v0.4.0
Checking rand_chacha v0.9.0
Checking rand v0.9.2
Checking fancy-regex v0.14.0
Checking email_address v0.2.9
Checking num-cmp v0.1.0
Checking unarray v0.1.4
Checking base64 v0.22.1
Checking bytecount v0.6.9
Checking proptest v1.10.0
Checking emojis v0.6.4
Checking jsonschema v0.30.0
Compiling pastey v0.2.1
Checking radicle-windows v0.1.0 (/2c11eb8c-0d06-4026-98ef-fd130662f3a8/w/crates/radicle-windows)
Checking git2 v0.21.0
Checking radicle-oid v0.2.0 (/2c11eb8c-0d06-4026-98ef-fd130662f3a8/w/crates/radicle-oid)
Checking radicle-term v0.18.0 (/2c11eb8c-0d06-4026-98ef-fd130662f3a8/w/crates/radicle-term)
Checking radicle-git-ext v0.13.0
error: enum `Oid` has a public `len` method, but no `is_empty` method
--> crates/radicle-oid/src/lib.rs:177:5
|
177 | pub fn len(&self) -> usize {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.95.0/index.html#len_without_is_empty
= note: `-D clippy::len-without-is-empty` implied by `-D warnings`
= help: to override `-D warnings` add `#[allow(clippy::len_without_is_empty)]`
error: could not compile `radicle-oid` (lib) due to 1 previous error
warning: build failed, waiting for other jobs to finish...
Exit code: 101
{
"response": "finished",
"result": "failure"
}