git_zero_core/fetch.rs
1/// Partial-clone object policies. Explicitly wanted objects are retained.
2#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
3pub enum BlobFilter {
4 /// Keep the complete selected closure.
5 #[default]
6 All,
7 /// Omit blobs reached through trees or tags. An explicit blob want wins.
8 None,
9 /// Omit blobs of this size or larger, in bytes.
10 Limit(u64),
11 /// Omit every other kind, retaining explicitly wanted objects.
12 Type(ObjectKind),
13 /// Omit trees and blobs at this depth or deeper, from each root tree.
14 TreeDepth(u64),
15}
16
17impl BlobFilter {
18 /// Apply the policy at root-tree depth zero. Use `includes_at` when an
19 /// object is reached below a root tree.
20 pub const fn includes(self, info: ObjectInfo, explicitly_wanted: bool) -> bool {
21 self.includes_at(info, explicitly_wanted, 0)
22 }
23
24 /// Apply the policy at the minimum zero-based tree distance. Explicitly
25 /// requested objects bypass filtering; commits and tags have no tree depth.
26 pub const fn includes_at(
27 self,
28 info: ObjectInfo,
29 explicitly_wanted: bool,
30 tree_depth: u32,
31 ) -> bool {
32 if explicitly_wanted {
33 return true;
34 }
35 match self {
36 Self::All => true,
37 Self::None => !matches!(info.kind, ObjectKind::Blob),
38 Self::Limit(limit) => !matches!(info.kind, ObjectKind::Blob) || info.size < limit,
39 Self::Type(kind) => info.kind as u8 == kind as u8,
40 Self::TreeDepth(limit) => {
41 !matches!(info.kind, ObjectKind::Tree | ObjectKind::Blob)
42 || (tree_depth as u64) < limit
43 }
44 }
45 }
46
47 /// A tree at the exclusion depth contributes no children, including when
48 /// the tree itself is retained as an explicit want.
49 pub const fn follows_tree(self, tree_depth: u32) -> bool {
50 match self {
51 Self::TreeDepth(limit) => (tree_depth as u64) < limit,
52 _ => true,
53 }
54 }
55}
56
57/// Bounded history and object selection, sharing the ordinary graph-read budget.
58#[derive(Clone, Copy, Debug, Default)]
59pub struct FetchOptions {
60 /// Absolute commit depth from the wanted tips (1..=2147483647).
61 /// None preserves client shallow boundaries. A depth request retransmits
62 /// its bounded closure so shallow haves cannot hide newly requested history.
63 /// A relative request without client boundaries keeps common-history pruning.
64 pub depth: Option<u32>,
65 /// Add `depth` to the nearest client boundary measured from wanted tips
66 /// (Git 2.55). Inert without a depth; unrelated client boundaries are preserved.
67 /// Without a boundary in wanted history, select its complete closure.
68 pub deepen_relative: bool,
69 /// Apply the object policy after reachability and common-history exclusion.
70 /// Filtering does not change commit parents or tree bodies. The receiving
71 /// host must retain Git's shallow/promisor metadata when importing the pack.
72 pub filter: BlobFilter,
73}
74use crate::{Error, ObjectInfo, ObjectKind, Result};
75
76/// History decisions for an external, host-owned graph frontier. The host
77/// supplies the shortest commit distance from wanted roots (starting at one)
78/// and persists deduplication, exclusions and work budgets outside this value.
79#[derive(Clone, Copy, Debug, Default)]
80pub struct HistoryPolicy {
81 pub(crate) limit: Option<u32>,
82 pub(crate) client: bool,
83 pub(crate) distance: bool,
84}
85impl HistoryPolicy {
86 /// Measure distances to existing client boundaries without crossing them.
87 /// A host only needs commit-parent and tag-target edges during this probe.
88 pub const fn boundary_probe() -> Self {
89 Self {
90 limit: None,
91 client: true,
92 distance: true,
93 }
94 }
95 pub const fn follows_parents(self, depth: u32, client_boundary: bool) -> bool {
96 !((self.client && client_boundary)
97 || match self.limit {
98 Some(limit) => depth >= limit,
99 None => false,
100 })
101 }
102 pub const fn parent_depth(self, depth: u32) -> u32 {
103 if self.limit.is_some() || self.distance {
104 depth.saturating_add(1)
105 } else {
106 1
107 }
108 }
109}
110
111impl FetchOptions {
112 /// Shallow haves cannot exclude the newly requested history. A relative
113 /// request from a complete client preserves ordinary common exclusion.
114 pub const fn deepens_history(self, has_client_boundaries: bool) -> bool {
115 self.depth.is_some() && (!self.deepen_relative || has_client_boundaries)
116 }
117 /// Derive the wanted-history cut after a relative boundary probe. None
118 /// means no client boundary occurs in wanted history, leaving it complete.
119 pub fn history(
120 self,
121 has_client_boundaries: bool,
122 nearest_boundary_depth: Option<u32>,
123 ) -> Result<HistoryPolicy> {
124 if self
125 .depth
126 .is_some_and(|depth| depth == 0 || depth > i32::MAX as u32)
127 || nearest_boundary_depth == Some(0)
128 {
129 return Err(Error::InvalidRequest);
130 }
131 let limit = match self
132 .depth
133 .filter(|_| self.deepens_history(has_client_boundaries))
134 {
135 Some(depth) if self.deepen_relative => nearest_boundary_depth
136 .map(|boundary| boundary.checked_add(depth).ok_or(Error::LimitExceeded))
137 .transpose()?,
138 depth => depth,
139 };
140 Ok(HistoryPolicy {
141 limit,
142 client: self.depth.is_none(),
143 distance: false,
144 })
145 }
146}