Skip to main content

radicle_fetch/
lib.rs

1pub mod git;
2pub mod handle;
3pub mod policy;
4pub mod transport;
5
6pub(crate) mod sigrefs;
7
8mod refs;
9mod stage;
10mod state;
11
12use std::io;
13use std::time::Instant;
14
15use gix_protocol::{handshake, Handshake};
16
17pub use gix_protocol::{transport::bstr::ByteSlice, RemoteProgress};
18pub use handle::Handle;
19pub use policy::{Allowed, BlockList, Scope};
20use radicle::storage::git::Repository;
21pub use state::{FetchLimit, FetchResult};
22pub use transport::Transport;
23
24use radicle::crypto::PublicKey;
25use radicle::storage::refs::RefsAt;
26use radicle::storage::ReadRepository as _;
27use state::FetchState;
28use thiserror::Error;
29
30#[derive(Debug, Error)]
31pub enum Error {
32    #[error(transparent)]
33    Handshake(Box<HandshakeError>),
34    #[error("failed to load `rad/id`")]
35    Identity {
36        #[source]
37        err: Box<dyn std::error::Error + Send + Sync + 'static>,
38    },
39    #[error(transparent)]
40    Protocol(#[from] state::error::Protocol),
41    #[error("missing `rad/id`")]
42    MissingRadId,
43    #[error("attempted to replicate from self")]
44    ReplicateSelf,
45}
46
47impl From<HandshakeError> for Error {
48    fn from(err: HandshakeError) -> Self {
49        Self::Handshake(Box::new(err))
50    }
51}
52
53#[derive(Debug, Error)]
54pub enum HandshakeError {
55    #[error("failed to perform fetch handshake: {0}")]
56    Gix(handshake::Error),
57    #[error("an I/O error occurred during the fetch handshake ({0})")]
58    Io(io::Error),
59}
60
61/// Pull changes from the `remote`.
62///
63/// It is expected that the local peer has a copy of the repository
64/// and is pulling new changes. If the repository does not exist, then
65/// [`clone`] should be used.
66pub fn pull<R, S>(
67    handle: &mut Handle<R, S>,
68    limit: FetchLimit,
69    remote: PublicKey,
70    refs_at: Option<Vec<RefsAt>>,
71) -> Result<FetchResult, Error>
72where
73    R: AsRef<Repository>,
74    S: transport::ConnectionStream,
75{
76    let start = Instant::now();
77    let local = *handle.local();
78    if local == remote {
79        return Err(Error::ReplicateSelf);
80    }
81    let handshake = perform_handshake(handle)?;
82    let state = FetchState::default();
83
84    // N.b. ensure that we ignore the local peer's key.
85    handle.blocked.extend([local]);
86    let result = state
87        .run(handle, &handshake, limit, remote, refs_at)
88        .map_err(Error::Protocol);
89
90    log::debug!(
91        "Finished pull of {} ({}ms)",
92        handle.repository().id(),
93        start.elapsed().as_millis()
94    );
95    result
96}
97
98/// Clone changes from the `remote`.
99///
100/// It is expected that the local peer has an empty repository which
101/// they want to populate with the `remote`'s view of the project.
102pub fn clone<R, S>(
103    handle: &mut Handle<R, S>,
104    limit: FetchLimit,
105    remote: PublicKey,
106) -> Result<FetchResult, Error>
107where
108    R: AsRef<Repository>,
109    S: transport::ConnectionStream,
110{
111    let start = Instant::now();
112    if *handle.local() == remote {
113        return Err(Error::ReplicateSelf);
114    }
115    let handshake = perform_handshake(handle)?;
116    let state = FetchState::default();
117    let result = state
118        .run(handle, &handshake, limit, remote, None)
119        .map_err(Error::Protocol);
120    let elapsed = start.elapsed().as_millis();
121    let rid = handle.repository().id();
122
123    match &result {
124        Ok(_) => {
125            log::debug!("Finished clone of {rid} from {remote} ({elapsed}ms)",);
126        }
127        Err(e) => {
128            log::debug!("Clone of {rid} from {remote} failed with '{e}' ({elapsed}ms)",);
129        }
130    }
131    result
132}
133
134fn perform_handshake<R, S>(handle: &mut Handle<R, S>) -> Result<Handshake, Error>
135where
136    S: transport::ConnectionStream,
137{
138    handle
139        .transport
140        .handshake()
141        .map_err(handle_handshake_err)
142        .map_err(Error::from)
143}
144
145fn handle_handshake_err(err: handshake::Error) -> HandshakeError {
146    match err {
147        handshake::Error::Transport(error) => match error {
148            gix_transport::client::Error::Io(error) => HandshakeError::Io(error),
149            err => HandshakeError::Gix(handshake::Error::Transport(err)),
150        },
151        err => HandshakeError::Gix(err),
152    }
153}