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