From 4194c59effffbbce674d2e6df99c568e7028a4d5 Mon Sep 17 00:00:00 2001 From: "chatgpt-codex-connector[bot]" <199175422+chatgpt-codex-connector[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 05:37:38 +0100 Subject: [PATCH] feat: add traits to represent the lowest common denominator for reference query and mutation. This is in preparation for supporting reftables Co-authored-by: Sebastian Thiel --- Cargo.lock | 1 + gix-ref/Cargo.toml | 1 + gix-ref/src/lib.rs | 2 + gix-ref/src/traits.rs | 256 +++++++++++++++++++++++++++++++++++ gix-ref/tests/refs/main.rs | 1 + gix-ref/tests/refs/traits.rs | 142 +++++++++++++++++++ 6 files changed, 403 insertions(+) create mode 100644 gix-ref/src/traits.rs create mode 100644 gix-ref/tests/refs/traits.rs diff --git a/Cargo.lock b/Cargo.lock index 3b79097f977..3d7da475fed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2219,6 +2219,7 @@ dependencies = [ "document-features", "gix-actor", "gix-date", + "gix-error", "gix-features", "gix-fs", "gix-hash", diff --git a/gix-ref/Cargo.toml b/gix-ref/Cargo.toml index a554e282dcd..0f236dd0ace 100644 --- a/gix-ref/Cargo.toml +++ b/gix-ref/Cargo.toml @@ -29,6 +29,7 @@ gix-object = { version = "^0.57.0", path = "../gix-object" } gix-utils = { version = "^0.3.1", path = "../gix-utils" } gix-validate = { version = "^0.11.0", path = "../gix-validate" } gix-actor = { version = "^0.40.0", path = "../gix-actor" } +gix-error = { version = "^0.2.0", path = "../gix-error" } gix-lock = { version = "^21.0.0", path = "../gix-lock" } gix-tempfile = { version = "^21.0.0", default-features = false, path = "../gix-tempfile" } diff --git a/gix-ref/src/lib.rs b/gix-ref/src/lib.rs index 8e8e808bc1c..41cf1a61c97 100644 --- a/gix-ref/src/lib.rs +++ b/gix-ref/src/lib.rs @@ -41,8 +41,10 @@ pub mod transaction; mod parse; mod raw; +mod traits; pub use raw::Reference; +pub use traits::{StoreMutate, StoreRead, StoreReadExt}; mod target; diff --git a/gix-ref/src/traits.rs b/gix-ref/src/traits.rs new file mode 100644 index 00000000000..494f2bfdadc --- /dev/null +++ b/gix-ref/src/traits.rs @@ -0,0 +1,256 @@ +use std::{rc::Rc, sync::Arc}; + +use gix_error::{ErrorExt, Exn}; + +/// Read capabilities of a reference store. +pub trait StoreRead { + /// Try to find a reference by `partial` name. + /// + /// Returns `Ok(None)` if no matching reference exists. + fn try_find(&self, partial: &crate::PartialNameRef) -> Result, Exn>; + + /// Return a platform to iterate references as loose-and-packed overlay. + fn iter(&self) -> Result, Exn>; + + /// Return `true` if a reflog exists for `name`. + fn reflog_exists(&self, name: &crate::FullNameRef) -> Result; + + /// Return a forward reflog iterator for `name`, or `None` if there is no reflog. + fn reflog_iter<'a>( + &self, + name: &crate::FullNameRef, + buf: &'a mut Vec, + ) -> Result>, Exn>; + + /// Return a reverse reflog iterator for `name`, or `None` if there is no reflog. + fn reflog_iter_rev<'a>( + &self, + name: &crate::FullNameRef, + buf: &'a mut [u8], + ) -> Result>, Exn>; +} + +/// Convenience methods built on top of [`StoreRead`]. +pub trait StoreReadExt: StoreRead { + /// Like [`StoreRead::try_find()`], but a missing reference is treated as error. + fn find(&self, partial: &crate::PartialNameRef) -> Result { + self.try_find(partial)?.ok_or_else(|| { + crate::file::find::existing::Error::NotFound { + name: partial.to_partial_path().to_owned(), + } + .raise_erased() + }) + } +} + +impl StoreReadExt for T {} + +/// Mutation capabilities of a reference store. +pub trait StoreMutate { + /// Return a transaction platform for mutating references. + fn transaction(&self) -> Result, Exn>; +} + +impl StoreRead for crate::file::Store { + fn try_find(&self, partial: &crate::PartialNameRef) -> Result, Exn> { + crate::file::Store::try_find(self, partial).map_err(|err| err.raise_erased()) + } + + fn iter(&self) -> Result, Exn> { + crate::file::Store::iter(self).map_err(|err| err.raise_erased()) + } + + fn reflog_exists(&self, name: &crate::FullNameRef) -> Result { + Ok(crate::file::Store::reflog_exists(self, name).expect("a FullNameRef is always valid")) + } + + fn reflog_iter<'a>( + &self, + name: &crate::FullNameRef, + buf: &'a mut Vec, + ) -> Result>, Exn> { + crate::file::Store::reflog_iter(self, name, buf).map_err(|err| err.raise_erased()) + } + + fn reflog_iter_rev<'a>( + &self, + name: &crate::FullNameRef, + buf: &'a mut [u8], + ) -> Result>, Exn> { + crate::file::Store::reflog_iter_rev(self, name, buf).map_err(|err| err.raise_erased()) + } +} + +impl StoreMutate for crate::file::Store { + fn transaction(&self) -> Result, Exn> { + Ok(crate::file::Store::transaction(self)) + } +} + +impl StoreRead for &T +where + T: StoreRead + ?Sized, +{ + fn try_find(&self, partial: &crate::PartialNameRef) -> Result, Exn> { + (*self).try_find(partial) + } + + fn iter(&self) -> Result, Exn> { + (*self).iter() + } + + fn reflog_exists(&self, name: &crate::FullNameRef) -> Result { + (*self).reflog_exists(name) + } + + fn reflog_iter<'a>( + &self, + name: &crate::FullNameRef, + buf: &'a mut Vec, + ) -> Result>, Exn> { + (*self).reflog_iter(name, buf) + } + + fn reflog_iter_rev<'a>( + &self, + name: &crate::FullNameRef, + buf: &'a mut [u8], + ) -> Result>, Exn> { + (*self).reflog_iter_rev(name, buf) + } +} + +impl StoreMutate for &T +where + T: StoreMutate + ?Sized, +{ + fn transaction(&self) -> Result, Exn> { + (*self).transaction() + } +} + +impl StoreRead for Rc +where + T: StoreRead + ?Sized, +{ + fn try_find(&self, partial: &crate::PartialNameRef) -> Result, Exn> { + self.as_ref().try_find(partial) + } + + fn iter(&self) -> Result, Exn> { + self.as_ref().iter() + } + + fn reflog_exists(&self, name: &crate::FullNameRef) -> Result { + self.as_ref().reflog_exists(name) + } + + fn reflog_iter<'a>( + &self, + name: &crate::FullNameRef, + buf: &'a mut Vec, + ) -> Result>, Exn> { + self.as_ref().reflog_iter(name, buf) + } + + fn reflog_iter_rev<'a>( + &self, + name: &crate::FullNameRef, + buf: &'a mut [u8], + ) -> Result>, Exn> { + self.as_ref().reflog_iter_rev(name, buf) + } +} + +impl StoreMutate for Rc +where + T: StoreMutate + ?Sized, +{ + fn transaction(&self) -> Result, Exn> { + self.as_ref().transaction() + } +} + +impl StoreRead for Arc +where + T: StoreRead + ?Sized, +{ + fn try_find(&self, partial: &crate::PartialNameRef) -> Result, Exn> { + self.as_ref().try_find(partial) + } + + fn iter(&self) -> Result, Exn> { + self.as_ref().iter() + } + + fn reflog_exists(&self, name: &crate::FullNameRef) -> Result { + self.as_ref().reflog_exists(name) + } + + fn reflog_iter<'a>( + &self, + name: &crate::FullNameRef, + buf: &'a mut Vec, + ) -> Result>, Exn> { + self.as_ref().reflog_iter(name, buf) + } + + fn reflog_iter_rev<'a>( + &self, + name: &crate::FullNameRef, + buf: &'a mut [u8], + ) -> Result>, Exn> { + self.as_ref().reflog_iter_rev(name, buf) + } +} + +impl StoreMutate for Arc +where + T: StoreMutate + ?Sized, +{ + fn transaction(&self) -> Result, Exn> { + self.as_ref().transaction() + } +} + +impl StoreRead for Box +where + T: StoreRead + ?Sized, +{ + fn try_find(&self, partial: &crate::PartialNameRef) -> Result, Exn> { + self.as_ref().try_find(partial) + } + + fn iter(&self) -> Result, Exn> { + self.as_ref().iter() + } + + fn reflog_exists(&self, name: &crate::FullNameRef) -> Result { + self.as_ref().reflog_exists(name) + } + + fn reflog_iter<'a>( + &self, + name: &crate::FullNameRef, + buf: &'a mut Vec, + ) -> Result>, Exn> { + self.as_ref().reflog_iter(name, buf) + } + + fn reflog_iter_rev<'a>( + &self, + name: &crate::FullNameRef, + buf: &'a mut [u8], + ) -> Result>, Exn> { + self.as_ref().reflog_iter_rev(name, buf) + } +} + +impl StoreMutate for Box +where + T: StoreMutate + ?Sized, +{ + fn transaction(&self) -> Result, Exn> { + self.as_ref().transaction() + } +} diff --git a/gix-ref/tests/refs/main.rs b/gix-ref/tests/refs/main.rs index 7ec1b16c266..544eefa3770 100644 --- a/gix-ref/tests/refs/main.rs +++ b/gix-ref/tests/refs/main.rs @@ -44,4 +44,5 @@ mod namespace; mod packed; mod reference; mod store; +mod traits; mod transaction; diff --git a/gix-ref/tests/refs/traits.rs b/gix-ref/tests/refs/traits.rs new file mode 100644 index 00000000000..059fc908f3c --- /dev/null +++ b/gix-ref/tests/refs/traits.rs @@ -0,0 +1,142 @@ +use std::{rc::Rc, sync::Arc}; + +use gix_lock::acquire::Fail; +use gix_ref::{ + transaction::{Change, LogChange, PreviousValue, RefEdit}, + FullNameRef, PartialNameRef, StoreMutate, StoreRead, StoreReadExt, Target, +}; + +#[test] +fn try_find_success_and_miss() -> crate::Result { + let store = crate::file::store_with_packed_refs()?; + + let present: &PartialNameRef = "main".try_into()?; + assert!(StoreRead::try_find(&store, present).expect("lookup succeeds").is_some()); + + let missing: &PartialNameRef = "this-definitely-does-not-exist".try_into()?; + assert!(StoreRead::try_find(&store, missing).expect("lookup succeeds").is_none()); + Ok(()) +} + +#[test] +fn find_miss_returns_exn() -> crate::Result { + let store = crate::file::store_with_packed_refs()?; + let missing: &PartialNameRef = "this-definitely-does-not-exist".try_into()?; + + let err = StoreReadExt::find(&store, missing).expect_err("must report not-found"); + assert!( + err.to_string().contains("could not be found"), + "the missing-reference condition is reported through Exn" + ); + Ok(()) +} + +#[test] +fn iter_all_works_via_trait() -> crate::Result { + let store = crate::file::store_with_packed_refs()?; + let platform = StoreRead::iter(&store).expect("iterator platform can be created"); + assert!( + platform.all()?.next().is_some(), + "fixture has at least one reference in iteration" + ); + Ok(()) +} + +#[test] +fn reflog_apis_work_via_trait() -> crate::Result { + let store = crate::file::store_at("make_repo_for_reflog.sh")?; + let head: &FullNameRef = "HEAD".try_into()?; + let missing: &FullNameRef = "refs/heads/does-not-exist".try_into()?; + + assert!(StoreRead::reflog_exists(&store, head).expect("lookup succeeds")); + assert!(!StoreRead::reflog_exists(&store, missing).expect("lookup succeeds")); + + let mut buf = Vec::new(); + assert!( + StoreRead::reflog_iter(&store, head, &mut buf) + .expect("iteration works") + .is_some(), + "HEAD has a reflog in the fixture" + ); + assert!( + StoreRead::reflog_iter(&store, missing, &mut buf) + .expect("iteration works") + .is_none(), + "missing refs have no reflog" + ); + + let mut reverse_buf = [0u8; 512]; + assert!( + StoreRead::reflog_iter_rev(&store, head, &mut reverse_buf) + .expect("reverse iteration works") + .is_some(), + "HEAD has a reverse reflog iterator" + ); + assert!( + StoreRead::reflog_iter_rev(&store, missing, &mut reverse_buf) + .expect("reverse iteration works") + .is_none(), + "missing refs have no reflog" + ); + Ok(()) +} + +#[test] +fn transaction_via_trait_is_usable() -> crate::Result { + let dir = gix_testtools::scripted_fixture_writable_standalone("make_repo_for_reflog.sh")?; + let store = gix_ref::file::Store::at( + dir.path().join(".git"), + gix_ref::store::init::Options { + write_reflog: gix_ref::store::WriteReflog::Disable, + ..Default::default() + }, + ); + + let expected = crate::hex_to_id("28ce6a8b26aa170e1de65536fe8abe1832bd3242"); + StoreMutate::transaction(&store) + .expect("transaction opens") + .prepare( + [RefEdit { + change: Change::Update { + log: LogChange::default(), + expected: PreviousValue::MustNotExist, + new: Target::Object(expected), + }, + name: "refs/heads/trait-created".try_into()?, + deref: false, + }], + Fail::Immediately, + Fail::Immediately, + )? + .commit(None)?; + + let created: &PartialNameRef = "refs/heads/trait-created".try_into()?; + let reference = StoreReadExt::find(&store, created).expect("newly created reference is readable"); + assert_eq!(reference.target.id(), expected.as_ref()); + Ok(()) +} + +#[test] +fn blanket_impls_compile_for_shared_owners() -> crate::Result { + fn use_read_api(store: &T) { + let partial: &PartialNameRef = "main".try_into().expect("valid"); + let _ = StoreRead::try_find(store, partial).expect("read works"); + let _ = StoreRead::iter(store).expect("iter works"); + } + + fn use_mutate_api(store: &T) { + let _ = StoreMutate::transaction(store).expect("transaction can be obtained"); + } + + let store = crate::file::store_with_packed_refs()?; + use_read_api(&store); + use_read_api(&Rc::new(store.clone())); + use_read_api(&Arc::new(store.clone())); + use_read_api(&Box::new(store.clone())); + + use_mutate_api(&store); + use_mutate_api(&Rc::new(store.clone())); + use_mutate_api(&Arc::new(store.clone())); + use_mutate_api(&Box::new(store)); + Ok(()) +}