gitoxide_core/pack/
receive.rs

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