Skip to main content

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}