This repository has no description
0

Configure Feed

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

core / knot2 / crates / knot-git / src / objects.rs
51 kB 1555 lines
1use std::collections::{HashMap, HashSet, VecDeque}; 2use std::io::{BufRead, Read}; 3use std::ops::ControlFlow; 4use std::path::PathBuf; 5use std::sync::atomic::{AtomicUsize, Ordering}; 6use std::time::{Duration, Instant}; 7 8use knot_types::{AuthorName, Email, ObjectCount, Oid, ParseError, RepoPath, UnixSeconds}; 9 10use crate::error::{GitError, SelectionLimit}; 11use crate::repo::Repo; 12 13// why? idk. should we let this be deeper 14pub const MAX_TREE_DEPTH: usize = 1024; 15const MAX_TAG_DEPTH: usize = 32; 16 17#[derive(Debug, Clone, Copy)] 18pub struct Wants<'a>(&'a [Oid]); 19 20#[derive(Debug, Clone, Copy)] 21pub struct Haves<'a>(&'a [Oid]); 22 23#[derive(Debug, Clone, Copy)] 24pub struct ShallowCommits<'a>(&'a [Oid]); 25 26impl<'a> Wants<'a> { 27 pub fn new(oids: &'a [Oid]) -> Self { 28 Self(oids) 29 } 30 31 pub fn as_slice(self) -> &'a [Oid] { 32 self.0 33 } 34} 35 36impl<'a> Haves<'a> { 37 pub fn new(oids: &'a [Oid]) -> Self { 38 Self(oids) 39 } 40 41 pub fn as_slice(self) -> &'a [Oid] { 42 self.0 43 } 44} 45 46impl<'a> ShallowCommits<'a> { 47 pub fn new(oids: &'a [Oid]) -> Self { 48 Self(oids) 49 } 50 51 pub fn as_slice(self) -> &'a [Oid] { 52 self.0 53 } 54} 55 56enum Peeled { 57 Commit(gix::ObjectId), 58 Direct(gix::ObjectId), 59} 60 61#[derive(Debug, Clone, PartialEq, Eq)] 62pub struct Identity { 63 pub name: AuthorName, 64 pub email: Email, 65 pub time: UnixSeconds, 66 pub offset_seconds: i32, 67} 68 69#[derive(Debug, Clone, PartialEq, Eq)] 70pub struct Commit { 71 pub id: Oid, 72 pub tree: Oid, 73 pub parents: Vec<Oid>, 74 pub author: Identity, 75 pub committer: Identity, 76 pub message: String, 77 pub pgp_signature: Option<String>, 78 pub merge_tag: Option<String>, 79 pub extra_headers: Vec<(String, Vec<u8>)>, 80} 81 82impl Commit { 83 pub fn change_id(&self) -> Option<CommitChangeId> { 84 self.extra_headers 85 .iter() 86 .find(|(name, _)| name == "change-id") 87 .and_then(|(_, value)| std::str::from_utf8(value).ok()) 88 .and_then(|value| CommitChangeId::new(value).ok()) 89 } 90} 91 92#[derive(Debug, Clone, PartialEq, Eq)] 93pub struct CommitChangeId(String); 94 95impl CommitChangeId { 96 pub fn new(value: impl Into<String>) -> Result<Self, ParseError> { 97 let value = value.into(); 98 let valid = 99 !value.is_empty() && value.len() <= 100 && value.chars().all(|c| c.is_ascii_graphic()); 100 match valid { 101 true => Ok(Self(value)), 102 false => Err(ParseError::Invalid { 103 kind: "commit change-id", 104 value, 105 }), 106 } 107 } 108 109 pub fn as_str(&self) -> &str { 110 &self.0 111 } 112} 113 114impl std::fmt::Display for CommitChangeId { 115 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 116 f.pad(&self.0) 117 } 118} 119 120#[derive(Debug, Clone, Copy, PartialEq, Eq)] 121pub enum EntryKind { 122 Tree, 123 Blob, 124 BlobExecutable, 125 Link, 126 Commit, 127} 128 129impl EntryKind { 130 pub fn mode_octal(self) -> &'static str { 131 match self { 132 EntryKind::Tree => "0040000", 133 EntryKind::Blob => "0100644", 134 EntryKind::BlobExecutable => "0100755", 135 EntryKind::Link => "0120000", 136 EntryKind::Commit => "0160000", 137 } 138 } 139 140 pub fn is_file(self) -> bool { 141 matches!(self, EntryKind::Blob | EntryKind::BlobExecutable) 142 } 143 144 pub fn from_git_mode(mode: &str) -> Option<EntryKind> { 145 match mode.trim() { 146 "100644" | "100664" => Some(EntryKind::Blob), 147 "100755" => Some(EntryKind::BlobExecutable), 148 "120000" => Some(EntryKind::Link), 149 "160000" => Some(EntryKind::Commit), 150 "040000" | "40000" => Some(EntryKind::Tree), 151 _ => None, 152 } 153 } 154} 155 156impl From<EntryKind> for gix::objs::tree::EntryKind { 157 fn from(kind: EntryKind) -> Self { 158 match kind { 159 EntryKind::Tree => Self::Tree, 160 EntryKind::Blob => Self::Blob, 161 EntryKind::BlobExecutable => Self::BlobExecutable, 162 EntryKind::Link => Self::Link, 163 EntryKind::Commit => Self::Commit, 164 } 165 } 166} 167 168#[derive(Debug, Clone, PartialEq, Eq)] 169pub struct TreeEntry { 170 pub name: String, 171 pub oid: Oid, 172 pub kind: EntryKind, 173} 174 175#[derive(Debug, Clone, PartialEq, Eq)] 176pub struct Tree { 177 pub entries: Vec<TreeEntry>, 178} 179 180#[derive(Debug, Clone, PartialEq, Eq)] 181pub enum FileChange { 182 Added { 183 path: RepoPath, 184 oid: Oid, 185 }, 186 Deleted { 187 path: RepoPath, 188 oid: Oid, 189 }, 190 Modified { 191 path: RepoPath, 192 old: Oid, 193 new: Oid, 194 }, 195 Renamed { 196 from: RepoPath, 197 to: RepoPath, 198 old: Oid, 199 new: Oid, 200 }, 201} 202 203#[derive(Debug, Clone, PartialEq, Eq)] 204pub struct Comparison { 205 pub commits: Vec<Oid>, 206 pub changes: Vec<FileChange>, 207} 208 209#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 210pub struct CommitRange { 211 pub base: Oid, 212 pub head: Oid, 213} 214 215#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] 216pub struct TreeDepth(u32); 217 218impl TreeDepth { 219 pub const fn new(depth: u32) -> Self { 220 Self(depth) 221 } 222 223 pub const fn deeper(self) -> Self { 224 Self(self.0.saturating_add(1)) 225 } 226 227 const fn is_exhausted(self) -> bool { 228 self.0 == 0 229 } 230 231 const fn shallower(self) -> Self { 232 Self(self.0.saturating_sub(1)) 233 } 234} 235 236#[derive(Debug, Clone, Copy, PartialEq, Eq)] 237pub enum Filter { 238 None, 239 BlobNone, 240 BlobLimit(u64), 241 TreeDepth(TreeDepth), 242} 243 244#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] 245pub struct CommitDepth(u32); 246 247impl CommitDepth { 248 pub const fn new(depth: u32) -> Self { 249 Self(depth) 250 } 251 252 const fn deeper(self) -> Self { 253 Self(self.0.saturating_add(1)) 254 } 255} 256 257#[derive(Debug, Clone, Default, PartialEq, Eq)] 258pub struct Deepen { 259 pub depth: Option<CommitDepth>, 260 pub since: Option<UnixSeconds>, 261 pub not: Vec<Oid>, 262 pub relative: bool, 263} 264 265impl Deepen { 266 pub fn is_shallow_request(&self) -> bool { 267 self.depth.is_some() || self.since.is_some() || !self.not.is_empty() 268 } 269} 270 271#[derive(Debug, Clone, PartialEq, Eq)] 272pub struct ShallowPlan { 273 pub commits: Vec<Oid>, 274 pub shallow: Vec<Oid>, 275 pub unshallow: Vec<Oid>, 276} 277 278#[derive(Debug, Clone, PartialEq, Eq)] 279pub struct PackSelection { 280 pub send: Vec<Oid>, 281 pub client_has: HashSet<Oid>, 282} 283 284#[derive(Debug, Clone, Copy)] 285pub struct PackBudget { 286 max_objects: ObjectCount, 287 stall: Option<Duration>, 288} 289 290impl PackBudget { 291 pub fn new(max_objects: ObjectCount, stall: Duration) -> Self { 292 Self { 293 max_objects, 294 stall: Some(stall), 295 } 296 } 297 298 pub fn unbounded() -> Self { 299 Self { 300 max_objects: ObjectCount::new(usize::MAX), 301 stall: None, 302 } 303 } 304} 305 306#[derive(Clone, Copy)] 307pub(crate) struct Walked { 308 budget: PackBudget, 309 count: usize, 310 deadline: Option<Instant>, 311} 312 313impl Walked { 314 pub(crate) fn new(budget: PackBudget) -> Self { 315 Self { 316 budget, 317 count: 0, 318 deadline: budget.stall.map(|stall| Instant::now() + stall), 319 } 320 } 321 322 pub(crate) fn tick(&mut self) -> Result<(), GitError> { 323 self.count += 1; 324 if self.count > self.budget.max_objects.get() { 325 return Err(GitError::Selection(SelectionLimit::Objects)); 326 } 327 advance_stall(&mut self.deadline, self.budget.stall) 328 } 329} 330 331fn advance_stall(deadline: &mut Option<Instant>, stall: Option<Duration>) -> Result<(), GitError> { 332 if let (Some(deadline), Some(stall)) = (deadline.as_mut(), stall) { 333 let now = Instant::now(); 334 if now >= *deadline { 335 return Err(GitError::Selection(SelectionLimit::Time)); 336 } 337 *deadline = now + stall; 338 } 339 Ok(()) 340} 341 342const MAX_LOOSE_HEADER: usize = 64; 343 344const PARALLEL_SELECT_MIN: usize = 4096; 345 346struct SharedWalk<'a> { 347 counter: &'a AtomicUsize, 348 max_objects: usize, 349 stall: Option<Duration>, 350 deadline: Option<Instant>, 351} 352 353impl<'a> SharedWalk<'a> { 354 fn new(counter: &'a AtomicUsize, budget: &PackBudget) -> Self { 355 Self { 356 counter, 357 max_objects: budget.max_objects.get(), 358 stall: budget.stall, 359 deadline: budget.stall.map(|stall| Instant::now() + stall), 360 } 361 } 362 363 fn tick(&mut self) -> Result<(), GitError> { 364 let count = self.counter.fetch_add(1, Ordering::Relaxed) + 1; 365 if count > self.max_objects { 366 return Err(GitError::Selection(SelectionLimit::Objects)); 367 } 368 advance_stall(&mut self.deadline, self.stall) 369 } 370} 371 372pub enum BlobReader { 373 Loose(std::io::BufReader<flate2::read::ZlibDecoder<std::fs::File>>), 374 Packed(std::io::Cursor<Vec<u8>>), 375} 376 377impl Read for BlobReader { 378 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> { 379 match self { 380 BlobReader::Loose(reader) => reader.read(buf), 381 BlobReader::Packed(reader) => reader.read(buf), 382 } 383 } 384} 385 386fn skip_loose_header(reader: &mut impl std::io::BufRead, oid: Oid) -> Result<(), GitError> { 387 let corrupt = |message: String| GitError::Corrupt { oid, message }; 388 let mut header = Vec::with_capacity(MAX_LOOSE_HEADER); 389 reader 390 .take(MAX_LOOSE_HEADER as u64) 391 .read_until(0, &mut header) 392 .map_err(|error| corrupt(error.to_string()))?; 393 if header.last() != Some(&0) || !header.starts_with(b"blob ") { 394 return Err(corrupt("loose object header isn't blob".to_string())); 395 } 396 Ok(()) 397} 398 399pub(crate) fn identity(signature: gix::actor::SignatureRef<'_>) -> Result<Identity, GitError> { 400 let time = signature 401 .time() 402 .map_err(|error| GitError::Decode(error.to_string()))?; 403 Ok(Identity { 404 name: AuthorName::new(signature.name.to_string()), 405 email: Email::new(signature.email.to_string()), 406 time: UnixSeconds::new(time.seconds), 407 offset_seconds: time.offset, 408 }) 409} 410 411pub(crate) fn signature(identity: &Identity) -> gix::actor::Signature { 412 let clean = |raw: &str| raw.replace(['<', '>'], "").trim().to_string(); 413 gix::actor::Signature { 414 name: clean(identity.name.as_str()).into(), 415 email: clean(identity.email.as_str()).into(), 416 time: gix::date::Time { 417 seconds: identity.time.get(), 418 offset: identity.offset_seconds, 419 }, 420 } 421} 422 423pub(crate) fn map_kind(kind: gix::objs::tree::EntryKind) -> EntryKind { 424 use gix::objs::tree::EntryKind as Source; 425 match kind { 426 Source::Tree => EntryKind::Tree, 427 Source::Blob => EntryKind::Blob, 428 Source::BlobExecutable => EntryKind::BlobExecutable, 429 Source::Link => EntryKind::Link, 430 Source::Commit => EntryKind::Commit, 431 } 432} 433 434impl Repo { 435 fn load_object(&self, oid: Oid) -> Result<gix::Object<'_>, GitError> { 436 #[cfg(feature = "instrument")] 437 crate::instrument::record_read(); 438 match self.git().try_find_object(oid.object_id()) { 439 Ok(Some(object)) => Ok(object), 440 Ok(None) => Err(GitError::ObjectNotFound(oid)), 441 Err(error) => Err(GitError::Corrupt { 442 oid, 443 message: error.to_string(), 444 }), 445 } 446 } 447 448 pub fn find_commit(&self, oid: Oid) -> Result<Commit, GitError> { 449 let object = self.load_object(oid)?; 450 let commit = object.try_into_commit().map_err(|_| GitError::ObjectType { 451 oid, 452 expected: "commit", 453 })?; 454 let tree = Oid::from( 455 commit 456 .tree_id() 457 .map_err(|error| GitError::Decode(error.to_string()))? 458 .detach(), 459 ); 460 let parents = commit 461 .parent_ids() 462 .map(|id| Oid::from(id.detach())) 463 .collect(); 464 let author = identity( 465 commit 466 .author() 467 .map_err(|error| GitError::Decode(error.to_string()))?, 468 )?; 469 let committer = identity( 470 commit 471 .committer() 472 .map_err(|error| GitError::Decode(error.to_string()))?, 473 )?; 474 let message = commit 475 .message_raw() 476 .map_err(|error| GitError::Decode(error.to_string()))? 477 .to_string(); 478 let decoded = commit 479 .decode() 480 .map_err(|error| GitError::Decode(error.to_string()))?; 481 let (mut pgp_signature, mut merge_tag) = (None, None); 482 let extra_headers = decoded 483 .extra_headers 484 .iter() 485 .filter_map(|(name, value)| match name.to_string().as_str() { 486 "gpgsig" => { 487 pgp_signature = Some(value.to_string()); 488 None 489 } 490 "mergetag" => { 491 merge_tag = Some(value.to_string()); 492 None 493 } 494 other => Some((other.to_string(), value.to_vec())), 495 }) 496 .collect(); 497 Ok(Commit { 498 id: oid, 499 tree, 500 parents, 501 author, 502 committer, 503 message, 504 pgp_signature, 505 merge_tag, 506 extra_headers, 507 }) 508 } 509 510 pub fn find_tree(&self, oid: Oid) -> Result<Tree, GitError> { 511 let object = self.load_object(oid)?; 512 let tree = object.try_into_tree().map_err(|_| GitError::ObjectType { 513 oid, 514 expected: "tree", 515 })?; 516 let decoded = tree 517 .decode() 518 .map_err(|error| GitError::Decode(error.to_string()))?; 519 let entries = decoded 520 .entries 521 .iter() 522 .map(|entry| TreeEntry { 523 name: entry.filename.to_string(), 524 oid: Oid::from(entry.oid.to_owned()), 525 kind: map_kind(entry.mode.kind()), 526 }) 527 .collect(); 528 Ok(Tree { entries }) 529 } 530 531 pub fn blob_size(&self, oid: Oid) -> Result<u64, GitError> { 532 match self.git().try_find_header(oid.object_id()) { 533 Ok(Some(header)) if header.kind() == gix::object::Kind::Blob => Ok(header.size()), 534 Ok(Some(_)) => Err(GitError::ObjectType { 535 oid, 536 expected: "blob", 537 }), 538 Ok(None) => Err(GitError::ObjectNotFound(oid)), 539 Err(error) => Err(GitError::Corrupt { 540 oid, 541 message: error.to_string(), 542 }), 543 } 544 } 545 546 pub fn read_blob(&self, oid: Oid) -> Result<Vec<u8>, GitError> { 547 let object = self.load_object(oid)?; 548 let mut blob = object.try_into_blob().map_err(|_| GitError::ObjectType { 549 oid, 550 expected: "blob", 551 })?; 552 Ok(blob.take_data()) 553 } 554 555 fn loose_object_path(&self, oid: Oid) -> PathBuf { 556 let hex = oid.to_hex(); 557 let (shard, rest) = hex.split_at(2); 558 self.git().git_dir().join("objects").join(shard).join(rest) 559 } 560 561 pub fn remove_loose_object(&self, oid: Oid) -> Result<(), GitError> { 562 match std::fs::remove_file(self.loose_object_path(oid)) { 563 Ok(()) => Ok(()), 564 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()), 565 Err(error) => Err(GitError::RemoveObject { 566 oid, 567 message: error.to_string(), 568 }), 569 } 570 } 571 572 pub fn open_blob(&self, oid: Oid) -> Result<(u64, BlobReader), GitError> { 573 let header = match self.git().try_find_header(oid.object_id()) { 574 Ok(Some(header)) => header, 575 Ok(None) => return Err(GitError::ObjectNotFound(oid)), 576 Err(error) => { 577 return Err(GitError::Corrupt { 578 oid, 579 message: error.to_string(), 580 }); 581 } 582 }; 583 if header.kind() != gix::object::Kind::Blob { 584 return Err(GitError::ObjectType { 585 oid, 586 expected: "blob", 587 }); 588 } 589 let size = header.size(); 590 match std::fs::File::open(self.loose_object_path(oid)) { 591 Ok(file) => { 592 let mut reader = std::io::BufReader::new(flate2::read::ZlibDecoder::new(file)); 593 skip_loose_header(&mut reader, oid)?; 594 Ok((size, BlobReader::Loose(reader))) 595 } 596 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(( 597 size, 598 BlobReader::Packed(std::io::Cursor::new(self.read_blob(oid)?)), 599 )), 600 Err(error) => Err(GitError::Corrupt { 601 oid, 602 message: error.to_string(), 603 }), 604 } 605 } 606 607 fn graph_tree_and_parents(&self, commit: Oid) -> Option<(gix::ObjectId, Vec<gix::ObjectId>)> { 608 let graph = self.commit_graph()?; 609 let node = graph.commit_by_id(commit.object_id())?; 610 let tree = node.root_tree_id().to_owned(); 611 let parents = node 612 .iter_parents() 613 .filter_map(Result::ok) 614 .map(|position| graph.commit_at(position).id().to_owned()) 615 .collect(); 616 Some((tree, parents)) 617 } 618 619 pub(crate) fn commit_tree(&self, commit: Oid) -> Result<gix::ObjectId, GitError> { 620 if let Some(node) = self 621 .commit_graph() 622 .and_then(|graph| graph.commit_by_id(commit.object_id())) 623 { 624 return Ok(node.root_tree_id().to_owned()); 625 } 626 let object = self.load_object(commit)?; 627 let commit_object = object.try_into_commit().map_err(|_| GitError::ObjectType { 628 oid: commit, 629 expected: "commit", 630 })?; 631 Ok(commit_object 632 .tree_id() 633 .map_err(|error| GitError::Decode(error.to_string()))? 634 .detach()) 635 } 636 637 pub fn diff(&self, range: CommitRange) -> Result<Vec<FileChange>, GitError> { 638 let old_tree_oid = self.commit_tree(range.base)?; 639 let new_tree_oid = self.commit_tree(range.head)?; 640 let old_tree = self 641 .load_object(Oid::from(old_tree_oid))? 642 .try_into_tree() 643 .map_err(|error| GitError::Backend(error.to_string()))?; 644 let new_tree = self 645 .load_object(Oid::from(new_tree_oid))? 646 .try_into_tree() 647 .map_err(|error| GitError::Backend(error.to_string()))?; 648 649 let mut changes = Vec::new(); 650 old_tree 651 .changes() 652 .map_err(|error| GitError::Backend(error.to_string()))? 653 .for_each_to_obtain_tree(&new_tree, |change| { 654 use gix::object::tree::diff::Change; 655 let tree_path = |location: &gix::bstr::BStr| { 656 RepoPath::new(location.to_string()) 657 .map_err(|error| GitError::Decode(error.to_string())) 658 }; 659 let mapped = match change { 660 Change::Addition { location, id, .. } => FileChange::Added { 661 path: tree_path(location)?, 662 oid: Oid::from(id.detach()), 663 }, 664 Change::Deletion { location, id, .. } => FileChange::Deleted { 665 path: tree_path(location)?, 666 oid: Oid::from(id.detach()), 667 }, 668 Change::Modification { 669 location, 670 previous_id, 671 id, 672 .. 673 } => FileChange::Modified { 674 path: tree_path(location)?, 675 old: Oid::from(previous_id.detach()), 676 new: Oid::from(id.detach()), 677 }, 678 Change::Rewrite { 679 source_location, 680 location, 681 source_id, 682 id, 683 .. 684 } => FileChange::Renamed { 685 from: tree_path(source_location)?, 686 to: tree_path(location)?, 687 old: Oid::from(source_id.detach()), 688 new: Oid::from(id.detach()), 689 }, 690 }; 691 changes.push(mapped); 692 Ok::<_, GitError>(ControlFlow::Continue(())) 693 }) 694 .map_err(|error| GitError::Backend(error.to_string()))?; 695 Ok(changes) 696 } 697 698 pub fn compare(&self, range: CommitRange) -> Result<Comparison, GitError> { 699 let commits = self.rev_walk(Wants::new(&[range.head]), Haves::new(&[range.base]))?; 700 let changes = self.diff(range)?; 701 Ok(Comparison { commits, changes }) 702 } 703 704 fn peel( 705 &self, 706 oid: gix::ObjectId, 707 tags: &mut Vec<Oid>, 708 depth: usize, 709 ) -> Result<Peeled, GitError> { 710 if depth == 0 { 711 return Err(GitError::DepthExceeded("annotated tag chain")); 712 } 713 let object = self.load_object(Oid::from(oid))?; 714 match object.kind { 715 gix::object::Kind::Commit => Ok(Peeled::Commit(oid)), 716 gix::object::Kind::Tree | gix::object::Kind::Blob => Ok(Peeled::Direct(oid)), 717 gix::object::Kind::Tag => { 718 tags.push(Oid::from(oid)); 719 let target = object 720 .try_into_tag() 721 .map_err(|error| GitError::Decode(error.to_string()))? 722 .target_id() 723 .map_err(|error| GitError::Decode(error.to_string()))? 724 .detach(); 725 self.peel(target, tags, depth - 1) 726 } 727 } 728 } 729 730 pub fn peeled_target(&self, oid: Oid) -> Result<Option<Oid>, GitError> { 731 let object = self.load_object(oid)?; 732 match object.kind { 733 gix::object::Kind::Tag => { 734 let mut tags = Vec::new(); 735 let peeled = match self.peel(oid.object_id(), &mut tags, MAX_TAG_DEPTH)? { 736 Peeled::Commit(id) | Peeled::Direct(id) => Oid::from(id), 737 }; 738 Ok(Some(peeled)) 739 } 740 _ => Ok(None), 741 } 742 } 743 744 pub(crate) fn commit_tree_and_parents( 745 &self, 746 commit: Oid, 747 ) -> Result<(gix::ObjectId, Vec<gix::ObjectId>), GitError> { 748 if let Some(found) = self.graph_tree_and_parents(commit) { 749 return Ok(found); 750 } 751 let object = self.load_object(commit)?; 752 let commit_object = object.try_into_commit().map_err(|_| GitError::ObjectType { 753 oid: commit, 754 expected: "commit", 755 })?; 756 let tree = commit_object 757 .tree_id() 758 .map_err(|error| GitError::Decode(error.to_string()))? 759 .detach(); 760 let parents = commit_object.parent_ids().map(|id| id.detach()).collect(); 761 Ok((tree, parents)) 762 } 763 764 fn blob_kept(&self, oid: gix::ObjectId, filter: Filter) -> Result<bool, GitError> { 765 match filter { 766 Filter::None | Filter::TreeDepth(_) => Ok(true), 767 Filter::BlobNone => Ok(false), 768 Filter::BlobLimit(limit) => { 769 let header = self 770 .git() 771 .try_find_header(oid) 772 .map_err(|error| GitError::Corrupt { 773 oid: Oid::from(oid), 774 message: error.to_string(), 775 })? 776 .ok_or(GitError::ObjectNotFound(Oid::from(oid)))?; 777 Ok(header.size() < limit) 778 } 779 } 780 } 781 782 fn walk_tree( 783 &self, 784 tree: gix::ObjectId, 785 seen: &mut HashSet<gix::ObjectId>, 786 visit: &mut dyn FnMut(Oid), 787 filter: Filter, 788 nesting: usize, 789 walked: &mut Walked, 790 ) -> Result<(), GitError> { 791 if nesting == 0 { 792 return Err(GitError::DepthExceeded("tree nesting")); 793 } 794 if tree == gix::ObjectId::empty_tree(self.git().object_hash()) { 795 return Ok(()); 796 } 797 if !seen.insert(tree) { 798 return Ok(()); 799 } 800 visit(Oid::from(tree)); 801 walked.tick()?; 802 let object = self.load_object(Oid::from(tree))?; 803 let decoded = object 804 .try_into_tree() 805 .map_err(|error| GitError::Decode(error.to_string()))?; 806 let decoded = decoded 807 .decode() 808 .map_err(|error| GitError::Decode(error.to_string()))?; 809 decoded.entries.iter().try_for_each(|entry| { 810 let oid = entry.oid.to_owned(); 811 match entry.mode.kind() { 812 gix::objs::tree::EntryKind::Tree => { 813 self.walk_tree(oid, seen, visit, filter, nesting - 1, walked) 814 } 815 gix::objs::tree::EntryKind::Commit => Ok(()), 816 _ => { 817 if !seen.contains(&oid) && self.blob_kept(oid, filter)? { 818 seen.insert(oid); 819 visit(Oid::from(oid)); 820 walked.tick()?; 821 } 822 Ok(()) 823 } 824 } 825 }) 826 } 827 828 #[allow(clippy::too_many_arguments)] 829 fn walk_tree_depth( 830 &self, 831 tree: gix::ObjectId, 832 remaining: TreeDepth, 833 seen: &mut HashSet<gix::ObjectId>, 834 expanded: &mut HashMap<gix::ObjectId, TreeDepth>, 835 visit: &mut dyn FnMut(Oid), 836 nesting: usize, 837 walked: &mut Walked, 838 ) -> Result<(), GitError> { 839 if nesting == 0 { 840 return Err(GitError::DepthExceeded("tree nesting")); 841 } 842 if remaining.is_exhausted() || tree == gix::ObjectId::empty_tree(self.git().object_hash()) { 843 return Ok(()); 844 } 845 if expanded 846 .get(&tree) 847 .is_some_and(|deepest| *deepest >= remaining) 848 { 849 return Ok(()); 850 } 851 expanded.insert(tree, remaining); 852 if seen.insert(tree) { 853 visit(Oid::from(tree)); 854 walked.tick()?; 855 } 856 let object = self.load_object(Oid::from(tree))?; 857 let decoded = object 858 .try_into_tree() 859 .map_err(|error| GitError::Decode(error.to_string()))?; 860 let decoded = decoded 861 .decode() 862 .map_err(|error| GitError::Decode(error.to_string()))?; 863 let next = remaining.shallower(); 864 decoded.entries.iter().try_for_each(|entry| { 865 let oid = entry.oid.to_owned(); 866 match entry.mode.kind() { 867 gix::objs::tree::EntryKind::Tree => { 868 self.walk_tree_depth(oid, next, seen, expanded, visit, nesting - 1, walked) 869 } 870 gix::objs::tree::EntryKind::Commit => Ok(()), 871 _ => { 872 if !next.is_exhausted() && seen.insert(oid) { 873 visit(Oid::from(oid)); 874 walked.tick()?; 875 } 876 Ok(()) 877 } 878 } 879 }) 880 } 881 882 fn walk_root_tree( 883 &self, 884 tree: gix::ObjectId, 885 seen: &mut HashSet<gix::ObjectId>, 886 expanded: &mut HashMap<gix::ObjectId, TreeDepth>, 887 visit: &mut dyn FnMut(Oid), 888 filter: Filter, 889 walked: &mut Walked, 890 ) -> Result<(), GitError> { 891 match filter { 892 Filter::TreeDepth(max) => { 893 self.walk_tree_depth(tree, max, seen, expanded, visit, MAX_TREE_DEPTH, walked) 894 } 895 _ => self.walk_tree(tree, seen, visit, filter, MAX_TREE_DEPTH, walked), 896 } 897 } 898 899 fn walk_tree_shared( 900 &self, 901 tree: gix::ObjectId, 902 seen: &scc::HashSet<gix::ObjectId>, 903 out: &mut Vec<Oid>, 904 nesting: usize, 905 walk: &mut SharedWalk, 906 ) -> Result<(), GitError> { 907 if nesting == 0 { 908 return Err(GitError::DepthExceeded("tree nesting")); 909 } 910 if tree == gix::ObjectId::empty_tree(self.git().object_hash()) { 911 return Ok(()); 912 } 913 if seen.insert_sync(tree).is_err() { 914 return Ok(()); 915 } 916 out.push(Oid::from(tree)); 917 walk.tick()?; 918 let object = self.load_object(Oid::from(tree))?; 919 let decoded = object 920 .try_into_tree() 921 .map_err(|error| GitError::Decode(error.to_string()))?; 922 let decoded = decoded 923 .decode() 924 .map_err(|error| GitError::Decode(error.to_string()))?; 925 decoded.entries.iter().try_for_each(|entry| { 926 let oid = entry.oid.to_owned(); 927 match entry.mode.kind() { 928 gix::objs::tree::EntryKind::Tree => { 929 self.walk_tree_shared(oid, seen, out, nesting - 1, walk) 930 } 931 gix::objs::tree::EntryKind::Commit => Ok(()), 932 _ => { 933 if seen.insert_sync(oid).is_ok() { 934 out.push(Oid::from(oid)); 935 walk.tick()?; 936 } 937 Ok(()) 938 } 939 } 940 }) 941 } 942 943 fn walk_send_trees( 944 &self, 945 send: &[(Oid, gix::ObjectId, Vec<gix::ObjectId>)], 946 seen: HashSet<gix::ObjectId>, 947 mut out: Vec<Oid>, 948 budget: PackBudget, 949 ) -> Result<Vec<Oid>, GitError> { 950 let seen: scc::HashSet<gix::ObjectId> = seen.into_iter().collect(); 951 let counter = AtomicUsize::new(out.len()); 952 let path = self.path().to_owned(); 953 let walk = 954 |batch: &[(Oid, gix::ObjectId, Vec<gix::ObjectId>)]| -> Result<Vec<Oid>, GitError> { 955 let local = Repo::open(&path)?; 956 let mut walk = SharedWalk::new(&counter, &budget); 957 batch.iter().try_fold( 958 Vec::new(), 959 |mut acc, (commit, tree, _)| -> Result<Vec<Oid>, GitError> { 960 if seen.insert_sync(commit.object_id()).is_ok() { 961 acc.push(*commit); 962 } 963 local.walk_tree_shared( 964 *tree, 965 &seen, 966 &mut acc, 967 MAX_TREE_DEPTH, 968 &mut walk, 969 )?; 970 Ok(acc) 971 }, 972 ) 973 }; 974 out.extend(knot_resource::map_chunks(send, walk)?); 975 Ok(out) 976 } 977 978 fn collect_direct( 979 &self, 980 oid: Oid, 981 seen: &mut HashSet<gix::ObjectId>, 982 expanded: &mut HashMap<gix::ObjectId, TreeDepth>, 983 visit: &mut dyn FnMut(Oid), 984 filter: Filter, 985 walked: &mut Walked, 986 ) -> Result<(), GitError> { 987 let object = self.load_object(oid)?; 988 match (object.kind, filter) { 989 (gix::object::Kind::Tree, Filter::TreeDepth(max)) => self.walk_tree_depth( 990 oid.object_id(), 991 max.deeper(), 992 seen, 993 expanded, 994 visit, 995 MAX_TREE_DEPTH, 996 walked, 997 ), 998 (gix::object::Kind::Tree, _) => { 999 self.walk_tree(oid.object_id(), seen, visit, filter, MAX_TREE_DEPTH, walked) 1000 } 1001 _ => { 1002 if seen.insert(oid.object_id()) { 1003 visit(oid); 1004 walked.tick()?; 1005 } 1006 Ok(()) 1007 } 1008 } 1009 } 1010 1011 fn commit_time(&self, commit: gix::ObjectId) -> Result<UnixSeconds, GitError> { 1012 let object = self.load_object(Oid::from(commit))?; 1013 let commit = object.try_into_commit().map_err(|_| GitError::ObjectType { 1014 oid: Oid::from(commit), 1015 expected: "commit", 1016 })?; 1017 let time = commit 1018 .committer() 1019 .map_err(|error| GitError::Decode(error.to_string()))? 1020 .time() 1021 .map_err(|error| GitError::Decode(error.to_string()))?; 1022 Ok(UnixSeconds::new(time.seconds)) 1023 } 1024 1025 pub fn shallow_walk( 1026 &self, 1027 wants: Wants<'_>, 1028 deepen: &Deepen, 1029 client_shallow: ShallowCommits<'_>, 1030 ) -> Result<ShallowPlan, GitError> { 1031 let want_commits: Vec<gix::ObjectId> = wants 1032 .as_slice() 1033 .iter() 1034 .map(|want| self.peel(want.object_id(), &mut Vec::new(), MAX_TAG_DEPTH)) 1035 .collect::<Result<Vec<_>, _>>()? 1036 .into_iter() 1037 .filter_map(|peeled| match peeled { 1038 Peeled::Commit(commit) => Some(commit), 1039 Peeled::Direct(_) => None, 1040 }) 1041 .collect(); 1042 1043 let excluded: HashSet<gix::ObjectId> = if deepen.not.is_empty() { 1044 HashSet::new() 1045 } else { 1046 self.rev_walk(Wants::new(&deepen.not), Haves::new(&[]))? 1047 .into_iter() 1048 .map(Oid::object_id) 1049 .collect() 1050 }; 1051 1052 let drop = |oid: gix::ObjectId, depth: CommitDepth| -> Result<bool, GitError> { 1053 if excluded.contains(&oid) { 1054 return Ok(true); 1055 } 1056 if let Some(max) = deepen.depth 1057 && depth > max 1058 { 1059 return Ok(true); 1060 } 1061 if let Some(since) = deepen.since 1062 && self.commit_time(oid)? < since 1063 { 1064 return Ok(true); 1065 } 1066 Ok(false) 1067 }; 1068 1069 let grafts = self.shallow_grafts()?; 1070 let mut min_depth: HashMap<gix::ObjectId, CommitDepth> = HashMap::new(); 1071 let mut parents_of: HashMap<gix::ObjectId, Vec<gix::ObjectId>> = HashMap::new(); 1072 let mut queue: VecDeque<(gix::ObjectId, CommitDepth)> = want_commits 1073 .iter() 1074 .map(|commit| (*commit, CommitDepth::new(1))) 1075 .collect(); 1076 if deepen.relative { 1077 client_shallow 1078 .as_slice() 1079 .iter() 1080 .for_each(|oid| queue.push_back((oid.object_id(), CommitDepth::new(0)))); 1081 } 1082 while let Some((commit, depth)) = queue.pop_front() { 1083 if drop(commit, depth)? { 1084 continue; 1085 } 1086 if min_depth.get(&commit).is_some_and(|seen| *seen <= depth) { 1087 continue; 1088 } 1089 min_depth.insert(commit, depth); 1090 let (_, parents) = self.commit_tree_and_parents(Oid::from(commit))?; 1091 if !grafts.contains(&commit) { 1092 parents 1093 .iter() 1094 .for_each(|parent| queue.push_back((*parent, depth.deeper()))); 1095 } 1096 parents_of.insert(commit, parents); 1097 } 1098 1099 let included: HashSet<gix::ObjectId> = min_depth.keys().copied().collect(); 1100 let boundary: HashSet<gix::ObjectId> = included 1101 .iter() 1102 .filter(|commit| { 1103 parents_of 1104 .get(*commit) 1105 .is_some_and(|parents| parents.iter().any(|parent| !included.contains(parent))) 1106 }) 1107 .copied() 1108 .collect(); 1109 1110 let commits: Vec<Oid> = min_depth.keys().map(|oid| Oid::from(*oid)).collect(); 1111 let shallow: Vec<Oid> = boundary.iter().map(|oid| Oid::from(*oid)).collect(); 1112 let unshallow: Vec<Oid> = client_shallow 1113 .as_slice() 1114 .iter() 1115 .filter(|oid| { 1116 min_depth.contains_key(&oid.object_id()) && !boundary.contains(&oid.object_id()) 1117 }) 1118 .copied() 1119 .collect(); 1120 Ok(ShallowPlan { 1121 commits, 1122 shallow, 1123 unshallow, 1124 }) 1125 } 1126 1127 pub fn select_shallow_objects( 1128 &self, 1129 wants: Wants, 1130 commits: ShallowCommits, 1131 haves: Haves, 1132 filter: Filter, 1133 budget: PackBudget, 1134 ) -> Result<PackSelection, GitError> { 1135 let wants = wants.as_slice(); 1136 let commits = commits.as_slice(); 1137 let haves = haves.as_slice(); 1138 let mut walked = Walked::new(budget); 1139 let mut seen: HashSet<gix::ObjectId> = HashSet::new(); 1140 let mut expanded: HashMap<gix::ObjectId, TreeDepth> = HashMap::new(); 1141 let mut have_commits: Vec<gix::ObjectId> = Vec::new(); 1142 haves 1143 .iter() 1144 .filter(|have| self.contains(**have)) 1145 .try_for_each(|have| -> Result<(), GitError> { 1146 match self.peel(have.object_id(), &mut Vec::new(), MAX_TAG_DEPTH)? { 1147 Peeled::Commit(commit) => { 1148 have_commits.push(commit); 1149 let tree = self.commit_tree(Oid::from(commit))?; 1150 self.walk_tree( 1151 tree, 1152 &mut seen, 1153 &mut |_| {}, 1154 Filter::None, 1155 MAX_TREE_DEPTH, 1156 &mut walked, 1157 ) 1158 } 1159 Peeled::Direct(direct) => self.collect_direct( 1160 Oid::from(direct), 1161 &mut seen, 1162 &mut expanded, 1163 &mut |_| {}, 1164 Filter::None, 1165 &mut walked, 1166 ), 1167 } 1168 })?; 1169 let client_has: HashSet<Oid> = seen 1170 .iter() 1171 .copied() 1172 .chain(have_commits) 1173 .map(Oid::from) 1174 .collect(); 1175 1176 let mut want_tags = Vec::new(); 1177 wants.iter().try_for_each(|want| -> Result<(), GitError> { 1178 self.peel(want.object_id(), &mut want_tags, MAX_TAG_DEPTH) 1179 .map(|_| ()) 1180 })?; 1181 1182 let mut out: Vec<Oid> = Vec::new(); 1183 want_tags 1184 .iter() 1185 .try_for_each(|tag| -> Result<(), GitError> { 1186 if seen.insert(tag.object_id()) { 1187 out.push(*tag); 1188 walked.tick()?; 1189 } 1190 Ok(()) 1191 })?; 1192 commits 1193 .iter() 1194 .try_for_each(|commit| -> Result<(), GitError> { 1195 if seen.insert(commit.object_id()) { 1196 out.push(*commit); 1197 walked.tick()?; 1198 } 1199 let tree = self.commit_tree(*commit)?; 1200 self.walk_root_tree( 1201 tree, 1202 &mut seen, 1203 &mut expanded, 1204 &mut |oid| out.push(oid), 1205 filter, 1206 &mut walked, 1207 ) 1208 })?; 1209 Ok(PackSelection { 1210 send: out, 1211 client_has, 1212 }) 1213 } 1214 1215 pub fn select_pack_objects(&self, wants: Wants, haves: Haves) -> Result<Vec<Oid>, GitError> { 1216 self.select_pack_objects_filtered(wants, haves, Filter::None, PackBudget::unbounded()) 1217 .map(|selection| selection.send) 1218 } 1219 1220 pub fn clone_roots(&self, wants: &[Oid], budget: PackBudget) -> Result<Vec<Oid>, GitError> { 1221 let mut walked = Walked::new(budget); 1222 let mut want_tags = Vec::new(); 1223 let mut want_commits = Vec::new(); 1224 let mut want_direct = Vec::new(); 1225 wants.iter().try_for_each(|want| -> Result<(), GitError> { 1226 match self.peel(want.object_id(), &mut want_tags, MAX_TAG_DEPTH)? { 1227 Peeled::Commit(commit) => want_commits.push(Oid::from(commit)), 1228 Peeled::Direct(direct) => want_direct.push(Oid::from(direct)), 1229 } 1230 Ok(()) 1231 })?; 1232 let commits = 1233 self.rev_walk_each(Wants::new(&want_commits), Haves::new(&[]), &mut walked)?; 1234 Ok(want_tags 1235 .into_iter() 1236 .chain(commits) 1237 .chain(want_direct) 1238 .collect()) 1239 } 1240 1241 pub fn reachable_commits( 1242 &self, 1243 tips: &[Oid], 1244 budget: PackBudget, 1245 ) -> Result<HashSet<Oid>, GitError> { 1246 let mut walked = Walked::new(budget); 1247 let commits: Vec<Oid> = self 1248 .peel_to_commits(tips.iter().copied())? 1249 .into_iter() 1250 .map(Oid::from) 1251 .collect(); 1252 Ok(self 1253 .rev_walk_each(Wants::new(&commits), Haves::new(&[]), &mut walked)? 1254 .into_iter() 1255 .collect()) 1256 } 1257 1258 fn peel_to_commits( 1259 &self, 1260 oids: impl Iterator<Item = Oid>, 1261 ) -> Result<Vec<gix::ObjectId>, GitError> { 1262 oids.map(|oid| self.peel(oid.object_id(), &mut Vec::new(), MAX_TAG_DEPTH)) 1263 .filter_map(|peeled| match peeled { 1264 Ok(Peeled::Commit(commit)) => Some(Ok(commit)), 1265 Ok(Peeled::Direct(_)) => None, 1266 Err(error) => Some(Err(error)), 1267 }) 1268 .collect() 1269 } 1270 1271 pub fn wants_satisfied_by(&self, wants: Wants, haves: Haves) -> Result<bool, GitError> { 1272 let wants = wants.as_slice(); 1273 let haves = haves.as_slice(); 1274 let commons: HashSet<gix::ObjectId> = self 1275 .peel_to_commits(haves.iter().copied().filter(|have| self.contains(*have)))? 1276 .into_iter() 1277 .collect(); 1278 if commons.is_empty() { 1279 return Ok(false); 1280 } 1281 let oldest = commons 1282 .iter() 1283 .map(|commit| self.commit_time(*commit)) 1284 .collect::<Result<Vec<_>, _>>()? 1285 .into_iter() 1286 .min() 1287 .unwrap_or(UnixSeconds::new(i64::MIN)); 1288 let grafts = self.shallow_grafts()?; 1289 1290 let reaches_a_common = |want: gix::ObjectId| -> Result<bool, GitError> { 1291 let mut seen: HashSet<gix::ObjectId> = HashSet::new(); 1292 let mut queue: VecDeque<gix::ObjectId> = VecDeque::from([want]); 1293 while let Some(commit) = queue.pop_front() { 1294 if commons.contains(&commit) { 1295 return Ok(true); 1296 } 1297 if !seen.insert(commit) || grafts.contains(&commit) { 1298 continue; 1299 } 1300 if self.commit_time(commit)? < oldest { 1301 continue; 1302 } 1303 let (_, parents) = self.commit_tree_and_parents(Oid::from(commit))?; 1304 queue.extend(parents); 1305 } 1306 Ok(false) 1307 }; 1308 1309 self.peel_to_commits(wants.iter().copied())? 1310 .into_iter() 1311 .try_fold(true, |all, want| Ok(all && reaches_a_common(want)?)) 1312 } 1313 1314 pub fn select_pack_objects_filtered( 1315 &self, 1316 wants: Wants, 1317 haves: Haves, 1318 filter: Filter, 1319 budget: PackBudget, 1320 ) -> Result<PackSelection, GitError> { 1321 let wants = wants.as_slice(); 1322 let haves = haves.as_slice(); 1323 let mut walked = Walked::new(budget); 1324 let mut expanded: HashMap<gix::ObjectId, TreeDepth> = HashMap::new(); 1325 let mut want_tags = Vec::new(); 1326 let mut want_commits = Vec::new(); 1327 let mut want_direct = Vec::new(); 1328 wants.iter().try_for_each(|want| -> Result<(), GitError> { 1329 match self.peel(want.object_id(), &mut want_tags, MAX_TAG_DEPTH)? { 1330 Peeled::Commit(commit) => want_commits.push(Oid::from(commit)), 1331 Peeled::Direct(direct) => want_direct.push(Oid::from(direct)), 1332 } 1333 Ok(()) 1334 })?; 1335 1336 let mut have_tags = Vec::new(); 1337 let mut have_commits = Vec::new(); 1338 let mut have_direct = Vec::new(); 1339 haves 1340 .iter() 1341 .filter(|have| self.contains(**have)) 1342 .try_for_each(|have| -> Result<(), GitError> { 1343 match self.peel(have.object_id(), &mut have_tags, MAX_TAG_DEPTH)? { 1344 Peeled::Commit(commit) => have_commits.push(Oid::from(commit)), 1345 Peeled::Direct(direct) => have_direct.push(Oid::from(direct)), 1346 } 1347 Ok(()) 1348 })?; 1349 1350 let grafts = self.shallow_grafts()?; 1351 let send: Vec<(Oid, gix::ObjectId, Vec<gix::ObjectId>)> = self 1352 .rev_walk_each( 1353 Wants::new(&want_commits), 1354 Haves::new(&have_commits), 1355 &mut walked, 1356 )? 1357 .into_iter() 1358 .map(|commit| { 1359 let (tree, parents) = self.commit_tree_and_parents(commit)?; 1360 let parents = match grafts.contains(&commit.object_id()) { 1361 true => Vec::new(), 1362 false => parents, 1363 }; 1364 Ok::<_, GitError>((commit, tree, parents)) 1365 }) 1366 .collect::<Result<_, _>>()?; 1367 let send_set: HashSet<gix::ObjectId> = send 1368 .iter() 1369 .map(|(commit, _, _)| commit.object_id()) 1370 .collect(); 1371 1372 let mut uninteresting: HashSet<gix::ObjectId> = 1373 have_tags.iter().map(|tag| tag.object_id()).collect(); 1374 let boundary_commits: Vec<gix::ObjectId> = have_commits 1375 .iter() 1376 .map(|commit| commit.object_id()) 1377 .chain( 1378 send.iter() 1379 .flat_map(|(_, _, parents)| parents.iter().copied()) 1380 .filter(|parent| !send_set.contains(parent)), 1381 ) 1382 .collect(); 1383 boundary_commits 1384 .iter() 1385 .try_for_each(|commit| -> Result<(), GitError> { 1386 let tree = self.commit_tree(Oid::from(*commit))?; 1387 self.walk_tree( 1388 tree, 1389 &mut uninteresting, 1390 &mut |_| {}, 1391 Filter::None, 1392 MAX_TREE_DEPTH, 1393 &mut walked, 1394 ) 1395 })?; 1396 have_direct.iter().try_for_each(|direct| { 1397 self.collect_direct( 1398 *direct, 1399 &mut uninteresting, 1400 &mut expanded, 1401 &mut |_| {}, 1402 Filter::None, 1403 &mut walked, 1404 ) 1405 })?; 1406 let client_has: HashSet<Oid> = uninteresting 1407 .iter() 1408 .copied() 1409 .chain(boundary_commits) 1410 .map(Oid::from) 1411 .collect(); 1412 1413 let mut seen = uninteresting; 1414 let mut out: Vec<Oid> = Vec::new(); 1415 want_tags 1416 .iter() 1417 .try_for_each(|tag| -> Result<(), GitError> { 1418 if seen.insert(tag.object_id()) { 1419 out.push(*tag); 1420 walked.tick()?; 1421 } 1422 Ok(()) 1423 })?; 1424 if matches!(filter, Filter::None) 1425 && want_direct.is_empty() 1426 && send.len() >= PARALLEL_SELECT_MIN 1427 { 1428 out = self.walk_send_trees(&send, seen, out, walked.budget)?; 1429 } else { 1430 send.iter() 1431 .try_for_each(|(commit, tree, _)| -> Result<(), GitError> { 1432 if seen.insert(commit.object_id()) { 1433 out.push(*commit); 1434 } 1435 self.walk_root_tree( 1436 *tree, 1437 &mut seen, 1438 &mut expanded, 1439 &mut |oid| out.push(oid), 1440 filter, 1441 &mut walked, 1442 ) 1443 })?; 1444 want_direct.iter().try_for_each(|direct| { 1445 self.collect_direct( 1446 *direct, 1447 &mut seen, 1448 &mut expanded, 1449 &mut |oid| out.push(oid), 1450 filter, 1451 &mut walked, 1452 ) 1453 })?; 1454 } 1455 Ok(PackSelection { 1456 send: out, 1457 client_has, 1458 }) 1459 } 1460} 1461 1462#[cfg(test)] 1463mod tests { 1464 use std::os::unix::fs::PermissionsExt; 1465 1466 use knot_types::RepoDid; 1467 1468 use super::*; 1469 use crate::Layout; 1470 1471 fn seeded() -> (tempfile::TempDir, Layout, RepoDid) { 1472 let dir = tempfile::tempdir().unwrap(); 1473 let layout = Layout::new(dir.path()); 1474 let did = RepoDid::new("did:plc:squid").unwrap(); 1475 layout.create(&did).unwrap(); 1476 (dir, layout, did) 1477 } 1478 1479 #[test] 1480 fn open_blob_streams_a_loose_blob() { 1481 let (_dir, layout, did) = seeded(); 1482 let content: Vec<u8> = (0..8192u32).map(|byte| byte as u8).collect(); 1483 let repo = layout.open(&did).unwrap(); 1484 let oid = Oid::from(repo.git().write_blob(&content).unwrap().detach()); 1485 1486 let reread = layout.open(&did).unwrap(); 1487 let (size, mut reader) = reread.open_blob(oid).unwrap(); 1488 assert_eq!(size, content.len() as u64); 1489 assert!(matches!(reader, BlobReader::Loose(_))); 1490 let mut buf = Vec::new(); 1491 reader.read_to_end(&mut buf).unwrap(); 1492 assert_eq!(buf, content); 1493 } 1494 1495 #[test] 1496 fn open_blob_rejects_a_non_blob() { 1497 let (_dir, layout, did) = seeded(); 1498 let repo = layout.open(&did).unwrap(); 1499 let tree = repo 1500 .git() 1501 .write_object(gix::objs::Tree { 1502 entries: Vec::new(), 1503 }) 1504 .unwrap() 1505 .detach(); 1506 assert!(matches!( 1507 repo.open_blob(Oid::from(tree)), 1508 Err(GitError::ObjectType { 1509 expected: "blob", 1510 .. 1511 }) 1512 )); 1513 } 1514 1515 #[test] 1516 fn corrupt_loose_object_is_a_typed_error_not_a_panic() { 1517 let (_dir, layout, did) = seeded(); 1518 let repo = layout.open(&did).unwrap(); 1519 let oid = Oid::from( 1520 repo.git() 1521 .write_blob(b"hello streaming world\n") 1522 .unwrap() 1523 .detach(), 1524 ); 1525 let loose = repo.loose_object_path(oid); 1526 1527 std::fs::set_permissions(&loose, std::fs::Permissions::from_mode(0o644)).unwrap(); 1528 std::fs::write(&loose, b"this isn't a valid zlib object").unwrap(); 1529 1530 let reopened = layout.open(&did).unwrap(); 1531 assert!(matches!( 1532 reopened.read_blob(oid), 1533 Err(GitError::Corrupt { .. }) 1534 )); 1535 assert!(matches!( 1536 reopened.open_blob(oid), 1537 Err(GitError::Corrupt { .. }) 1538 )); 1539 } 1540 1541 #[test] 1542 fn missing_object_is_not_found() { 1543 let (_dir, layout, did) = seeded(); 1544 let repo = layout.open(&did).unwrap(); 1545 let absent = Oid::from_hex("dead00000000000000000000000000000000beef").unwrap(); 1546 assert!(matches!( 1547 repo.read_blob(absent), 1548 Err(GitError::ObjectNotFound(_)) 1549 )); 1550 assert!(matches!( 1551 repo.open_blob(absent), 1552 Err(GitError::ObjectNotFound(_)) 1553 )); 1554 } 1555}