fetch: use `AsRef<Repository>`

Allow the fetch interface to accept anything that implements
`AsRef<Repository>`. This allows flexibility in the types that the `Handle` can
accept.

This change is motivated by wanting to introduce a type that is a temporary
repository that wraps a `Repository`.
This commit is contained in:
Fintan Halpenny 2025-10-03 15:52:25 +01:00 committed by Lorenz Leutgeb
parent a163f4e939
commit e40fe86ff8
6 changed files with 101 additions and 60 deletions

View File

@ -12,9 +12,9 @@ use crate::policy::{Allowed, BlockList};
use crate::transport::{ConnectionStream, Transport}; use crate::transport::{ConnectionStream, Transport};
/// The handle used for pulling or cloning changes from a remote peer. /// The handle used for pulling or cloning changes from a remote peer.
pub struct Handle<S> { pub struct Handle<R, S> {
pub(crate) local: PublicKey, pub(crate) local: PublicKey,
repo: Repository, repo: R,
pub(crate) allowed: Allowed, pub(crate) allowed: Allowed,
pub(crate) transport: Transport<S>, pub(crate) transport: Transport<S>,
/// The set of keys we will ignore when fetching from a /// The set of keys we will ignore when fetching from a
@ -29,39 +29,11 @@ pub struct Handle<S> {
pub(crate) interrupt: Arc<AtomicBool>, pub(crate) interrupt: Arc<AtomicBool>,
} }
impl<S> Handle<S> { impl<R, S> Handle<R, S> {
pub fn new(
local: PublicKey,
repo: Repository,
follow: Allowed,
blocked: BlockList,
connection: S,
) -> Result<Self, error::Init>
where
S: ConnectionStream,
{
let git_dir = repo.backend.path().to_path_buf();
let transport = Transport::new(git_dir, BString::from(repo.id.canonical()), connection);
Ok(Self {
local,
repo,
allowed: follow,
transport,
blocked,
interrupt: Arc::new(AtomicBool::new(false)),
})
}
pub fn is_blocked(&self, key: &PublicKey) -> bool { pub fn is_blocked(&self, key: &PublicKey) -> bool {
self.blocked.is_blocked(key) self.blocked.is_blocked(key)
} }
#[inline]
pub fn repository(&self) -> &Repository {
&self.repo
}
#[inline] #[inline]
pub fn local(&self) -> &PublicKey { pub fn local(&self) -> &PublicKey {
&self.local &self.local
@ -71,15 +43,52 @@ impl<S> Handle<S> {
self.interrupt.store(true, atomic::Ordering::Relaxed); self.interrupt.store(true, atomic::Ordering::Relaxed);
} }
pub fn verified(&self, head: Oid) -> Result<Doc, DocError> {
Ok(self.repo.identity_doc_at(head)?.doc)
}
pub fn allowed(&self) -> Allowed { pub fn allowed(&self) -> Allowed {
self.allowed.clone() self.allowed.clone()
} }
} }
impl<R, S> Handle<R, S>
where
R: AsRef<Repository>,
{
pub fn new(
local: PublicKey,
repo: R,
follow: Allowed,
blocked: BlockList,
connection: S,
) -> Result<Self, error::Init>
where
S: ConnectionStream,
{
let git_dir = repo.as_ref().backend.path().to_path_buf();
let transport = Transport::new(
git_dir,
BString::from(repo.as_ref().id.canonical()),
connection,
);
Ok(Self {
local,
repo,
allowed: follow,
transport,
blocked,
interrupt: Arc::new(AtomicBool::new(false)),
})
}
#[inline]
pub fn repository(&self) -> &Repository {
self.repo.as_ref()
}
pub fn verified(&self, head: Oid) -> Result<Doc, DocError> {
Ok(self.repository().identity_doc_at(head)?.doc)
}
}
pub mod error { pub mod error {
use radicle::node::policy; use radicle::node::policy;
use radicle::prelude::RepoId; use radicle::prelude::RepoId;

View File

@ -16,6 +16,7 @@ use gix_protocol::handshake;
pub use gix_protocol::{transport::bstr::ByteSlice, RemoteProgress}; pub use gix_protocol::{transport::bstr::ByteSlice, RemoteProgress};
pub use handle::Handle; pub use handle::Handle;
pub use policy::{Allowed, BlockList, Scope}; pub use policy::{Allowed, BlockList, Scope};
use radicle::storage::git::Repository;
pub use state::{FetchLimit, FetchResult}; pub use state::{FetchLimit, FetchResult};
pub use transport::Transport; pub use transport::Transport;
@ -47,13 +48,14 @@ pub enum Error {
/// It is expected that the local peer has a copy of the repository /// It is expected that the local peer has a copy of the repository
/// and is pulling new changes. If the repository does not exist, then /// and is pulling new changes. If the repository does not exist, then
/// [`clone`] should be used. /// [`clone`] should be used.
pub fn pull<S>( pub fn pull<R, S>(
handle: &mut Handle<S>, handle: &mut Handle<R, S>,
limit: FetchLimit, limit: FetchLimit,
remote: PublicKey, remote: PublicKey,
refs_at: Option<Vec<RefsAt>>, refs_at: Option<Vec<RefsAt>>,
) -> Result<FetchResult, Error> ) -> Result<FetchResult, Error>
where where
R: AsRef<Repository>,
S: transport::ConnectionStream, S: transport::ConnectionStream,
{ {
let start = Instant::now(); let start = Instant::now();
@ -83,12 +85,13 @@ where
/// ///
/// It is expected that the local peer has an empty repository which /// It is expected that the local peer has an empty repository which
/// they want to populate with the `remote`'s view of the project. /// they want to populate with the `remote`'s view of the project.
pub fn clone<S>( pub fn clone<R, S>(
handle: &mut Handle<S>, handle: &mut Handle<R, S>,
limit: FetchLimit, limit: FetchLimit,
remote: PublicKey, remote: PublicKey,
) -> Result<FetchResult, Error> ) -> Result<FetchResult, Error>
where where
R: AsRef<Repository>,
S: transport::ConnectionStream, S: transport::ConnectionStream,
{ {
let start = Instant::now(); let start = Instant::now();
@ -120,7 +123,7 @@ where
result result
} }
fn perform_handshake<S>(handle: &mut Handle<S>) -> Result<handshake::Outcome, Error> fn perform_handshake<R, S>(handle: &mut Handle<R, S>) -> Result<handshake::Outcome, Error>
where where
S: transport::ConnectionStream, S: transport::ConnectionStream,
{ {

View File

@ -1,6 +1,7 @@
use std::collections::{BTreeMap, BTreeSet}; use std::collections::{BTreeMap, BTreeSet};
use std::ops::{Deref, Not as _}; use std::ops::{Deref, Not as _};
use radicle::storage::git::Repository;
pub use radicle::storage::refs::SignedRefsAt; pub use radicle::storage::refs::SignedRefsAt;
pub use radicle::storage::{git::Validation, Validations}; pub use radicle::storage::{git::Validation, Validations};
use radicle::{crypto::PublicKey, storage::ValidateRepository}; use radicle::{crypto::PublicKey, storage::ValidateRepository};
@ -53,10 +54,13 @@ impl<T> DelegateStatus<T> {
/// Construct a `DelegateStatus` with [`SignedRefsAt`] signed reference /// Construct a `DelegateStatus` with [`SignedRefsAt`] signed reference
/// data, if it can be found in `repo`. /// data, if it can be found in `repo`.
pub fn load<S>( pub fn load<R, S>(
self, self,
cached: &Cached<S>, cached: &Cached<R, S>,
) -> Result<DelegateStatus<Option<SignedRefsAt>>, radicle::storage::refs::Error> { ) -> Result<DelegateStatus<Option<SignedRefsAt>>, radicle::storage::refs::Error>
where
R: AsRef<Repository>,
{
let remote = *self.remote(); let remote = *self.remote();
self.traverse(|_| cached.load(&remote)) self.traverse(|_| cached.load(&remote))
} }
@ -102,10 +106,13 @@ impl RemoteRefs {
/// ///
/// If the sigrefs are missing for a given remote, regardless of delegate /// If the sigrefs are missing for a given remote, regardless of delegate
/// status, then that remote is filtered out. /// status, then that remote is filtered out.
pub(crate) fn load<'a, S>( pub(crate) fn load<'a, R, S>(
cached: &Cached<S>, cached: &Cached<R, S>,
remotes: impl Iterator<Item = &'a PublicKey>, remotes: impl Iterator<Item = &'a PublicKey>,
) -> Result<Self, error::RemoteRefs> { ) -> Result<Self, error::RemoteRefs>
where
R: AsRef<Repository>,
{
remotes remotes
.filter_map(|id| match cached.load(id) { .filter_map(|id| match cached.load(id) {
Ok(None) => None, Ok(None) => None,

View File

@ -8,6 +8,7 @@ use radicle::identity::{Did, Doc, DocError};
use radicle::prelude::Verified; use radicle::prelude::Verified;
use radicle::storage; use radicle::storage;
use radicle::storage::git::Repository;
use radicle::storage::refs::RefsAt; use radicle::storage::refs::RefsAt;
use radicle::storage::{ use radicle::storage::{
git::Validation, Remote, RemoteId, RemoteRepository, Remotes, ValidateRepository, Validations, git::Validation, Remote, RemoteId, RemoteRepository, Remotes, ValidateRepository, Validations,
@ -196,7 +197,10 @@ impl FetchState {
ap ap
} }
pub(crate) fn as_cached<'a, S>(&'a mut self, handle: &'a mut Handle<S>) -> Cached<'a, S> { pub(crate) fn as_cached<'a, R, S>(
&'a mut self,
handle: &'a mut Handle<R, S>,
) -> Cached<'a, R, S> {
Cached { Cached {
handle, handle,
state: self, state: self,
@ -207,13 +211,14 @@ impl FetchState {
impl FetchState { impl FetchState {
/// Perform the ls-refs and fetch for the given `step`. The result /// Perform the ls-refs and fetch for the given `step`. The result
/// of these processes is kept track of in the internal state. /// of these processes is kept track of in the internal state.
pub(super) fn run_stage<S, F>( pub(super) fn run_stage<R, S, F>(
&mut self, &mut self,
handle: &mut Handle<S>, handle: &mut Handle<R, S>,
handshake: &handshake::Outcome, handshake: &handshake::Outcome,
step: &F, step: &F,
) -> Result<BTreeSet<PublicKey>, error::Step> ) -> Result<BTreeSet<PublicKey>, error::Step>
where where
R: AsRef<Repository>,
S: transport::ConnectionStream, S: transport::ConnectionStream,
F: ProtocolStage, F: ProtocolStage,
{ {
@ -280,9 +285,9 @@ impl FetchState {
/// The resulting [`sigrefs::RemoteRefs`] will be the set of /// The resulting [`sigrefs::RemoteRefs`] will be the set of
/// `rad/sigrefs` of the fetched remotes. /// `rad/sigrefs` of the fetched remotes.
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
fn run_special_refs<S>( fn run_special_refs<R, S>(
&mut self, &mut self,
handle: &mut Handle<S>, handle: &mut Handle<R, S>,
handshake: &handshake::Outcome, handshake: &handshake::Outcome,
delegates: BTreeSet<PublicKey>, delegates: BTreeSet<PublicKey>,
threshold: usize, threshold: usize,
@ -291,6 +296,7 @@ impl FetchState {
refs_at: Option<Vec<RefsAt>>, refs_at: Option<Vec<RefsAt>>,
) -> Result<sigrefs::RemoteRefs, error::Protocol> ) -> Result<sigrefs::RemoteRefs, error::Protocol>
where where
R: AsRef<Repository>,
S: transport::ConnectionStream, S: transport::ConnectionStream,
{ {
match refs_at { match refs_at {
@ -348,15 +354,16 @@ impl FetchState {
/// of updating tips. /// of updating tips.
/// 7. Apply the valid tips, iff no delegates failed validation. /// 7. Apply the valid tips, iff no delegates failed validation.
/// 8. Signal to the other side that the process has completed. /// 8. Signal to the other side that the process has completed.
pub(super) fn run<S>( pub(super) fn run<R, S>(
mut self, mut self,
handle: &mut Handle<S>, handle: &mut Handle<R, S>,
handshake: &handshake::Outcome, handshake: &handshake::Outcome,
limit: FetchLimit, limit: FetchLimit,
remote: PublicKey, remote: PublicKey,
refs_at: Option<Vec<RefsAt>>, refs_at: Option<Vec<RefsAt>>,
) -> Result<FetchResult, error::Protocol> ) -> Result<FetchResult, error::Protocol>
where where
R: AsRef<Repository>,
S: transport::ConnectionStream, S: transport::ConnectionStream,
{ {
let start = Instant::now(); let start = Instant::now();
@ -598,12 +605,15 @@ impl FetchState {
/// A cached version of [`Handle`] by using the underlying /// A cached version of [`Handle`] by using the underlying
/// [`FetchState`]'s data for performing lookups. /// [`FetchState`]'s data for performing lookups.
pub(crate) struct Cached<'a, S> { pub(crate) struct Cached<'a, R, S> {
handle: &'a mut Handle<S>, handle: &'a mut Handle<R, S>,
state: &'a mut FetchState, state: &'a mut FetchState,
} }
impl<S> Cached<'_, S> { impl<R, S> Cached<'_, R, S>
where
R: AsRef<Repository>,
{
/// Resolves `refname` to its [`ObjectId`] by first looking at the /// Resolves `refname` to its [`ObjectId`] by first looking at the
/// [`FetchState`] and falling back to the [`Handle::refdb`]. /// [`FetchState`] and falling back to the [`Handle::refdb`].
pub fn refname_to_id<'b, N>( pub fn refname_to_id<'b, N>(
@ -651,7 +661,10 @@ impl<S> Cached<'_, S> {
} }
} }
impl<S> RemoteRepository for Cached<'_, S> { impl<R, S> RemoteRepository for Cached<'_, R, S>
where
R: AsRef<Repository>,
{
fn remote(&self, remote: &RemoteId) -> Result<Remote, storage::refs::Error> { fn remote(&self, remote: &RemoteId) -> Result<Remote, storage::refs::Error> {
// N.b. this is unused so we just delegate to the underlying // N.b. this is unused so we just delegate to the underlying
// repository for a correct implementation. // repository for a correct implementation.
@ -671,7 +684,10 @@ impl<S> RemoteRepository for Cached<'_, S> {
} }
} }
impl<S> ValidateRepository for Cached<'_, S> { impl<R, S> ValidateRepository for Cached<'_, R, S>
where
R: AsRef<Repository>,
{
// N.b. we don't verify the `rad/id` of each remote since they may // N.b. we don't verify the `rad/id` of each remote since they may
// not have a reference to the COB if they have not interacted // not have a reference to the COB if they have not interacted
// with it. // with it.

View File

@ -27,11 +27,11 @@ use super::channels::ChannelsFlush;
pub enum Handle { pub enum Handle {
Clone { Clone {
handle: radicle_fetch::Handle<ChannelsFlush>, handle: radicle_fetch::Handle<Repository, ChannelsFlush>,
tmp: tempfile::TempDir, tmp: tempfile::TempDir,
}, },
Pull { Pull {
handle: radicle_fetch::Handle<ChannelsFlush>, handle: radicle_fetch::Handle<Repository, ChannelsFlush>,
notifications: node::notifications::StoreWriter, notifications: node::notifications::StoreWriter,
}, },
} }

View File

@ -285,6 +285,12 @@ pub struct Repository {
pub backend: git2::Repository, pub backend: git2::Repository,
} }
impl AsRef<Repository> for Repository {
fn as_ref(&self) -> &Repository {
self
}
}
impl git::canonical::effects::Ancestry for Repository { impl git::canonical::effects::Ancestry for Repository {
fn graph_ahead_behind( fn graph_ahead_behind(
&self, &self,