use std::{ io, path::PathBuf, sync::{atomic::AtomicBool, Arc}, }; use git_repository::{ hash::ObjectId, objs::bstr::{BString, ByteSlice}, odb::pack, protocol, protocol::{ fetch::{Action, Arguments, LsRefsAction, Ref, Response}, transport, transport::client::Capabilities, }, Progress, }; use crate::{remote::refs::JsonRef, OutputFormat}; pub const PROGRESS_RANGE: std::ops::RangeInclusive = 1..=3; pub struct Context { pub thread_limit: Option, pub format: OutputFormat, pub should_interrupt: Arc, pub out: W, pub object_hash: git_repository::hash::Kind, } struct CloneDelegate { ctx: Context, directory: Option, refs_directory: Option, ref_filter: Option<&'static [&'static str]>, wanted_refs: Vec, } static FILTER: &[&str] = &["HEAD", "refs/tags", "refs/heads"]; fn remote_supports_ref_in_want(server: &Capabilities) -> bool { server .capability("fetch") .and_then(|cap| cap.supports("ref-in-want")) .unwrap_or(false) } impl protocol::fetch::DelegateBlocking for CloneDelegate { fn prepare_ls_refs( &mut self, server: &Capabilities, arguments: &mut Vec, _features: &mut Vec<(&str, Option<&str>)>, ) -> io::Result { if server.contains("ls-refs") { arguments.extend(FILTER.iter().map(|r| format!("ref-prefix {}", r).into())); } Ok(if self.wanted_refs.is_empty() { LsRefsAction::Continue } else { LsRefsAction::Skip }) } fn prepare_fetch( &mut self, version: transport::Protocol, server: &Capabilities, _features: &mut Vec<(&str, Option<&str>)>, _refs: &[Ref], ) -> io::Result { if !self.wanted_refs.is_empty() && !remote_supports_ref_in_want(server) { return Err(io::Error::new( io::ErrorKind::Other, "Want to get specific refs, but remote doesn't support this capability", )); } if version == transport::Protocol::V1 { self.ref_filter = Some(FILTER); } Ok(Action::Continue) } fn negotiate( &mut self, refs: &[Ref], arguments: &mut Arguments, _previous_response: Option<&Response>, ) -> io::Result { if self.wanted_refs.is_empty() { for r in refs { let (path, id) = r.unpack(); match self.ref_filter { Some(ref_prefixes) => { if ref_prefixes.iter().any(|prefix| path.starts_with_str(prefix)) { arguments.want(id); } } None => arguments.want(id), } } } else { for r in &self.wanted_refs { arguments.want_ref(r.as_ref()) } } Ok(Action::Cancel) } } #[cfg(feature = "blocking-client")] mod blocking_io { use std::{io, io::BufRead, path::PathBuf}; use git_repository::{ bstr::BString, protocol, protocol::fetch::{Ref, Response}, Progress, }; use super::{receive_pack_blocking, CloneDelegate, Context}; use crate::net; impl protocol::fetch::Delegate for CloneDelegate { fn receive_pack( &mut self, input: impl BufRead, progress: impl Progress, refs: &[Ref], _previous_response: &Response, ) -> io::Result<()> { receive_pack_blocking( self.directory.take(), self.refs_directory.take(), &mut self.ctx, input, progress, refs, ) } } pub fn receive( protocol: Option, url: &str, directory: Option, refs_directory: Option, wanted_refs: Vec, progress: P, ctx: Context, ) -> anyhow::Result<()> { let transport = net::connect(url.as_bytes(), protocol.unwrap_or_default().into())?; let delegate = CloneDelegate { ctx, directory, refs_directory, ref_filter: None, wanted_refs, }; protocol::fetch( transport, delegate, protocol::credentials::helper, progress, protocol::FetchConnection::TerminateOnSuccessfulCompletion, )?; Ok(()) } } #[cfg(feature = "blocking-client")] pub use blocking_io::receive; #[cfg(feature = "async-client")] mod async_io { use std::{io, io::BufRead, path::PathBuf}; use async_trait::async_trait; use futures_io::AsyncBufRead; use git_repository::{ bstr::{BString, ByteSlice}, odb::pack, protocol, protocol::fetch::{Ref, Response}, Progress, }; use super::{print, receive_pack_blocking, write_raw_refs, CloneDelegate, Context}; use crate::{net, OutputFormat}; #[async_trait(?Send)] impl protocol::fetch::Delegate for CloneDelegate { async fn receive_pack( &mut self, input: impl AsyncBufRead + Unpin + 'async_trait, progress: impl Progress, refs: &[Ref], _previous_response: &Response, ) -> io::Result<()> { receive_pack_blocking( self.directory.take(), self.refs_directory.take(), &mut self.ctx, futures_lite::io::BlockOn::new(input), progress, &refs, ) } } pub async fn receive( protocol: Option, url: &str, directory: Option, refs_directory: Option, wanted_refs: Vec, progress: P, ctx: Context, ) -> anyhow::Result<()> { let transport = net::connect(url.as_bytes(), protocol.unwrap_or_default().into()).await?; let mut delegate = CloneDelegate { ctx, directory, refs_directory, ref_filter: None, wanted_refs, }; blocking::unblock(move || { futures_lite::future::block_on(protocol::fetch( transport, delegate, protocol::credentials::helper, progress, protocol::FetchConnection::TerminateOnSuccessfulCompletion, )) }) .await?; Ok(()) } } #[cfg(feature = "async-client")] pub use self::async_io::receive; #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] pub struct JsonBundleWriteOutcome { pub index_kind: pack::index::Version, pub index_hash: String, pub data_hash: String, pub num_objects: u32, } impl From for JsonBundleWriteOutcome { fn from(v: pack::index::write::Outcome) -> Self { JsonBundleWriteOutcome { index_kind: v.index_kind, num_objects: v.num_objects, data_hash: v.data_hash.to_string(), index_hash: v.index_hash.to_string(), } } } #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] pub struct JsonOutcome { pub index: JsonBundleWriteOutcome, pub pack_kind: pack::data::Version, pub index_path: Option, pub data_path: Option, pub refs: Vec, } 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_kind, 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) -> 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::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(git_features::path::from_byte_slice_or_panic_on_windows(path)); std::fs::create_dir_all(path.parent().expect("multi-component path")).map(|_| path) }; for r in refs { let (path, content) = match r { Ref::Symbolic { path, target, .. } => (assure_dir_exists(path)?, format!("ref: {}", target)), Ref::Peeled { path, tag: object, .. } | Ref::Direct { path, object } => { (assure_dir_exists(path)?, object.to_string()) } }; std::fs::write(path, content.as_bytes())?; } Ok(()) } fn receive_pack_blocking( mut directory: Option, mut refs_directory: Option, ctx: &mut Context, input: impl io::BufRead, progress: impl Progress, refs: &[Ref], ) -> io::Result<()> { let options = pack::bundle::write::Options { thread_limit: ctx.thread_limit, index_kind: pack::index::Version::V2, iteration_mode: pack::data::input::Mode::Verify, object_hash: ctx.object_hash, }; let outcome = pack::Bundle::write_to_directory(input, directory.take(), progress, &ctx.should_interrupt, None, options) .map_err(|err| io::Error::new(io::ErrorKind::Other, err))?; if let Some(directory) = refs_directory.take() { write_raw_refs(refs, directory)?; } match ctx.format { OutputFormat::Human => drop(print(&mut ctx.out, outcome, refs)), #[cfg(feature = "serde1")] OutputFormat::Json => { serde_json::to_writer_pretty(&mut ctx.out, &JsonOutcome::from_outcome_and_refs(outcome, refs))? } }; Ok(()) }