This repository has no description
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}