Now let's take a silly one
1use std::collections::HashSet;
2use std::io::{self, Write};
3use std::time::{Duration, Instant};
4
5use knot_git::{Deepen, Filter, PackBudget, Repo};
6use knot_types::{ObjectFormat, Oid, UnixSeconds};
7
8use crate::error::PackError;
9use crate::objects;
10use crate::pkt;
11use crate::{HaveOids, WantOids};
12
13const AGENT: &[u8] = b"agent=knot2/0\n";
14const SELECTION_MAX_OBJECTS: usize = 5_000_000;
15const SELECTION_TIME_BUDGET: Duration = Duration::from_secs(60);
16const V0_CAPS_BASE: &str = "multi_ack_detailed no-done side-band-64k ofs-delta shallow deepen-since deepen-not filter agent=knot2/0";
17
18fn v0_caps(format: ObjectFormat) -> String {
19 format!("{V0_CAPS_BASE} object-format={}", format.capability())
20}
21
22pub struct StreamOpts {
23 pub side_band: bool,
24 pub sideband_all: bool,
25 pub no_progress: bool,
26 pub thin: bool,
27 pub filter: Filter,
28 pub shallow_commits: Option<Vec<Oid>>,
29 pub packfile_uris: Vec<(Oid, String, String)>,
30 pub emit_packfile_header: bool,
31}
32
33pub enum UploadOutcome {
34 Buffered(Vec<u8>),
35 Streaming {
36 preamble: Vec<u8>,
37 wants: WantOids,
38 haves: HaveOids,
39 opts: StreamOpts,
40 },
41}
42
43fn write_v2_caps(buf: &mut Vec<u8>, format: ObjectFormat) -> Result<(), PackError> {
44 pkt::write_data(buf, b"version 2\n")?;
45 pkt::write_data(buf, AGENT)?;
46 pkt::write_data(buf, b"ls-refs\n")?;
47 pkt::write_data(
48 buf,
49 b"fetch=shallow filter wait-for-done packfile-uris sideband-all\n",
50 )?;
51 pkt::write_data(
52 buf,
53 format!("object-format={}\n", format.capability()).as_bytes(),
54 )?;
55 pkt::write_flush(buf)?;
56 Ok(())
57}
58
59pub fn advertise(repo: &Repo) -> Result<Vec<u8>, PackError> {
60 let mut buf = Vec::new();
61 pkt::write_data(&mut buf, b"# service=git-upload-pack\n")?;
62 pkt::write_flush(&mut buf)?;
63 write_v2_caps(&mut buf, repo.object_format())?;
64 Ok(buf)
65}
66
67pub fn advertise_ssh(repo: &Repo) -> Result<Vec<u8>, PackError> {
68 let mut buf = Vec::new();
69 write_v2_caps(&mut buf, repo.object_format())?;
70 Ok(buf)
71}
72
73pub fn advertise_v0(repo: &Repo) -> Result<Vec<u8>, PackError> {
74 let mut buf = Vec::new();
75 pkt::write_data(&mut buf, b"# service=git-upload-pack\n")?;
76 pkt::write_flush(&mut buf)?;
77 write_v0_advert(&mut buf, repo)?;
78 Ok(buf)
79}
80
81pub fn advertise_v0_ssh(repo: &Repo) -> Result<Vec<u8>, PackError> {
82 let mut buf = Vec::new();
83 write_v0_advert(&mut buf, repo)?;
84 Ok(buf)
85}
86
87fn write_v0_advert(buf: &mut Vec<u8>, repo: &Repo) -> Result<(), PackError> {
88 let format = repo.object_format();
89 let caps = v0_caps(format);
90 let refs = repo.advertised_refs_for(knot_git::AdvertScope::Upload)?;
91 match repo.head() {
92 Some(head) => {
93 pkt::write_data(
94 buf,
95 format!("{} HEAD\0{caps} symref=HEAD:{}\n", head.target, head.name).as_bytes(),
96 )?;
97 write_plain_refs(buf, &refs)?;
98 }
99 None => match refs.split_first() {
100 Some((first, rest)) => {
101 pkt::write_data(
102 buf,
103 format!("{} {}\0{caps}\n", first.target, first.name).as_bytes(),
104 )?;
105 write_plain_refs(buf, rest)?;
106 }
107 None => {
108 pkt::write_data(
109 buf,
110 format!("{} capabilities^{{}}\0{caps}\n", format.null_oid()).as_bytes(),
111 )?;
112 }
113 },
114 }
115 pkt::write_flush(buf)?;
116 Ok(())
117}
118
119fn write_plain_refs(buf: &mut Vec<u8>, refs: &[knot_git::RefRecord]) -> Result<(), PackError> {
120 refs.iter().try_for_each(|record| {
121 pkt::write_data(
122 buf,
123 format!("{} {}\n", record.target, record.name).as_bytes(),
124 )
125 .map_err(PackError::from)
126 })
127}
128
129pub(crate) fn fuzz(body: &[u8]) {
130 if let Ok(lines) = pkt::data_payloads_all(body) {
131 let _ = parse_oids(&lines, b"want ");
132 let _ = parse_oids(&lines, b"have ");
133 let _ = first_caps(&lines);
134 let _ = parse_ls_refs_args(&lines);
135 }
136}
137
138pub fn plan(repo: &Repo, body: &[u8]) -> Result<UploadOutcome, PackError> {
139 let peek = pkt::data_payloads(body)?;
140 match peek.first() {
141 Some(line) if line.starts_with(b"command=") => plan_v2(repo, &peek),
142 _ => plan_v0(repo, body),
143 }
144}
145
146pub fn buffered(repo: &Repo, body: &[u8]) -> Result<Vec<u8>, PackError> {
147 let mut out = Vec::new();
148 streamed(repo, body, &mut |chunk| {
149 out.extend_from_slice(chunk);
150 Ok(())
151 })?;
152 Ok(out)
153}
154
155pub fn streamed(
156 repo: &Repo,
157 body: &[u8],
158 sink: &mut dyn FnMut(&[u8]) -> io::Result<()>,
159) -> Result<(), PackError> {
160 match plan(repo, body)? {
161 UploadOutcome::Buffered(bytes) => sink(&bytes).map_err(PackError::from),
162 UploadOutcome::Streaming {
163 preamble,
164 wants,
165 haves,
166 opts,
167 } => {
168 sink(&preamble)?;
169 stream_pack(repo, &wants, &haves, &opts, sink)?;
170 if opts.side_band {
171 let mut flush = Vec::new();
172 pkt::write_flush(&mut flush)?;
173 sink(&flush)?;
174 }
175 Ok(())
176 }
177 }
178}
179
180fn plan_v2(repo: &Repo, lines: &[&[u8]]) -> Result<UploadOutcome, PackError> {
181 match lines.first().copied().unwrap_or_default() {
182 command if command.starts_with(b"command=ls-refs") => {
183 Ok(UploadOutcome::Buffered(ls_refs(repo, lines)?))
184 }
185 command if command.starts_with(b"command=fetch") => plan_v2_fetch(repo, lines),
186 _ => Err(PackError::Protocol(
187 "unsupported protocol v2 command".to_string(),
188 )),
189 }
190}
191
192struct LsRefsArgs {
193 symrefs: bool,
194 peel: bool,
195 prefixes: Vec<String>,
196}
197
198fn parse_ls_refs_args(lines: &[&[u8]]) -> LsRefsArgs {
199 LsRefsArgs {
200 symrefs: lines.iter().any(|line| line.starts_with(b"symrefs")),
201 peel: lines.iter().any(|line| line.starts_with(b"peel")),
202 prefixes: lines
203 .iter()
204 .filter_map(|line| {
205 std::str::from_utf8(line)
206 .ok()?
207 .trim_end()
208 .strip_prefix("ref-prefix ")
209 .map(str::to_string)
210 })
211 .collect(),
212 }
213}
214
215pub(crate) fn matches_prefix<S: AsRef<str>>(name: &str, prefixes: &[S]) -> bool {
216 prefixes.is_empty()
217 || prefixes
218 .iter()
219 .any(|prefix| name.starts_with(prefix.as_ref()))
220}
221
222fn ls_refs(repo: &Repo, lines: &[&[u8]]) -> Result<Vec<u8>, PackError> {
223 let args = parse_ls_refs_args(lines);
224 let mut buf = Vec::new();
225 if let Some(head) = repo.head()
226 && matches_prefix("HEAD", &args.prefixes)
227 {
228 let mut line = format!("{} HEAD", head.target);
229 if args.symrefs {
230 line.push_str(&format!(" symref-target:{}", head.name));
231 }
232 line.push('\n');
233 pkt::write_data(&mut buf, line.as_bytes())?;
234 }
235 repo.advertised_refs_for(knot_git::AdvertScope::Upload)?
236 .iter()
237 .filter(|record| matches_prefix(record.name.as_str(), &args.prefixes))
238 .try_fold(&mut buf, |buf, record| {
239 let mut line = format!("{} {}", record.target, record.name);
240 if args.peel
241 && let Some(peeled) = repo.peeled_target(record.target)?
242 {
243 line.push_str(&format!(" peeled:{peeled}"));
244 }
245 line.push('\n');
246 pkt::write_data(buf, line.as_bytes())?;
247 Ok::<_, PackError>(buf)
248 })?;
249 pkt::write_flush(&mut buf)?;
250 Ok(buf)
251}
252
253fn plan_v2_fetch(repo: &Repo, lines: &[&[u8]]) -> Result<UploadOutcome, PackError> {
254 let wants = WantOids::new(parse_oids(lines, b"want "));
255 ensure_wanted(repo, &wants)?;
256 let haves = HaveOids::new(parse_oids(lines, b"have "));
257 let done = lines.iter().any(|line| line.starts_with(b"done"));
258 let wait_for_done = lines.iter().any(|line| line.starts_with(b"wait-for-done"));
259 let sideband_all = lines.iter().any(|line| line.starts_with(b"sideband-all"));
260 let no_progress = lines.iter().any(|line| line.starts_with(b"no-progress"));
261 let thin = lines.iter().any(|line| line.starts_with(b"thin-pack"));
262 let filter = parse_filter(lines)?;
263 let deepen = parse_deepen(repo, lines)?;
264 let client_shallow = parse_oids(lines, b"shallow ");
265 let common: HaveOids = haves
266 .iter()
267 .copied()
268 .filter(|oid| repo.contains(*oid))
269 .collect();
270
271 let mut preamble = Vec::new();
272 if !haves.is_empty() && !done {
273 seg(&mut preamble, sideband_all, b"acknowledgments\n")?;
274 if common.is_empty() {
275 seg(&mut preamble, sideband_all, b"NAK\n")?;
276 pkt::write_flush(&mut preamble)?;
277 return Ok(UploadOutcome::Buffered(preamble));
278 }
279 common.iter().try_for_each(|oid| {
280 seg(
281 &mut preamble,
282 sideband_all,
283 format!("ACK {oid}\n").as_bytes(),
284 )
285 })?;
286 if wait_for_done {
287 pkt::write_flush(&mut preamble)?;
288 return Ok(UploadOutcome::Buffered(preamble));
289 }
290 seg(&mut preamble, sideband_all, b"ready\n")?;
291 pkt::write_delim(&mut preamble)?;
292 }
293
294 let shallow_commits = if deepen.is_shallow_request() || repo.is_shallow() {
295 let plan = repo.shallow_walk(wants.as_slice(), &deepen, &client_shallow)?;
296 seg(&mut preamble, sideband_all, b"shallow-info\n")?;
297 plan.shallow.iter().try_for_each(|oid| {
298 seg(
299 &mut preamble,
300 sideband_all,
301 format!("shallow {oid}\n").as_bytes(),
302 )
303 })?;
304 plan.unshallow.iter().try_for_each(|oid| {
305 seg(
306 &mut preamble,
307 sideband_all,
308 format!("unshallow {oid}\n").as_bytes(),
309 )
310 })?;
311 pkt::write_delim(&mut preamble)?;
312 Some(plan.commits)
313 } else {
314 None
315 };
316
317 Ok(UploadOutcome::Streaming {
318 preamble,
319 wants,
320 haves: common,
321 opts: StreamOpts {
322 side_band: true,
323 sideband_all,
324 no_progress,
325 thin,
326 filter,
327 shallow_commits,
328 packfile_uris: packfile_uri_candidates(repo, lines),
329 emit_packfile_header: true,
330 },
331 })
332}
333
334fn seg(buf: &mut Vec<u8>, sideband_all: bool, content: &[u8]) -> io::Result<()> {
335 if sideband_all {
336 pkt::write_band(buf, content)
337 } else {
338 pkt::write_data(buf, content)
339 }
340}
341
342fn packfile_uri_candidates(repo: &Repo, lines: &[&[u8]]) -> Vec<(Oid, String, String)> {
343 let Some(protocols) = lines.iter().find_map(|line| {
344 std::str::from_utf8(line)
345 .ok()?
346 .trim_end()
347 .strip_prefix("packfile-uris ")
348 .map(str::to_string)
349 }) else {
350 return Vec::new();
351 };
352 let allowed: Vec<&str> = protocols.split(',').map(str::trim).collect();
353 repo.blob_packfile_uris()
354 .into_iter()
355 .filter(|(_, _, uri)| {
356 uri.split_once("://")
357 .is_some_and(|(scheme, _)| allowed.contains(&scheme))
358 })
359 .collect()
360}
361
362fn parse_filter(lines: &[&[u8]]) -> Result<Filter, PackError> {
363 let spec = lines.iter().find_map(|line| {
364 std::str::from_utf8(line)
365 .ok()?
366 .trim_end()
367 .strip_prefix("filter ")
368 });
369 match spec {
370 None => Ok(Filter::None),
371 Some("blob:none") => Ok(Filter::BlobNone),
372 Some(rest) if rest.starts_with("blob:limit=") => parse_size(&rest["blob:limit=".len()..])
373 .map(Filter::BlobLimit)
374 .ok_or_else(|| PackError::Protocol(format!("bad blob:limit filter: {rest}"))),
375 Some(rest) if rest.starts_with("tree:") => rest["tree:".len()..]
376 .parse::<u32>()
377 .map(Filter::TreeDepth)
378 .map_err(|_| PackError::Protocol(format!("bad tree filter: {rest}"))),
379 Some(other) => Err(PackError::Protocol(format!("unsupported filter: {other}"))),
380 }
381}
382
383fn parse_size(text: &str) -> Option<u64> {
384 let (digits, scale) = match text.chars().last() {
385 Some('k') | Some('K') => (&text[..text.len() - 1], 1024),
386 Some('m') | Some('M') => (&text[..text.len() - 1], 1024 * 1024),
387 Some('g') | Some('G') => (&text[..text.len() - 1], 1024 * 1024 * 1024),
388 _ => (text, 1),
389 };
390 digits.parse::<u64>().ok().map(|value| value * scale)
391}
392
393fn parse_deepen(repo: &Repo, lines: &[&[u8]]) -> Result<Deepen, PackError> {
394 let value = |prefix: &str| -> Option<&str> {
395 lines.iter().find_map(|line| {
396 std::str::from_utf8(line)
397 .ok()?
398 .trim_end()
399 .strip_prefix(prefix)
400 })
401 };
402 let depth = value("deepen ").and_then(|text| text.parse::<u32>().ok());
403 let since = value("deepen-since ")
404 .and_then(|text| text.parse::<i64>().ok())
405 .map(UnixSeconds::new);
406 let relative = lines
407 .iter()
408 .any(|line| line.starts_with(b"deepen-relative"));
409 let not = lines
410 .iter()
411 .filter_map(|line| {
412 std::str::from_utf8(line)
413 .ok()?
414 .trim_end()
415 .strip_prefix("deepen-not ")
416 })
417 .map(|spec| resolve_commitish(repo, spec))
418 .collect::<Result<Vec<_>, _>>()?;
419 Ok(Deepen {
420 depth,
421 since,
422 not,
423 relative,
424 })
425}
426
427fn resolve_commitish(repo: &Repo, spec: &str) -> Result<Oid, PackError> {
428 if let Ok(oid) = Oid::from_hex(spec)
429 && repo.contains(oid)
430 {
431 return Ok(oid);
432 }
433 repo.git()
434 .rev_parse_single(spec.as_bytes())
435 .map(|id| Oid::from(id.detach()))
436 .map_err(|error| PackError::Protocol(format!("deepen-not {spec}: {error}")))
437}
438
439fn plan_v0(repo: &Repo, body: &[u8]) -> Result<UploadOutcome, PackError> {
440 let lines = pkt::data_payloads_all(body)?;
441 let wants = WantOids::new(parse_oids(&lines, b"want "));
442 ensure_wanted(repo, &wants)?;
443 let haves = HaveOids::new(parse_oids(&lines, b"have "));
444 let done = lines.iter().any(|line| line.starts_with(b"done"));
445 let caps = first_caps(&lines);
446 let side_band = caps
447 .map(|caps| {
448 caps.split(' ')
449 .any(|cap| cap == "side-band-64k" || cap == "side-band")
450 })
451 .unwrap_or(false);
452 let no_progress = caps
453 .map(|caps| caps.split(' ').any(|cap| cap == "no-progress"))
454 .unwrap_or(false);
455 let thin = caps
456 .map(|caps| caps.split(' ').any(|cap| cap == "thin-pack"))
457 .unwrap_or(false);
458 let multi_ack_detailed = caps
459 .map(|caps| caps.split(' ').any(|cap| cap == "multi_ack_detailed"))
460 .unwrap_or(false);
461 let no_done = caps
462 .map(|caps| caps.split(' ').any(|cap| cap == "no-done"))
463 .unwrap_or(false);
464 let filter = parse_filter(&lines)?;
465 let deepen = parse_deepen(repo, &lines)?;
466 let client_shallow = parse_oids(&lines, b"shallow ");
467 let common: HaveOids = haves
468 .iter()
469 .copied()
470 .filter(|oid| repo.contains(*oid))
471 .collect();
472
473 let mut preamble = Vec::new();
474 let shallow_commits = if deepen.is_shallow_request() || repo.is_shallow() {
475 let plan = repo.shallow_walk(wants.as_slice(), &deepen, &client_shallow)?;
476 plan.shallow.iter().try_for_each(|oid| {
477 pkt::write_data(&mut preamble, format!("shallow {oid}\n").as_bytes())
478 })?;
479 plan.unshallow.iter().try_for_each(|oid| {
480 pkt::write_data(&mut preamble, format!("unshallow {oid}\n").as_bytes())
481 })?;
482 pkt::write_flush(&mut preamble)?;
483 Some(plan.commits)
484 } else {
485 None
486 };
487
488 if haves.is_empty() && !done {
489 if !(deepen.is_shallow_request() || repo.is_shallow()) {
490 pkt::write_data(&mut preamble, b"NAK\n")?;
491 }
492 return Ok(UploadOutcome::Buffered(preamble));
493 }
494
495 let ready = multi_ack_detailed
496 && !done
497 && !common.is_empty()
498 && common.len() == haves.len()
499 && repo.wants_satisfied_by(wants.as_slice(), common.as_slice())?;
500
501 let stream = move |preamble: Vec<u8>, common: HaveOids| UploadOutcome::Streaming {
502 preamble,
503 wants,
504 haves: common,
505 opts: StreamOpts {
506 side_band,
507 sideband_all: false,
508 no_progress,
509 thin,
510 filter,
511 shallow_commits,
512 packfile_uris: Vec::new(),
513 emit_packfile_header: false,
514 },
515 };
516
517 if multi_ack_detailed {
518 common.iter().try_for_each(|oid| {
519 pkt::write_data(&mut preamble, format!("ACK {oid} common\n").as_bytes())
520 })?;
521 let last = common.as_slice().last().copied();
522 if done {
523 match last {
524 Some(oid) => {
525 pkt::write_data(&mut preamble, format!("ACK {oid}\n").as_bytes())?;
526 }
527 None => pkt::write_data(&mut preamble, b"NAK\n")?,
528 }
529 return Ok(stream(preamble, common));
530 }
531 match (ready, last) {
532 (true, Some(oid)) => {
533 pkt::write_data(&mut preamble, format!("ACK {oid} ready\n").as_bytes())?;
534 pkt::write_data(&mut preamble, b"NAK\n")?;
535 if no_done {
536 pkt::write_data(&mut preamble, format!("ACK {oid}\n").as_bytes())?;
537 return Ok(stream(preamble, common));
538 }
539 }
540 _ => pkt::write_data(&mut preamble, b"NAK\n")?,
541 }
542 return Ok(UploadOutcome::Buffered(preamble));
543 }
544
545 match common.as_slice().first() {
546 Some(oid) => pkt::write_data(&mut preamble, format!("ACK {oid}\n").as_bytes())?,
547 None => pkt::write_data(&mut preamble, b"NAK\n")?,
548 }
549 match done {
550 true => Ok(stream(preamble, common)),
551 false => Ok(UploadOutcome::Buffered(preamble)),
552 }
553}
554
555pub fn stream_pack(
556 repo: &Repo,
557 wants: &WantOids,
558 haves: &HaveOids,
559 opts: &StreamOpts,
560 sink: &mut dyn FnMut(&[u8]) -> io::Result<()>,
561) -> Result<(), PackError> {
562 let progress = opts.side_band && !opts.no_progress;
563 if opts.shallow_commits.is_none()
564 && haves.is_empty()
565 && opts.filter == Filter::None
566 && opts.packfile_uris.is_empty()
567 {
568 write_packfile_header(opts, sink)?;
569 return stream_full_clone(repo, wants.as_slice(), opts, progress, sink);
570 }
571 let budget = selection_budget();
572 let mut selection = match &opts.shallow_commits {
573 Some(commits) => repo.select_shallow_objects(
574 wants.as_slice(),
575 commits,
576 haves.as_slice(),
577 opts.filter,
578 budget,
579 )?,
580 None => repo.select_pack_objects_filtered(
581 wants.as_slice(),
582 haves.as_slice(),
583 opts.filter,
584 budget,
585 )?,
586 };
587 if !opts.packfile_uris.is_empty() {
588 offload_packfile_uris(
589 &mut selection.send,
590 &opts.packfile_uris,
591 opts.sideband_all,
592 sink,
593 )?;
594 }
595 write_packfile_header(opts, sink)?;
596 if progress {
597 let mut buf = Vec::new();
598 pkt::write_band_progress(
599 &mut buf,
600 format!("Enumerating objects: {}, done.\n", selection.send.len()).as_bytes(),
601 )?;
602 sink(&buf)?;
603 }
604 let count = selection.send.len();
605 {
606 let mut pack_sink = PackSink {
607 side_band: opts.side_band,
608 sink: &mut *sink,
609 };
610 let thin_bases = opts.thin.then_some(&selection.client_has);
611 objects::write_pack(
612 &repo.objects_dir(),
613 selection.send,
614 thin_bases,
615 &mut pack_sink,
616 repo.object_format().kind(),
617 )?;
618 }
619 if progress {
620 let mut buf = Vec::new();
621 pkt::write_band_progress(&mut buf, format!("Total {count}, done.\n").as_bytes())?;
622 sink(&buf)?;
623 }
624 Ok(())
625}
626
627fn write_packfile_header(
628 opts: &StreamOpts,
629 sink: &mut dyn FnMut(&[u8]) -> io::Result<()>,
630) -> Result<(), PackError> {
631 if !opts.emit_packfile_header {
632 return Ok(());
633 }
634 let mut buf = Vec::new();
635 seg(&mut buf, opts.sideband_all, b"packfile\n")?;
636 sink(&buf)?;
637 Ok(())
638}
639
640fn offload_packfile_uris(
641 send: &mut Vec<Oid>,
642 candidates: &[(Oid, String, String)],
643 sideband_all: bool,
644 sink: &mut dyn FnMut(&[u8]) -> io::Result<()>,
645) -> Result<(), PackError> {
646 let present: std::collections::HashSet<Oid> = send.iter().copied().collect();
647 let kept: Vec<&(Oid, String, String)> = candidates
648 .iter()
649 .filter(|(oid, _, _)| present.contains(oid))
650 .collect();
651 if kept.is_empty() {
652 return Ok(());
653 }
654 let excluded: std::collections::HashSet<Oid> = kept.iter().map(|(oid, _, _)| *oid).collect();
655 send.retain(|oid| !excluded.contains(oid));
656 let mut buf = Vec::new();
657 seg(&mut buf, sideband_all, b"packfile-uris\n")?;
658 kept.iter().try_for_each(|(_, packh, uri)| {
659 seg(
660 &mut buf,
661 sideband_all,
662 format!("{packh} {uri}\n").as_bytes(),
663 )
664 })?;
665 pkt::write_delim(&mut buf)?;
666 sink(&buf)?;
667 Ok(())
668}
669
670fn stream_full_clone(
671 repo: &Repo,
672 wants: &[Oid],
673 opts: &StreamOpts,
674 progress: bool,
675 sink: &mut dyn FnMut(&[u8]) -> io::Result<()>,
676) -> Result<(), PackError> {
677 let deadline = Instant::now() + SELECTION_TIME_BUDGET;
678 let roots = repo.clone_roots(wants, PackBudget::new(SELECTION_MAX_OBJECTS, deadline))?;
679 let pack = objects::count_expanded(
680 &repo.objects_dir(),
681 roots,
682 SELECTION_MAX_OBJECTS,
683 deadline,
684 repo.object_format().kind(),
685 )?;
686 let count = pack.len();
687 if progress {
688 let mut buf = Vec::new();
689 pkt::write_band_progress(
690 &mut buf,
691 format!("Enumerating objects: {count}, done.\n").as_bytes(),
692 )?;
693 sink(&buf)?;
694 }
695 {
696 let mut pack_sink = PackSink {
697 side_band: opts.side_band,
698 sink: &mut *sink,
699 };
700 objects::write_expanded(pack, &mut pack_sink)?;
701 }
702 if progress {
703 let mut buf = Vec::new();
704 pkt::write_band_progress(&mut buf, format!("Total {count}, done.\n").as_bytes())?;
705 sink(&buf)?;
706 }
707 Ok(())
708}
709
710struct PackSink<'a> {
711 side_band: bool,
712 sink: &'a mut dyn FnMut(&[u8]) -> io::Result<()>,
713}
714
715impl Write for PackSink<'_> {
716 fn write(&mut self, data: &[u8]) -> io::Result<usize> {
717 if self.side_band {
718 data.chunks(pkt::MAX_BAND).try_for_each(|chunk| {
719 let mut framed = Vec::with_capacity(chunk.len() + 5);
720 pkt::write_band(&mut framed, chunk)?;
721 (self.sink)(&framed)
722 })?;
723 } else {
724 (self.sink)(data)?;
725 }
726 Ok(data.len())
727 }
728
729 fn flush(&mut self) -> io::Result<()> {
730 Ok(())
731 }
732}
733
734pub fn selection_budget() -> PackBudget {
735 PackBudget::new(
736 SELECTION_MAX_OBJECTS,
737 Instant::now() + SELECTION_TIME_BUDGET,
738 )
739}
740
741fn ensure_wanted(repo: &Repo, wants: &WantOids) -> Result<(), PackError> {
742 let tips: Vec<Oid> = repo
743 .advertised_refs()?
744 .iter()
745 .map(|record| record.target)
746 .collect();
747 let advertised: HashSet<Oid> = tips.iter().copied().collect();
748 if wants.iter().all(|want| advertised.contains(want)) {
749 return Ok(());
750 }
751 let commit_closure = repo.reachable_commits(&tips, selection_budget())?;
752 let unresolved: Vec<Oid> = wants
753 .iter()
754 .copied()
755 .filter(|want| !advertised.contains(want) && !commit_closure.contains(want))
756 .collect();
757 if unresolved.is_empty() {
758 return Ok(());
759 }
760 let reachable: HashSet<Oid> = repo
761 .select_pack_objects_filtered(&tips, &[], Filter::None, selection_budget())?
762 .send
763 .into_iter()
764 .collect();
765 match unresolved.iter().find(|want| !reachable.contains(want)) {
766 Some(hidden) => Err(PackError::Protocol(format!(
767 "want {hidden} is not reachable from public ref"
768 ))),
769 None => Ok(()),
770 }
771}
772
773fn first_caps<'a>(lines: &[&'a [u8]]) -> Option<&'a str> {
774 let line = lines.iter().find(|line| line.starts_with(b"want "))?;
775 let text = std::str::from_utf8(line).ok()?.trim_end();
776 text.strip_prefix("want ")?
777 .split_once(' ')
778 .map(|(_oid, caps)| caps)
779}
780
781fn parse_oids(lines: &[&[u8]], prefix: &[u8]) -> Vec<Oid> {
782 lines
783 .iter()
784 .filter_map(|line| line.strip_prefix(prefix))
785 .filter_map(|rest| {
786 let hex = rest.split(|byte| *byte == b' ' || *byte == b'\n').next()?;
787 let hex = std::str::from_utf8(hex).ok()?;
788 Oid::from_hex(hex).ok()
789 })
790 .collect()
791}