pub mod git;
pub mod handle;
pub mod policy;
pub mod transport;
pub(crate) mod sigrefs;
mod refs;
mod stage;
mod state;
use std::io;
use std::time::Instant;
use gix_protocol::{Handshake, handshake};
pub use gix_protocol::{RemoteProgress, transport::bstr::ByteSlice};
pub use handle::Handle;
pub use policy::{Allowed, BlockList, Scope};
use radicle::storage::git::Repository;
pub use state::{Config, FetchLimit, FetchResult};
pub use transport::Transport;
use radicle::crypto::PublicKey;
use radicle::storage::ReadRepository as _;
use radicle::storage::refs::RefsAt;
use state::FetchState;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum Error {
#[error(transparent)]
Handshake(Box<HandshakeError>),
#[error("failed to load `rad/id`")]
Identity {
#[source]
err: Box<dyn std::error::Error + Send + Sync + 'static>,
},
#[error(transparent)]
Protocol(#[from] state::error::Protocol),
#[error("missing `rad/id`")]
MissingRadId,
#[error("attempted to replicate from self")]
ReplicateSelf,
}
impl From<HandshakeError> for Error {
fn from(err: HandshakeError) -> Self {
Self::Handshake(Box::new(err))
}
}
#[derive(Debug, Error)]
pub enum HandshakeError {
#[error("failed to perform fetch handshake: {0}")]
Gix(handshake::Error),
#[error("an I/O error occurred during the fetch handshake ({0})")]
Io(io::Error),
}
/// Pull changes from the `remote`.
///
/// 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<R, S>(
handle: &mut Handle<R, S>,
config: Config,
remote: PublicKey,
refs_at: Option<Vec<RefsAt>>,
) -> Result<FetchResult, Error>
where
R: AsRef<Repository>,
S: transport::ConnectionStream,
{
let start = Instant::now();
let local = *handle.local();
if local == remote {
return Err(Error::ReplicateSelf);
}
let handshake = perform_handshake(handle)?;
let state = FetchState::default();
// N.b. ensure that we ignore the local peer's key.
handle.blocked.extend([local]);
let result = state
.run(handle, &handshake, config, remote, refs_at)
.map_err(Error::Protocol);
log::debug!(
"Finished pull of {} ({}ms)",
handle.repository().id(),
start.elapsed().as_millis()
);
result
}
/// Clone changes from the `remote`.
///
/// 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<R, S>(
handle: &mut Handle<R, S>,
config: Config,
remote: PublicKey,
) -> Result<FetchResult, Error>
where
R: AsRef<Repository>,
S: transport::ConnectionStream,
{
let start = Instant::now();
if *handle.local() == remote {
return Err(Error::ReplicateSelf);
}
let handshake = perform_handshake(handle)?;
let state = FetchState::default();
let result = state
.run(handle, &handshake, config, remote, None)
.map_err(Error::Protocol);
let elapsed = start.elapsed().as_millis();
let rid = handle.repository().id();
match &result {
Ok(_) => {
log::debug!("Finished clone of {rid} from {remote} ({elapsed}ms)",);
}
Err(e) => {
log::debug!("Clone of {rid} from {remote} failed with '{e}' ({elapsed}ms)",);
}
}
result
}
fn perform_handshake<R, S>(handle: &mut Handle<R, S>) -> Result<Handshake, Error>
where
S: transport::ConnectionStream,
{
handle
.transport
.handshake()
.map_err(handle_handshake_err)
.map_err(Error::from)
}
fn handle_handshake_err(err: handshake::Error) -> HandshakeError {
match err {
handshake::Error::Transport(error) => match error {
gix_transport::client::Error::Io(error) => HandshakeError::Io(error),
err => HandshakeError::Gix(handshake::Error::Transport(err)),
},
err => HandshakeError::Gix(err),
}
}