Now let's take a silly one
1

Configure Feed

Select the types of activity you want to include in your feed.

at main 25 kB View raw
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}