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![], })
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 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}