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