gitoxide_core/pack/
receive.rs

1use std::{
2    io,
3    path::PathBuf,
4    sync::{atomic::AtomicBool, Arc},
5};
6
7use crate::{net, pack::receive::protocol::fetch::negotiate, OutputFormat};
8#[cfg(feature = "async-client")]
9use gix::protocol::transport::client::async_io::connect;
10#[cfg(feature = "blocking-client")]
11use gix::protocol::transport::client::blocking_io::connect;
12use gix::{config::tree::Key, protocol::maybe_async, remote::fetch::Error, DynNestedProgress};
13pub use gix::{
14    hash::ObjectId,
15    objs::bstr::{BString, ByteSlice},
16    odb::pack,
17    protocol,
18    protocol::{
19        fetch::{Arguments, Response},
20        handshake::Ref,
21        transport,
22        transport::client::Capabilities,
23    },
24    NestedProgress, Progress,
25};
26
27pub const PROGRESS_RANGE: std::ops::RangeInclusive<u8> = 1..=3;
28pub struct Context<W> {
29    pub thread_limit: Option<usize>,
30    pub format: OutputFormat,
31    pub should_interrupt: Arc<AtomicBool>,
32    pub out: W,
33    pub object_hash: gix::hash::Kind,
34}
35
36#[maybe_async::maybe_async]
37pub async fn receive<P, W>(
38    protocol: Option<net::Protocol>,
39    url: &str,
40    directory: Option<PathBuf>,
41    refs_directory: Option<PathBuf>,
42    mut wanted_refs: Vec<BString>,
43    mut progress: P,
44    ctx: Context<W>,
45) -> anyhow::Result<()>
46where
47    W: std::io::Write,
48    P: NestedProgress + 'static,
49    P::SubProgress: 'static,
50{
51    let mut transport = net::connect(
52        url,
53        connect::Options {
54            version: protocol.unwrap_or_default().into(),
55            ..Default::default()
56        },
57    )
58    .await?;
59    let trace_packetlines = std::env::var_os(
60        gix::config::tree::Gitoxide::TRACE_PACKET
61            .environment_override()
62            .expect("set"),
63    )
64    .is_some();
65
66    let agent = gix::protocol::agent(gix::env::agent());
67    let mut handshake = gix::protocol::handshake(
68        &mut transport.inner,
69        transport::Service::UploadPack,
70        gix::protocol::credentials::builtin,
71        vec![("agent".into(), Some(agent.clone()))],
72        &mut progress,
73    )
74    .await?;
75    if wanted_refs.is_empty() {
76        wanted_refs.push("refs/heads/*:refs/remotes/origin/*".into());
77    }
78    let fetch_refspecs: Vec<_> = wanted_refs
79        .into_iter()
80        .map(|ref_name| {
81            gix::refspec::parse(ref_name.as_bstr(), gix::refspec::parse::Operation::Fetch).map(|r| r.to_owned())
82        })
83        .collect::<Result<_, _>>()?;
84    let user_agent = ("agent", Some(agent.clone().into()));
85
86    let context = gix::protocol::fetch::refmap::init::Context {
87        fetch_refspecs: fetch_refspecs.clone(),
88        extra_refspecs: vec![],
89    };
90    let refmap = handshake
91        .fetch_or_extract_refmap(
92            &mut progress,
93            &mut transport.inner,
94            user_agent.clone(),
95            trace_packetlines,
96            true,
97            context,
98        )
99        .await?;
100
101    if refmap.mappings.is_empty() && !refmap.remote_refs.is_empty() {
102        return Err(Error::NoMapping {
103            refspecs: refmap.refspecs.clone(),
104            num_remote_refs: refmap.remote_refs.len(),
105        }
106        .into());
107    }
108
109    let mut negotiate = Negotiate { refmap: &refmap };
110    gix::protocol::fetch(
111        &mut negotiate,
112        |read_pack, progress, should_interrupt| {
113            receive_pack_blocking(
114                directory,
115                refs_directory,
116                read_pack,
117                progress,
118                &refmap.remote_refs,
119                should_interrupt,
120                ctx.out,
121                ctx.thread_limit,
122                ctx.object_hash,
123                ctx.format,
124            )
125            .map(|_| true)
126        },
127        progress,
128        &ctx.should_interrupt,
129        gix::protocol::fetch::Context {
130            handshake: &mut handshake,
131            transport: &mut transport.inner,
132            user_agent,
133            trace_packetlines,
134        },
135        gix::protocol::fetch::Options {
136            shallow_file: "no shallow file required as we reject it to keep it simple".into(),
137            shallow: &Default::default(),
138            tags: Default::default(),
139            reject_shallow_remote: true,
140        },
141    )
142    .await?;
143    Ok(())
144}
145
146struct Negotiate<'a> {
147    refmap: &'a gix::protocol::fetch::RefMap,
148}
149
150impl gix::protocol::fetch::Negotiate for Negotiate<'_> {
151    fn mark_complete_and_common_ref(&mut self) -> Result<negotiate::Action, negotiate::Error> {
152        Ok(negotiate::Action::MustNegotiate {
153            remote_ref_target_known: vec![], /* we don't really negotiate */
154        })
155    }
156
157    fn add_wants(&mut self, arguments: &mut Arguments, _remote_ref_target_known: &[bool]) -> bool {
158        let mut has_want = false;
159        for id in self.refmap.mappings.iter().filter_map(|m| m.remote.as_id()) {
160            arguments.want(id);
161            has_want = true;
162        }
163        has_want
164    }
165
166    fn one_round(
167        &mut self,
168        _state: &mut negotiate::one_round::State,
169        _arguments: &mut Arguments,
170        _previous_response: Option<&Response>,
171    ) -> Result<(negotiate::Round, bool), negotiate::Error> {
172        Ok((
173            negotiate::Round {
174                haves_sent: 0,
175                in_vain: 0,
176                haves_to_send: 0,
177                previous_response_had_at_least_one_in_common: false,
178            },
179            // is done
180            true,
181        ))
182    }
183}
184
185#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
186pub struct JsonBundleWriteOutcome {
187    pub index_version: pack::index::Version,
188    pub index_hash: String,
189
190    pub data_hash: String,
191    pub num_objects: u32,
192}
193
194impl From<pack::index::write::Outcome> for JsonBundleWriteOutcome {
195    fn from(v: pack::index::write::Outcome) -> Self {
196        JsonBundleWriteOutcome {
197            index_version: v.index_version,
198            num_objects: v.num_objects,
199            data_hash: v.data_hash.to_string(),
200            index_hash: v.index_hash.to_string(),
201        }
202    }
203}
204
205#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
206pub struct JsonOutcome {
207    pub index: JsonBundleWriteOutcome,
208    pub pack_kind: pack::data::Version,
209
210    pub index_path: Option<PathBuf>,
211    pub data_path: Option<PathBuf>,
212
213    pub refs: Vec<crate::repository::remote::JsonRef>,
214}
215
216impl JsonOutcome {
217    pub fn from_outcome_and_refs(v: pack::bundle::write::Outcome, refs: &[Ref]) -> Self {
218        JsonOutcome {
219            index: v.index.into(),
220            pack_kind: v.pack_version,
221            index_path: v.index_path,
222            data_path: v.data_path,
223            refs: refs.iter().cloned().map(Into::into).collect(),
224        }
225    }
226}
227
228fn print_hash_and_path(out: &mut impl io::Write, name: &str, id: ObjectId, path: Option<PathBuf>) -> io::Result<()> {
229    match path {
230        Some(path) => writeln!(out, "{}: {} ({})", name, id, path.display()),
231        None => writeln!(out, "{name}: {id}"),
232    }
233}
234
235fn print(out: &mut impl io::Write, res: pack::bundle::write::Outcome, refs: &[Ref]) -> io::Result<()> {
236    print_hash_and_path(out, "index", res.index.index_hash, res.index_path)?;
237    print_hash_and_path(out, "pack", res.index.data_hash, res.data_path)?;
238    writeln!(out)?;
239    crate::repository::remote::refs::print(out, refs)?;
240    Ok(())
241}
242
243fn write_raw_refs(refs: &[Ref], directory: PathBuf) -> std::io::Result<()> {
244    let assure_dir_exists = |path: &BString| {
245        assert!(!path.starts_with_str("/"), "no ref start with a /, they are relative");
246        let path = directory.join(gix::path::from_byte_slice(path));
247        std::fs::create_dir_all(path.parent().expect("multi-component path")).map(|_| path)
248    };
249    for r in refs {
250        let (path, content) = match r {
251            Ref::Unborn { full_ref_name, target } => {
252                (assure_dir_exists(full_ref_name)?, format!("unborn HEAD: {target}"))
253            }
254            Ref::Symbolic {
255                full_ref_name: path,
256                target,
257                ..
258            } => (assure_dir_exists(path)?, format!("ref: {target}")),
259            Ref::Peeled {
260                full_ref_name: path,
261                tag: object,
262                ..
263            }
264            | Ref::Direct {
265                full_ref_name: path,
266                object,
267            } => (assure_dir_exists(path)?, object.to_string()),
268        };
269        std::fs::write(path, content.as_bytes())?;
270    }
271    Ok(())
272}
273
274#[allow(clippy::too_many_arguments)]
275fn receive_pack_blocking(
276    mut directory: Option<PathBuf>,
277    mut refs_directory: Option<PathBuf>,
278    mut input: impl io::BufRead,
279    progress: &mut dyn DynNestedProgress,
280    refs: &[Ref],
281    should_interrupt: &AtomicBool,
282    mut out: impl std::io::Write,
283    thread_limit: Option<usize>,
284    object_hash: gix::hash::Kind,
285    format: OutputFormat,
286) -> io::Result<()> {
287    let options = pack::bundle::write::Options {
288        thread_limit,
289        index_version: pack::index::Version::V2,
290        iteration_mode: pack::data::input::Mode::Verify,
291        object_hash,
292    };
293    let outcome = pack::Bundle::write_to_directory(
294        &mut input,
295        directory.take().as_deref(),
296        progress,
297        should_interrupt,
298        None::<gix::objs::find::Never>,
299        options,
300    )
301    .map_err(io::Error::other)?;
302
303    if let Some(directory) = refs_directory.take() {
304        write_raw_refs(refs, directory)?;
305    }
306
307    match format {
308        OutputFormat::Human => drop(print(&mut out, outcome, refs)),
309        #[cfg(feature = "serde")]
310        OutputFormat::Json => {
311            serde_json::to_writer_pretty(&mut out, &JsonOutcome::from_outcome_and_refs(outcome, refs))?;
312        }
313    }
314    Ok(())
315}