1use std::collections::{HashMap, HashSet, VecDeque};
9use std::fs::File;
10use std::io::{BufRead, Read, Seek, Write};
11use std::path::{Path, PathBuf};
12
13use thiserror::Error;
14
15use crate::commit_pretty::message_subject;
16use crate::connectivity::bundle_prerequisites_connected_to_refs;
17use crate::error::{Error, Result};
18use crate::index_pack::{ingest_received_pack_path_with_progress, IngestPackOptions};
19use crate::objects::{parse_commit, HashAlgo, ObjectId, ObjectKind};
20use crate::odb::Odb;
21use crate::pack_objects::{write_upload_pack_stream, PackStats};
22use crate::progress::{NullProgress, ProgressPhase, ProgressSink, ProgressUnit};
23use crate::repo::Repository;
24use crate::rev_list::{
25 FilterObjectKind, MissingAction, ObjectFilter, RevListOptions, RevListResult,
26};
27use crate::transfer::{build_pack_from_send_list, reachable_closure, PackBuildOptions};
28
29const V2_SIGNATURE: &str = "# v2 git bundle\n";
30const V3_SIGNATURE: &str = "# v3 git bundle\n";
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum BundleVersion {
35 V2,
37 V3,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct FilterSpec(ObjectFilter);
44
45impl FilterSpec {
46 pub fn parse(spec: &str) -> std::result::Result<Self, BundleError> {
52 ObjectFilter::parse(spec)
53 .map(Self)
54 .map_err(BundleError::MalformedLine)
55 }
56
57 #[must_use]
59 pub fn as_filter(&self) -> &ObjectFilter {
60 &self.0
61 }
62
63 #[must_use]
65 pub fn to_capability_value(&self) -> String {
66 filter_to_capability_value(&self.0)
67 }
68}
69
70#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct BundleHeader {
73 pub version: BundleVersion,
75 pub object_format: HashAlgo,
77 pub filter: Option<FilterSpec>,
79 pub prerequisites: Vec<(ObjectId, Option<String>)>,
81 pub refs: Vec<(String, ObjectId)>,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct UnbundledRef {
88 pub name: String,
90 pub oid: ObjectId,
92}
93
94#[derive(Debug, Clone, PartialEq, Eq)]
96pub struct UnbundleOutcome {
97 pub refs: Vec<UnbundledRef>,
99 pub transfer: crate::transfer::TransferStats,
101 pub pack_object_ids: HashSet<ObjectId>,
103}
104
105#[derive(Debug, Clone, Default, PartialEq, Eq)]
107pub struct BundleVerifyReport {
108 pub missing_prerequisites: Vec<(ObjectId, Option<String>)>,
110 pub prerequisites_not_connected: bool,
112}
113
114impl BundleVerifyReport {
115 #[must_use]
117 pub fn is_ok(&self) -> bool {
118 self.missing_prerequisites.is_empty() && !self.prerequisites_not_connected
119 }
120
121 pub fn into_result(self) -> std::result::Result<(), BundleError> {
129 if let Some((oid, subject)) = self.missing_prerequisites.into_iter().next() {
130 return Err(BundleError::MissingPrerequisite { oid, subject });
131 }
132 if self.prerequisites_not_connected {
133 return Err(BundleError::PrerequisitesNotConnected);
134 }
135 Ok(())
136 }
137}
138
139#[non_exhaustive]
141#[derive(Debug, Error, Clone, PartialEq, Eq)]
142pub enum BundleError {
143 #[error("not a v2 or v3 git bundle")]
145 BadSignature,
146 #[error("unsupported bundle version")]
148 UnsupportedVersion,
149 #[error("unknown bundle capability: {0}")]
151 UnsupportedCapability(String),
152 #[error("malformed bundle header: {0}")]
154 MalformedLine(String),
155 #[error("bundle object format {bundle_name} does not match repository {repo_name}")]
157 ObjectFormatMismatch {
158 bundle_name: &'static str,
160 repo_name: &'static str,
162 },
163 #[error("missing prerequisite commit {oid}")]
165 MissingPrerequisite {
166 oid: ObjectId,
168 subject: Option<String>,
170 },
171 #[error("prerequisite commits are not connected to local refs")]
173 PrerequisitesNotConnected,
174 #[error("bundle contains no refs")]
176 EmptyRefs,
177 #[error("bundle pack stream missing or invalid")]
179 MissingPack,
180 #[error("{0}")]
182 Io(String),
183 #[error("operation cancelled")]
185 Cancelled,
186}
187
188impl From<std::io::Error> for BundleError {
189 fn from(e: std::io::Error) -> Self {
190 Self::Io(e.to_string())
191 }
192}
193
194fn io_open_path(path: &Path, source: std::io::Error) -> BundleError {
195 BundleError::Io(format!("could not open '{}': {source}", path.display()))
196}
197
198impl BundleError {
199 #[must_use]
201 pub fn is_cancelled(&self) -> bool {
202 matches!(self, Self::Cancelled)
203 }
204}
205
206#[derive(Debug, Clone, Default)]
208pub struct BundleSpec {
209 pub include: Vec<(ObjectId, String)>,
211 pub exclude: Vec<ObjectId>,
213 pub filter: Option<FilterSpec>,
215}
216
217#[derive(Debug, Clone, PartialEq, Eq)]
219pub struct BundleRefEntry {
220 pub name: String,
222 pub oid: ObjectId,
224}
225
226#[derive(Debug, Clone, PartialEq, Eq)]
228pub struct BundlePrerequisite {
229 pub oid: ObjectId,
231 pub subject: Option<String>,
233}
234
235#[derive(Debug, Clone, PartialEq, Eq)]
237pub struct BundleCreatePlan {
238 pub refs: Vec<BundleRefEntry>,
240 pub prerequisites: Vec<BundlePrerequisite>,
242 pub object_count: u32,
244}
245
246#[derive(Debug, Clone, PartialEq, Eq)]
248pub struct BundleRefUpdatePlan {
249 pub name: String,
251 pub old_oid: Option<ObjectId>,
253 pub new_oid: ObjectId,
255}
256
257#[derive(Debug, Clone, PartialEq, Eq)]
259pub struct BundleUnbundlePlan {
260 pub verify: BundleVerifyReport,
262 pub ref_updates: Vec<BundleRefUpdatePlan>,
264 pub object_count: u32,
266}
267
268#[derive(Debug, Clone)]
270pub struct Bundle {
271 path: PathBuf,
272 header: BundleHeader,
273 pack_offset: u64,
274}
275
276impl Bundle {
277 pub fn open(path: impl AsRef<Path>) -> std::result::Result<Self, BundleError> {
283 let path = path.as_ref().to_path_buf();
284 let (header, pack_offset) = read_header_from_file(&path)?;
285 Ok(Self {
286 path,
287 header,
288 pack_offset,
289 })
290 }
291
292 #[must_use]
294 pub fn header(&self) -> &BundleHeader {
295 &self.header
296 }
297
298 #[must_use]
300 pub fn path(&self) -> &Path {
301 &self.path
302 }
303
304 pub fn pack_object_count(&self) -> std::result::Result<u32, BundleError> {
310 bundle_pack_object_count(self)
311 }
312
313 #[must_use]
315 pub fn pack_byte_offset(&self) -> u64 {
316 self.pack_offset
317 }
318
319 pub fn verify(
325 &self,
326 repo: &Repository,
327 ) -> std::result::Result<BundleVerifyReport, BundleError> {
328 let repo_algo = repo.odb().hash_algo();
329 if self.header.object_format != repo_algo {
330 return Err(BundleError::ObjectFormatMismatch {
331 bundle_name: self.header.object_format.name(),
332 repo_name: repo_algo.name(),
333 });
334 }
335 let mut report = BundleVerifyReport::default();
336 for (oid, subject) in &self.header.prerequisites {
337 if repo.odb().read(oid).is_err() {
338 report.missing_prerequisites.push((*oid, subject.clone()));
339 }
340 }
341 if report.missing_prerequisites.is_empty() {
342 let prereq_oids: Vec<ObjectId> =
343 self.header.prerequisites.iter().map(|(o, _)| *o).collect();
344 let connected = bundle_prerequisites_connected_to_refs(repo, &prereq_oids)
345 .map_err(|e| BundleError::Io(e.to_string()))?;
346 report.prerequisites_not_connected = !connected;
347 }
348 Ok(report)
349 }
350
351 pub fn unbundle(
366 &self,
367 repo: &Repository,
368 ) -> std::result::Result<Vec<UnbundledRef>, BundleError> {
369 Ok(self
370 .unbundle_with_progress(repo, &mut NullProgress, &crate::progress::NeverCancel)?
371 .refs)
372 }
373
374 pub fn unbundle_with_progress(
376 &self,
377 repo: &Repository,
378 progress: &mut dyn ProgressSink,
379 cancel: &dyn crate::progress::Cancel,
380 ) -> std::result::Result<UnbundleOutcome, BundleError> {
381 crate::progress::poll_cancel(progress, cancel).map_err(bundle_ingest_error)?;
382 if self.header.object_format != repo.odb().hash_algo() {
383 return Err(BundleError::ObjectFormatMismatch {
384 bundle_name: self.header.object_format.name(),
385 repo_name: repo.odb().hash_algo().name(),
386 });
387 }
388 let ingested = if repo.odb().uses_files_primary() {
389 self.install_pack(repo, progress, cancel)?
390 } else {
391 let mut input = File::open(&self.path)?;
392 Seek::seek(&mut input, std::io::SeekFrom::Start(self.pack_offset))?;
393 let mut reader = std::io::BufReader::with_capacity(1 << 20, input);
394 crate::unpack_objects::ingest_pack_into_odb(
395 &mut reader,
396 repo.odb(),
397 &crate::unpack_objects::UnpackOptions::default(),
398 Some(progress),
399 Some(cancel),
400 )
401 .map(crate::index_pack::IngestedPack::from)
402 .map_err(bundle_ingest_error)?
403 };
404 if let (Some(_), Some(pack_path)) = (&self.header.filter, &ingested.pack_path) {
405 std::fs::write(pack_path.with_extension("promisor"), b"")
406 .map_err(|e| BundleError::Io(e.to_string()))?;
407 repo.odb().invalidate_packs();
408 }
409 let transfer = crate::transfer::TransferStats::from_ingested_pack(&ingested);
410 let refs = self
411 .header
412 .refs
413 .iter()
414 .map(|(name, oid)| UnbundledRef {
415 name: name.clone(),
416 oid: *oid,
417 })
418 .collect();
419 Ok(UnbundleOutcome {
420 refs,
421 transfer,
422 pack_object_ids: ingested.object_ids,
423 })
424 }
425
426 fn install_pack(
428 &self,
429 repo: &Repository,
430 progress: &mut dyn ProgressSink,
431 cancel: &dyn crate::progress::Cancel,
432 ) -> std::result::Result<crate::index_pack::IngestedPack, BundleError> {
433 let pack_bytes = std::fs::metadata(&self.path)
434 .map(|m| m.len().saturating_sub(self.pack_offset))
435 .unwrap_or(0);
436 progress.start_with_unit(
437 ProgressPhase::ReceivingObjects,
438 ProgressUnit::Bytes,
439 Some(pack_bytes),
440 );
441 let pack_dir = repo.odb().objects_dir().join("pack");
442 std::fs::create_dir_all(&pack_dir).map_err(BundleError::from)?;
443 let tmp = tempfile::NamedTempFile::new_in(&pack_dir)
444 .map_err(|e| BundleError::Io(e.to_string()))?;
445 {
446 let mut input = File::open(&self.path)?;
447 Seek::seek(&mut input, std::io::SeekFrom::Start(self.pack_offset))
448 .map_err(BundleError::from)?;
449 let mut output = std::io::BufWriter::new(tmp.as_file());
450 let mut copied = 0u64;
451 let mut buf = [0u8; 64 * 1024];
452 loop {
453 crate::progress::poll_cancel(progress, cancel).map_err(bundle_ingest_error)?;
454 let n = input.read(&mut buf).map_err(BundleError::from)?;
455 if n == 0 {
456 break;
457 }
458 output.write_all(&buf[..n]).map_err(BundleError::from)?;
459 copied = copied.saturating_add(n as u64);
460 progress.set(copied);
461 }
462 output.flush().map_err(BundleError::from)?;
463 }
464 progress.finish();
465 crate::progress::poll_cancel(progress, cancel).map_err(bundle_ingest_error)?;
466 let opts = IngestPackOptions {
467 fix_thin: true,
468 skip_post_index_verify: true,
471 ..Default::default()
472 };
473 ingest_received_pack_path_with_progress(
474 tmp.path().to_path_buf(),
475 repo.odb(),
476 &opts,
477 Some(progress),
478 Some(cancel),
479 )
480 .map_err(bundle_ingest_error)
481 }
482}
483
484fn bundle_ingest_error(e: Error) -> BundleError {
485 match e {
486 Error::Cancelled => BundleError::Cancelled,
487 other => BundleError::Io(other.to_string()),
488 }
489}
490
491pub fn read_header(reader: &mut dyn BufRead) -> std::result::Result<BundleHeader, BundleError> {
497 let header = read_header_lines(reader)?;
498 ensure_pack_magic(reader)?;
499 Ok(header)
500}
501
502fn read_header_lines(reader: &mut dyn BufRead) -> std::result::Result<BundleHeader, BundleError> {
505 let signature = read_line(reader)?;
506 let version = match signature.as_str() {
507 s if s == V2_SIGNATURE.trim_end() => BundleVersion::V2,
508 s if s == V3_SIGNATURE.trim_end() => BundleVersion::V3,
509 _ => return Err(BundleError::BadSignature),
510 };
511
512 let mut object_format = HashAlgo::Sha1;
513 let mut filter = None;
514 let mut prerequisites = Vec::new();
515 let mut refs = Vec::new();
516
517 loop {
518 let line = read_line(reader)?;
519 if line.is_empty() {
520 break;
521 }
522 if version == BundleVersion::V3 && line.starts_with('@') {
523 parse_capability(&line[1..], &mut object_format, &mut filter)?;
524 continue;
525 }
526 let (is_prereq, rest) = if let Some(stripped) = line.strip_prefix('-') {
527 (true, stripped)
528 } else {
529 (false, line.as_str())
530 };
531 let (oid, tail) = parse_oid_field(rest, object_format)?;
532 if is_prereq {
533 let subject = tail
534 .map(str::trim)
535 .filter(|s| !s.is_empty())
536 .map(str::to_owned);
537 prerequisites.push((oid, subject));
538 } else {
539 let refname = tail.ok_or_else(|| {
540 BundleError::MalformedLine("ref line missing ref name".to_owned())
541 })?;
542 let refname = refname.trim();
543 if refname.is_empty() {
544 return Err(BundleError::MalformedLine(
545 "ref line missing ref name".to_owned(),
546 ));
547 }
548 refs.push((refname.to_owned(), oid));
549 }
550 }
551
552 Ok(BundleHeader {
553 version,
554 object_format,
555 filter,
556 prerequisites,
557 refs,
558 })
559}
560
561const HEADER_READ_BUF: usize = 64 * 1024;
564
565fn read_header_from_file(path: &Path) -> std::result::Result<(BundleHeader, u64), BundleError> {
566 let file = File::open(path).map_err(|e| io_open_path(path, e))?;
567 let mut reader = std::io::BufReader::with_capacity(HEADER_READ_BUF, file);
568 let header = read_header_lines(&mut reader)?;
569 let mut pack_offset = reader.stream_position().map_err(BundleError::from)?;
570 let mut magic = [0u8; 4];
571 loop {
572 read_exact_or_missing_pack(&mut reader, &mut magic[..1])?;
573 if magic[0] != b'\n' && magic[0] != b'\r' {
574 break;
575 }
576 pack_offset += 1;
577 }
578 read_exact_or_missing_pack(&mut reader, &mut magic[1..])?;
579 if &magic != b"PACK" {
580 return Err(BundleError::MissingPack);
581 }
582 Ok((header, pack_offset))
583}
584
585fn read_exact_or_missing_pack(
586 reader: &mut dyn std::io::Read,
587 buf: &mut [u8],
588) -> std::result::Result<(), BundleError> {
589 reader.read_exact(buf).map_err(|e| match e.kind() {
590 std::io::ErrorKind::UnexpectedEof => BundleError::MissingPack,
591 _ => BundleError::from(e),
592 })
593}
594
595fn ensure_pack_magic(reader: &mut dyn BufRead) -> std::result::Result<(), BundleError> {
596 loop {
597 let buf = reader.fill_buf().map_err(BundleError::from)?;
598 if buf.is_empty() {
599 return Err(BundleError::MissingPack);
600 }
601 if buf.starts_with(b"PACK") {
602 return Ok(());
603 }
604 if buf[0] == b'\n' || buf[0] == b'\r' {
605 reader.consume(1);
606 continue;
607 }
608 return Err(BundleError::MissingPack);
609 }
610}
611
612pub fn plan_bundle_create(
618 repo: &Repository,
619 spec: &BundleSpec,
620) -> std::result::Result<BundleCreatePlan, BundleError> {
621 if spec.include.is_empty() {
622 return Err(BundleError::EmptyRefs);
623 }
624 let header = bundle_header_parts(repo, spec)?;
625 let object_count = write_bundle_pack(repo, spec, &mut std::io::sink())?.object_count;
626 Ok(BundleCreatePlan {
627 refs: header
628 .refs
629 .into_iter()
630 .map(|(oid, name)| BundleRefEntry { name, oid })
631 .collect(),
632 prerequisites: header
633 .prerequisites
634 .into_iter()
635 .map(|(oid, subject)| BundlePrerequisite { oid, subject })
636 .collect(),
637 object_count,
638 })
639}
640
641pub fn plan_bundle_unbundle(
647 bundle: &Bundle,
648 repo: &Repository,
649) -> std::result::Result<BundleUnbundlePlan, BundleError> {
650 let verify = bundle.verify(repo)?;
651 let ref_updates = planned_ref_updates_from_header(repo, &bundle.header().refs);
652 let object_count = bundle_pack_object_count(bundle)?;
653 Ok(BundleUnbundlePlan {
654 verify,
655 ref_updates,
656 object_count,
657 })
658}
659
660fn bundle_pack_object_count(bundle: &Bundle) -> std::result::Result<u32, BundleError> {
661 let mut input = File::open(&bundle.path).map_err(BundleError::from)?;
662 Seek::seek(&mut input, std::io::SeekFrom::Start(bundle.pack_offset))
663 .map_err(BundleError::from)?;
664 let mut header = [0u8; 12];
665 read_exact_or_missing_pack(&mut input, &mut header)?;
666 Ok(crate::receive_pack::pack_object_count(&header).unwrap_or(0))
667}
668
669fn planned_ref_updates_from_header(
670 repo: &Repository,
671 refs: &[(String, ObjectId)],
672) -> Vec<BundleRefUpdatePlan> {
673 let git_dir = repo.git_dir();
674 let mut updates = Vec::new();
675 for (name, new_oid) in refs {
676 if name.ends_with("^{}") {
677 continue;
678 }
679 let old_oid = crate::refs::resolve_ref(git_dir, name).ok();
680 if old_oid.as_ref() == Some(new_oid) {
681 continue;
682 }
683 updates.push(BundleRefUpdatePlan {
684 name: name.clone(),
685 old_oid,
686 new_oid: *new_oid,
687 });
688 }
689 updates
690}
691
692struct BundleHeaderParts {
693 prerequisites: Vec<(ObjectId, Option<String>)>,
694 refs: Vec<(ObjectId, String)>,
695}
696
697fn bundle_header_parts(
698 repo: &Repository,
699 spec: &BundleSpec,
700) -> std::result::Result<BundleHeaderParts, BundleError> {
701 let include_oids: Vec<ObjectId> = spec.include.iter().map(|(o, _)| *o).collect();
702 let prerequisites = prerequisite_commits(repo.odb(), &include_oids, &spec.exclude)?;
703 Ok(BundleHeaderParts {
704 prerequisites,
705 refs: spec.include.clone(),
706 })
707}
708
709fn write_bundle_pack(
716 repo: &Repository,
717 spec: &BundleSpec,
718 out: &mut dyn Write,
719) -> std::result::Result<PackStats, BundleError> {
720 let io_err = |e: Error| BundleError::Io(e.to_string());
721 let include_oids: Vec<ObjectId> = spec.include.iter().map(|(o, _)| *o).collect();
722 let mut haves: Vec<ObjectId> = spec
723 .exclude
724 .iter()
725 .map(|oid| peel_to_commit(repo.odb(), *oid))
726 .collect::<Result<Vec<_>>>()
727 .map_err(io_err)?;
728 haves.sort();
729 haves.dedup();
730 let cfg = repo.config().ok();
731 let pack_opts = PackBuildOptions::for_serve(cfg.as_deref(), !haves.is_empty(), true);
732 let no_shallow = HashSet::new();
733 let Some(filter) = spec.filter.as_ref() else {
734 return write_upload_pack_stream(
735 repo,
736 &include_oids,
737 &haves,
738 &no_shallow,
739 &no_shallow,
740 None,
741 &pack_opts,
742 &mut |_| {},
743 out,
744 )
745 .map_err(io_err);
746 };
747 let have_closure = reachable_closure(repo.odb(), &haves, &HashSet::new(), true, &no_shallow)
748 .map_err(io_err)?;
749 let send = filtered_pack_objects(
750 repo,
751 &include_oids,
752 &spec.exclude,
753 filter.as_filter(),
754 &have_closure,
755 )
756 .map_err(io_err)?;
757 let pack = build_pack_from_send_list(
758 repo.odb(),
759 &send,
760 &have_closure,
761 &pack_opts,
762 &HashMap::new(),
763 )
764 .map_err(io_err)?;
765 out.write_all(&pack).map_err(BundleError::from)?;
766 Ok(PackStats {
767 object_count: crate::receive_pack::pack_object_count(&pack).unwrap_or(0),
768 pack_bytes: pack.len() as u64,
769 ..PackStats::default()
770 })
771}
772
773pub fn write_bundle(
786 repo: &Repository,
787 spec: &BundleSpec,
788 out: &mut dyn Write,
789) -> std::result::Result<(), BundleError> {
790 if spec.include.is_empty() {
791 return Err(BundleError::EmptyRefs);
792 }
793 let header = bundle_header_parts(repo, spec)?;
794 write_header(
795 out,
796 repo.odb().hash_algo(),
797 spec.filter.as_ref(),
798 &header.prerequisites,
799 &header.refs,
800 )?;
801
802 write_bundle_pack(repo, spec, out)?;
803 Ok(())
804}
805
806pub fn write_bundle_header(
817 out: &mut dyn Write,
818 algo: HashAlgo,
819 prerequisites: &[(ObjectId, Option<String>)],
820 refs: &[(ObjectId, String)],
821) -> std::result::Result<(), BundleError> {
822 write_header(out, algo, None, prerequisites, refs)
823}
824
825fn write_header(
826 out: &mut dyn Write,
827 algo: HashAlgo,
828 filter: Option<&FilterSpec>,
829 prerequisites: &[(ObjectId, Option<String>)],
830 refs: &[(ObjectId, String)],
831) -> std::result::Result<(), BundleError> {
832 let version = if algo != HashAlgo::Sha1 || filter.is_some() {
833 BundleVersion::V3
834 } else {
835 BundleVersion::V2
836 };
837 write_signature(out, version)?;
838 if version == BundleVersion::V3 {
839 write_capability_object_format(out, algo)?;
840 if let Some(filter) = filter {
841 write_capability_filter(out, filter)?;
842 }
843 }
844 for (oid, subject) in prerequisites {
845 write!(out, "-{} ", oid.to_hex()).map_err(|e| BundleError::Io(e.to_string()))?;
846 if let Some(s) = subject {
847 write!(out, "{s}").map_err(|e| BundleError::Io(e.to_string()))?;
848 }
849 writeln!(out).map_err(|e| BundleError::Io(e.to_string()))?;
850 }
851 for (oid, name) in refs {
852 writeln!(out, "{} {name}", oid.to_hex()).map_err(|e| BundleError::Io(e.to_string()))?;
853 }
854 writeln!(out).map_err(|e| BundleError::Io(e.to_string()))
855}
856
857pub struct BundleWriter<W: Write> {
859 inner: W,
860 finished_header: bool,
861}
862
863impl<W: Write> BundleWriter<W> {
864 pub fn new(inner: W) -> Self {
866 Self {
867 inner,
868 finished_header: false,
869 }
870 }
871
872 pub fn write_header_and_pack(
878 mut self,
879 repo: &Repository,
880 spec: &BundleSpec,
881 ) -> std::result::Result<(), BundleError> {
882 write_bundle(repo, spec, &mut self.inner)?;
883 self.finished_header = true;
884 Ok(())
885 }
886}
887
888fn write_signature(
889 out: &mut dyn Write,
890 version: BundleVersion,
891) -> std::result::Result<(), BundleError> {
892 let sig = match version {
893 BundleVersion::V2 => V2_SIGNATURE,
894 BundleVersion::V3 => V3_SIGNATURE,
895 };
896 out.write_all(sig.as_bytes())
897 .map_err(|e| BundleError::Io(e.to_string()))
898}
899
900fn write_capability_object_format(
901 out: &mut dyn Write,
902 algo: HashAlgo,
903) -> std::result::Result<(), BundleError> {
904 writeln!(out, "@object-format={}", algo.name()).map_err(|e| BundleError::Io(e.to_string()))
905}
906
907fn write_capability_filter(
908 out: &mut dyn Write,
909 filter: &FilterSpec,
910) -> std::result::Result<(), BundleError> {
911 writeln!(out, "@filter={}", filter.to_capability_value())
912 .map_err(|e| BundleError::Io(e.to_string()))
913}
914
915fn parse_capability(
916 cap: &str,
917 object_format: &mut HashAlgo,
918 filter: &mut Option<FilterSpec>,
919) -> std::result::Result<(), BundleError> {
920 if let Some(name) = cap.strip_prefix("object-format=") {
921 *object_format = HashAlgo::from_name(name)
922 .ok_or_else(|| BundleError::MalformedLine(format!("unknown object format: {name}")))?;
923 return Ok(());
924 }
925 if let Some(spec) = cap.strip_prefix("filter=") {
926 *filter = Some(FilterSpec::parse(spec)?);
927 return Ok(());
928 }
929 Err(BundleError::UnsupportedCapability(cap.to_owned()))
930}
931
932fn read_line(reader: &mut dyn BufRead) -> std::result::Result<String, BundleError> {
933 let mut line = String::new();
934 let n = reader.read_line(&mut line).map_err(BundleError::from)?;
935 if n == 0 {
936 return Err(BundleError::MalformedLine(
937 "unexpected end of header".to_owned(),
938 ));
939 }
940 if line.ends_with('\n') {
941 line.pop();
942 if line.ends_with('\r') {
943 line.pop();
944 }
945 }
946 Ok(line)
947}
948
949fn parse_oid_field(
950 line: &str,
951 algo: HashAlgo,
952) -> std::result::Result<(ObjectId, Option<&str>), BundleError> {
953 let hex_len = algo.hex_len();
954 if line.len() < hex_len {
955 return Err(BundleError::MalformedLine("short object id".to_owned()));
956 }
957 let (hex, tail) = line.split_at(hex_len);
958 if !hex.chars().all(|c| c.is_ascii_hexdigit()) {
959 return Err(BundleError::MalformedLine("invalid object id".to_owned()));
960 }
961 let oid = ObjectId::from_hex(hex)
962 .map_err(|_| BundleError::MalformedLine("invalid object id".to_owned()))?;
963 if hex.len() != algo.hex_len() {
964 return Err(BundleError::MalformedLine(
965 "object id length mismatch".to_owned(),
966 ));
967 }
968 let tail = match tail.chars().next() {
969 None => None,
970 Some(c) if c.is_whitespace() => Some(tail.trim_start()),
971 Some(_) => {
972 return Err(BundleError::MalformedLine(
973 "garbage after object id".to_owned(),
974 ))
975 }
976 };
977 Ok((oid, tail))
978}
979
980fn peel_to_commit(odb: &Odb, oid: ObjectId) -> Result<ObjectId> {
981 match odb.peel_tags(oid)? {
982 (commit, ObjectKind::Commit) => Ok(commit),
983 (other, _) => Err(Error::CorruptObject(format!(
984 "object {other} is not commit or tag"
985 ))),
986 }
987}
988
989fn commit_parent_set(odb: &Odb, commit: ObjectId) -> Result<Vec<ObjectId>> {
990 let obj = odb.read(&commit)?;
991 let commit = parse_commit(&obj.data)?;
992 Ok(commit.parents)
993}
994
995fn commit_closure_roots(odb: &Odb, roots: &[ObjectId]) -> Result<HashSet<ObjectId>> {
996 let mut seen = HashSet::new();
997 let mut q: VecDeque<ObjectId> = VecDeque::new();
998 for &root in roots {
999 let c = peel_to_commit(odb, root)?;
1000 if seen.insert(c) {
1001 q.push_back(c);
1002 }
1003 }
1004 while let Some(c) = q.pop_front() {
1005 for p in commit_parent_set(odb, c)? {
1006 if seen.insert(p) {
1007 q.push_back(p);
1008 }
1009 }
1010 }
1011 Ok(seen)
1012}
1013
1014fn prerequisite_commits(
1015 odb: &Odb,
1016 includes: &[ObjectId],
1017 excludes: &[ObjectId],
1018) -> std::result::Result<Vec<(ObjectId, Option<String>)>, BundleError> {
1019 if excludes.is_empty() {
1020 return Ok(Vec::new());
1021 }
1022 let exclude_set =
1023 commit_closure_roots(odb, excludes).map_err(|e| BundleError::Io(e.to_string()))?;
1024 let mut prerequisites = Vec::new();
1025 let mut seen_prereq = HashSet::new();
1026 let mut walk_seen = HashSet::new();
1027 let mut q: VecDeque<ObjectId> = VecDeque::new();
1028 for &root in includes {
1029 let c = peel_to_commit(odb, root).map_err(|e| BundleError::Io(e.to_string()))?;
1030 if exclude_set.contains(&c) {
1031 continue;
1032 }
1033 if walk_seen.insert(c) {
1034 q.push_back(c);
1035 }
1036 }
1037 while let Some(c) = q.pop_front() {
1038 let parents = commit_parent_set(odb, c).map_err(|e| BundleError::Io(e.to_string()))?;
1039 for p in parents {
1040 if exclude_set.contains(&p) && seen_prereq.insert(p) {
1041 let subject = commit_subject(odb, p).map_err(|e| BundleError::Io(e.to_string()))?;
1042 prerequisites.push((p, subject));
1043 }
1044 if !exclude_set.contains(&p) && walk_seen.insert(p) {
1045 q.push_back(p);
1046 }
1047 }
1048 }
1049 Ok(prerequisites)
1050}
1051
1052fn commit_subject(odb: &Odb, commit: ObjectId) -> Result<Option<String>> {
1053 let obj = odb.read(&commit)?;
1054 let parsed = parse_commit(&obj.data)?;
1055 let subj = message_subject(&parsed.message);
1056 if subj.is_empty() {
1057 Ok(None)
1058 } else {
1059 Ok(Some(subj))
1060 }
1061}
1062
1063fn filter_to_capability_value(filter: &ObjectFilter) -> String {
1064 match filter {
1065 ObjectFilter::BlobNone => "blob:none".to_owned(),
1066 ObjectFilter::BlobLimit(n) => format!("blob:limit={n}"),
1067 ObjectFilter::TreeDepth(d) => format!("tree:{d}"),
1068 ObjectFilter::SparseOid(s) => format!("sparse:oid={s}"),
1069 ObjectFilter::ObjectType(k) => format!("object:type={}", filter_object_kind_name(*k)),
1070 ObjectFilter::Combine(parts) => {
1071 let encoded: Vec<String> = parts
1072 .iter()
1073 .map(|p| {
1074 crate::rev_list::url_encode_object_filter_subspec(&filter_to_capability_value(
1075 p,
1076 ))
1077 })
1078 .collect();
1079 format!("combine:{}", encoded.join("+"))
1080 }
1081 }
1082}
1083
1084fn filter_object_kind_name(kind: FilterObjectKind) -> &'static str {
1085 match kind {
1086 FilterObjectKind::Blob => "blob",
1087 FilterObjectKind::Tree => "tree",
1088 FilterObjectKind::Commit => "commit",
1089 FilterObjectKind::Tag => "tag",
1090 }
1091}
1092
1093fn filtered_pack_objects(
1095 repo: &Repository,
1096 include: &[ObjectId],
1097 exclude: &[ObjectId],
1098 filter: &ObjectFilter,
1099 have_closure: &HashSet<ObjectId>,
1100) -> Result<Vec<ObjectId>> {
1101 let positive: Vec<String> = include.iter().map(ObjectId::to_hex).collect();
1102 let negative: Vec<String> = exclude.iter().map(ObjectId::to_hex).collect();
1104 let options = RevListOptions {
1105 objects: true,
1106 no_object_names: true,
1107 quiet: true,
1108 filter: Some(filter.clone()),
1109 missing_action: MissingAction::Error,
1110 ..Default::default()
1111 };
1112 let walk = crate::rev_list::rev_list(repo, &positive, &negative, &options)?;
1113 Ok(collect_filtered_send_list(&walk, have_closure))
1114}
1115
1116fn collect_filtered_send_list(
1117 walk: &RevListResult,
1118 have_closure: &HashSet<ObjectId>,
1119) -> Vec<ObjectId> {
1120 let mut send = Vec::new();
1121 let mut seen = HashSet::new();
1122 let mut push = |oid: ObjectId| {
1123 if have_closure.contains(&oid) {
1124 return;
1125 }
1126 if seen.insert(oid) {
1127 send.push(oid);
1128 }
1129 };
1130 for oid in &walk.commits {
1131 push(*oid);
1132 }
1133 for (oid, _) in &walk.objects {
1134 push(*oid);
1135 }
1136 for tag_oid in walk.tip_annotated_tag_by_commit.values().flatten() {
1137 push(*tag_oid);
1138 }
1139 send
1140}
1141
1142#[cfg(test)]
1143mod tests {
1144 use super::*;
1145 use std::io::Cursor;
1146
1147 #[test]
1148 fn parse_v2_header_and_refs() {
1149 let oid = "a".repeat(40);
1150 let data = format!("# v2 git bundle\n{oid} refs/heads/main\n\nPACK");
1151 let mut cur = Cursor::new(data.into_bytes());
1152 let header = read_header(&mut cur).expect("header");
1153 assert_eq!(header.version, BundleVersion::V2);
1154 assert_eq!(header.object_format, HashAlgo::Sha1);
1155 assert_eq!(header.prerequisites.len(), 0);
1156 assert_eq!(header.refs.len(), 1);
1157 assert_eq!(header.refs[0].0, "refs/heads/main");
1158 }
1159
1160 #[test]
1161 fn bad_signature_yields_error() {
1162 let mut cur = Cursor::new(b"# v4 git bundle\n\nPACK");
1163 let err = read_header(&mut cur).unwrap_err();
1164 assert_eq!(err, BundleError::BadSignature);
1165 }
1166
1167 #[test]
1168 fn unknown_capability() {
1169 let mut cur = Cursor::new(b"# v3 git bundle\n@unknown=yes\n\nPACK");
1170 let err = read_header(&mut cur).unwrap_err();
1171 assert!(matches!(err, BundleError::UnsupportedCapability(_)));
1172 }
1173
1174 #[test]
1175 fn truncated_ref_line() {
1176 let oid = "a".repeat(40);
1177 let data = format!("# v2 git bundle\n{oid}\n\nPACK");
1178 let mut cur = Cursor::new(data.into_bytes());
1179 let err = read_header(&mut cur).unwrap_err();
1180 assert!(matches!(err, BundleError::MalformedLine(_)));
1181 }
1182
1183 #[test]
1184 fn filter_spec_roundtrip_blob_none() {
1185 let f = FilterSpec::parse("blob:none").expect("parse");
1186 assert_eq!(f.to_capability_value(), "blob:none");
1187 }
1188
1189 #[test]
1190 fn verify_report_missing_prerequisite_error() {
1191 let oid = ObjectId::from_hex(&"b".repeat(40)).expect("oid");
1192 let report = BundleVerifyReport {
1193 missing_prerequisites: vec![(oid, Some("subj".to_owned()))],
1194 prerequisites_not_connected: false,
1195 };
1196 let err = report.into_result().unwrap_err();
1197 assert!(matches!(err, BundleError::MissingPrerequisite { .. }));
1198 }
1199
1200 fn write_bundle_file(bytes: &[u8]) -> tempfile::NamedTempFile {
1201 let mut file = tempfile::NamedTempFile::new().expect("tempfile");
1202 file.write_all(bytes).expect("write bundle");
1203 file.flush().expect("flush bundle");
1204 file
1205 }
1206
1207 #[test]
1208 fn open_reports_pack_offset_after_header() {
1209 let oid = "a".repeat(40);
1210 let header = format!("# v2 git bundle\n{oid} refs/heads/main\n\n");
1211 let mut data = header.clone().into_bytes();
1212 data.extend_from_slice(b"PACK\0\0\0\x02\0\0\0\0");
1213 let file = write_bundle_file(&data);
1214 let bundle = Bundle::open(file.path()).expect("open");
1215 assert_eq!(bundle.pack_byte_offset(), header.len() as u64);
1216 assert_eq!(bundle.header().refs.len(), 1);
1217 }
1218
1219 #[test]
1220 fn open_skips_blank_lines_before_pack() {
1221 let oid = "a".repeat(40);
1222 let header = format!("# v2 git bundle\n{oid} HEAD\n\n\r\n\n");
1223 let mut data = header.clone().into_bytes();
1224 data.extend_from_slice(b"PACK");
1225 let file = write_bundle_file(&data);
1226 let bundle = Bundle::open(file.path()).expect("open");
1227 assert_eq!(bundle.pack_byte_offset(), header.len() as u64);
1228 }
1229
1230 #[test]
1231 fn open_handles_pack_magic_across_read_buffer_boundary() {
1232 let oid = "c".repeat(40);
1233 let signature = "# v2 git bundle\n";
1234 let ref_line = |i: usize| format!("{oid} refs/tags/t{i:06}\n");
1235 let line_len = ref_line(0).len();
1236 let mut refs = 0usize;
1239 while signature.len() + (refs + 1) * line_len + 1 + 2 <= HEADER_READ_BUF {
1240 refs += 1;
1241 }
1242 let mut header = String::from(signature);
1243 for i in 0..refs {
1244 header.push_str(&ref_line(i));
1245 }
1246 header.push('\n');
1247 let pad = HEADER_READ_BUF - 2 - header.len();
1248 let mut data = header.clone().into_bytes();
1249 data.extend(std::iter::repeat_n(b'\n', pad));
1250 data.extend_from_slice(b"PACK\0\0\0\x02");
1251 assert_eq!(&data[HEADER_READ_BUF - 2..HEADER_READ_BUF + 2], b"PACK");
1252 let file = write_bundle_file(&data);
1253 let bundle = Bundle::open(file.path()).expect("open");
1254 assert_eq!(bundle.header().refs.len(), refs);
1255 assert_eq!(bundle.pack_byte_offset(), (HEADER_READ_BUF - 2) as u64);
1256 }
1257
1258 #[test]
1259 fn open_missing_file_names_path() {
1260 let path = std::path::Path::new("/no/such/bundle/path.bundle");
1261 let err = Bundle::open(path).unwrap_err();
1262 let msg = err.to_string();
1263 assert!(
1264 msg.contains("could not open") && msg.contains("path.bundle"),
1265 "{msg}"
1266 );
1267 }
1268
1269 #[test]
1270 fn open_without_pack_is_missing_pack() {
1271 let oid = "a".repeat(40);
1272 let truncated = format!("# v2 git bundle\n{oid} HEAD\n\n");
1273 let file = write_bundle_file(truncated.as_bytes());
1274 assert_eq!(
1275 Bundle::open(file.path()).unwrap_err(),
1276 BundleError::MissingPack
1277 );
1278
1279 let short = format!("# v2 git bundle\n{oid} HEAD\n\nPA");
1280 let file = write_bundle_file(short.as_bytes());
1281 assert_eq!(
1282 Bundle::open(file.path()).unwrap_err(),
1283 BundleError::MissingPack
1284 );
1285
1286 let garbage = format!("# v2 git bundle\n{oid} HEAD\n\nJUNK");
1287 let file = write_bundle_file(garbage.as_bytes());
1288 assert_eq!(
1289 Bundle::open(file.path()).unwrap_err(),
1290 BundleError::MissingPack
1291 );
1292 }
1293}