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};
/// 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,
repo: Repository,
repo: R,
pub(crate) allowed: Allowed,
pub(crate) transport: Transport<S>,
/// The set of keys we will ignore when fetching from a
@ -29,39 +29,11 @@ pub struct Handle<S> {
pub(crate) interrupt: Arc<AtomicBool>,
}
impl<S> Handle<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)),
})
}
impl<R, S> Handle<R, S> {
pub fn is_blocked(&self, key: &PublicKey) -> bool {
self.blocked.is_blocked(key)
}
#[inline]
pub fn repository(&self) -> &Repository {
&self.repo
}
#[inline]
pub fn local(&self) -> &PublicKey {
&self.local
@ -71,15 +43,52 @@ impl<S> Handle<S> {
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 {
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 {
use radicle::node::policy;
use radicle::prelude::RepoId;

View File

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

View File

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

View File

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

View File

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

View File

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