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