gitoxide_core/pack/
receive.rs

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