317 lines · 9.6 KB · Rust
RawDownload
use std::{    io,    path::PathBuf,    sync::{Arc, atomic::AtomicBool},}; use crate::{OutputFormat, net, pack::receive::protocol::fetch::negotiate};#[cfg(feature = "async-client")]use gix::protocol::transport::client::async_io::connect;#[cfg(feature = "blocking-client")]use gix::protocol::transport::client::blocking_io::connect;use gix::{DynNestedProgress, config::tree::Key, protocol::bisync, remote::fetch::Error};pub use gix::{    NestedProgress, Progress,    hash::ObjectId,    objs::bstr::{BString, ByteSlice},    odb::pack,    protocol,    protocol::{        fetch::{Arguments, Response},        handshake::Ref,        transport,        transport::client::Capabilities,    },}; pub const PROGRESS_RANGE: std::ops::RangeInclusive<u8> = 1..=3;pub struct Context<W> {    pub thread_limit: Option<usize>,    pub format: OutputFormat,    pub should_interrupt: Arc<AtomicBool>,    pub out: W,    pub object_hash: gix::hash::Kind,} #[bisync::bisync]pub async fn receive<P, W>(    protocol: Option<net::Protocol>,    url: &str,    directory: Option<PathBuf>,    refs_directory: Option<PathBuf>,    mut wanted_refs: Vec<BString>,    mut progress: P,    ctx: Context<W>,) -> anyhow::Result<()>where    W: std::io::Write,    P: NestedProgress + 'static,    P::SubProgress: 'static,{    let mut transport = net::connect(        url,        connect::Options {            version: protocol.unwrap_or_default().into(),            ..Default::default()        },    )    .await?;    let trace_packetlines = std::env::var_os(        gix::config::tree::Gitoxide::TRACE_PACKET            .environment_override()            .expect("set"),    )    .is_some();     let agent = gix::protocol::agent(gix::env::agent());    let mut handshake = gix::protocol::handshake(        &mut transport.inner,        transport::Service::UploadPack,        gix::protocol::credentials::builtin,        vec![("agent".into(), Some(agent.clone()))],        &mut progress,    )    .await?;    if wanted_refs.is_empty() {        wanted_refs.push("refs/heads/*:refs/remotes/origin/*".into());    }    let fetch_refspecs: Vec<_> = wanted_refs        .into_iter()        .map(|ref_name| {            gix::refspec::parse(ref_name.as_bstr(), gix::refspec::parse::Operation::Fetch).map(|r| r.to_owned())        })        .collect::<Result<_, _>>()?;    let user_agent = ("agent", Some(agent.clone()));     let context = gix::protocol::fetch::refmap::init::Context {        fetch_refspecs: fetch_refspecs.clone(),        extra_refspecs: vec![],    };     let fetch_refmap = handshake.prepare_lsrefs_or_extract_refmap(user_agent.clone(), true, context)?;     #[cfg(feature = "async-client")]    let refmap = fetch_refmap        .fetch_async(&mut progress, &mut transport.inner, trace_packetlines)        .await?;     #[cfg(feature = "blocking-client")]    let refmap = fetch_refmap.fetch_blocking(&mut progress, &mut transport.inner, trace_packetlines)?;     if refmap.is_missing_required_mapping() {        return Err(Error::NoMapping {            refspecs: refmap.refspecs.clone(),            num_remote_refs: refmap.remote_refs.len(),        }        .into());    }     let mut negotiate = Negotiate { refmap: &refmap };    gix::protocol::fetch(        &mut negotiate,        |read_pack, progress, should_interrupt| {            receive_pack_blocking(                directory,                refs_directory,                read_pack,                progress,                &refmap.remote_refs,                should_interrupt,                ctx.out,                ctx.thread_limit,                ctx.object_hash,                ctx.format,            )            .map(|_| true)        },        progress,        &ctx.should_interrupt,        gix::protocol::fetch::Context {            handshake: &mut handshake,            transport: &mut transport.inner,            user_agent,            trace_packetlines,        },        gix::protocol::fetch::Options {            shallow_file: "no shallow file required as we reject it to keep it simple".into(),            shallow: &Default::default(),            tags: Default::default(),            reject_shallow_remote: true,        },    )    .await?;    Ok(())} struct Negotiate<'a> {    refmap: &'a gix::protocol::fetch::RefMap,} impl gix::protocol::fetch::Negotiate for Negotiate<'_> {    fn mark_complete_and_common_ref(&mut self) -> Result<negotiate::Action, negotiate::Error> {        Ok(negotiate::Action::MustNegotiate {            remote_ref_target_known: vec![], /* we don't really negotiate */        })    }     fn add_wants(&mut self, arguments: &mut Arguments, _remote_ref_target_known: &[bool]) -> bool {        let mut has_want = false;        for id in self.refmap.mappings.iter().filter_map(|m| m.remote.as_id()) {            arguments.want(id);            has_want = true;        }        has_want    }     fn one_round(        &mut self,        _state: &mut negotiate::one_round::State,        _arguments: &mut Arguments,        _previous_response: Option<&Response>,    ) -> Result<(negotiate::Round, bool), negotiate::Error> {        Ok((            negotiate::Round {                haves_sent: 0,                in_vain: 0,                haves_to_send: 0,                previous_response_had_at_least_one_in_common: false,            },            // is done            true,        ))    }} #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]pub struct JsonBundleWriteOutcome {    pub index_version: pack::index::Version,    pub index_hash: String,     pub data_hash: String,    pub num_objects: u32,} impl From<pack::index::write::Outcome> for JsonBundleWriteOutcome {    fn from(v: pack::index::write::Outcome) -> Self {        JsonBundleWriteOutcome {            index_version: v.index_version,            num_objects: v.num_objects,            data_hash: v.data_hash.to_string(),            index_hash: v.index_hash.to_string(),        }    }} #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]pub struct JsonOutcome {    pub index: JsonBundleWriteOutcome,    pub pack_kind: pack::data::Version,     pub index_path: Option<PathBuf>,    pub data_path: Option<PathBuf>,     pub refs: Vec<crate::repository::remote::JsonRef>,} impl JsonOutcome {    pub fn from_outcome_and_refs(v: pack::bundle::write::Outcome, refs: &[Ref]) -> Self {        JsonOutcome {            index: v.index.into(),            pack_kind: v.pack_version,            index_path: v.index_path,            data_path: v.data_path,            refs: refs.iter().cloned().map(Into::into).collect(),        }    }} fn print_hash_and_path(out: &mut impl io::Write, name: &str, id: ObjectId, path: Option<PathBuf>) -> io::Result<()> {    match path {        Some(path) => writeln!(out, "{}: {} ({})", name, id, path.display()),        None => writeln!(out, "{name}: {id}"),    }} fn print(out: &mut impl io::Write, res: pack::bundle::write::Outcome, refs: &[Ref]) -> io::Result<()> {    print_hash_and_path(out, "index", res.index.index_hash, res.index_path)?;    print_hash_and_path(out, "pack", res.index.data_hash, res.data_path)?;    writeln!(out)?;    crate::repository::remote::refs::print(out, refs)?;    Ok(())} fn write_raw_refs(refs: &[Ref], directory: PathBuf) -> std::io::Result<()> {    let assure_dir_exists = |path: &BString| {        assert!(!path.starts_with_str("/"), "no ref start with a /, they are relative");        let path = directory.join(gix::path::from_byte_slice(path));        std::fs::create_dir_all(path.parent().expect("multi-component path")).map(|_| path)    };    for r in refs {        let (path, content) = match r {            Ref::Unborn { full_ref_name, target } => {                (assure_dir_exists(full_ref_name)?, format!("unborn HEAD: {target}"))            }            Ref::Symbolic {                full_ref_name: path,                target,                ..            } => (assure_dir_exists(path)?, format!("ref: {target}")),            Ref::Peeled {                full_ref_name: path,                tag: object,                ..            }            | Ref::Direct {                full_ref_name: path,                object,            } => (assure_dir_exists(path)?, object.to_string()),        };        std::fs::write(path, content.as_bytes())?;    }    Ok(())} #[expect(clippy::too_many_arguments)]fn receive_pack_blocking(    mut directory: Option<PathBuf>,    mut refs_directory: Option<PathBuf>,    mut input: impl io::BufRead,    progress: &mut dyn DynNestedProgress,    refs: &[Ref],    should_interrupt: &AtomicBool,    mut out: impl std::io::Write,    thread_limit: Option<usize>,    object_hash: gix::hash::Kind,    format: OutputFormat,) -> io::Result<()> {    let options = pack::bundle::write::Options {        thread_limit,        index_version: pack::index::Version::V2,        iteration_mode: pack::data::input::Mode::Verify,        object_hash,        alloc_limit_bytes: None,        compression: gix::zlib::Compression::BEST_SPEED,    };    let outcome = pack::Bundle::write_to_directory(        &mut input,        directory.take().as_deref(),        progress,        should_interrupt,        None::<gix::objs::find::Never>,        options,    )    .map_err(io::Error::other)?;     if let Some(directory) = refs_directory.take() {        write_raw_refs(refs, directory)?;    }     match format {        OutputFormat::Human => drop(print(&mut out, outcome, refs)),        #[cfg(feature = "serde")]        OutputFormat::Json => {            serde_json::to_writer_pretty(&mut out, &JsonOutcome::from_outcome_and_refs(outcome, refs))?;        }    }    Ok(())}