1use objects::{object::StateId, store::ObjectStore};
10
11use crate::{
12 ObjectInfo, ObjectType, ObjectTypeBucket, PlannedObject, Result, StateClosureOptions,
13 enumerate_state_closure_plan_with_options,
14};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
17pub enum GitLaneTransferIntent {
18 #[default]
21 HeddleObjectsOnly,
22 ExistingImplementation,
26 BlockedOnSleyReachablePackPlanning,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct RepositoryTransferPlan<T = PlannedObject> {
34 pub partitions: TransferPartitions<T>,
35 pub stats: TransferPlanStats,
36 pub git_lane: GitLaneTransferIntent,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct TransferPartitions<T = PlannedObject> {
41 pub packable_objects: Vec<T>,
42 pub sidecar_objects: Vec<T>,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
46pub struct TransferPlanStats {
47 pub total_objects: usize,
48 pub packable_objects: usize,
49 pub sidecar_objects: usize,
50 pub blobs: usize,
51 pub trees: usize,
52 pub states: usize,
53 pub actions: usize,
54 pub redactions: usize,
55 pub state_visibilities: usize,
56 pub state_attachments: usize,
57 pub key_bindings: usize,
58}
59
60impl RepositoryTransferPlan<PlannedObject> {
61 pub fn from_state_closure_plan(
62 store: &impl ObjectStore,
63 root: StateId,
64 options: StateClosureOptions,
65 git_lane: GitLaneTransferIntent,
66 ) -> Result<Self> {
67 let objects = enumerate_state_closure_plan_with_options(store, root, options)?;
68 Ok(Self::from_planned_objects(objects, git_lane))
69 }
70
71 pub fn from_planned_objects(
72 objects: impl IntoIterator<Item = PlannedObject>,
73 git_lane: GitLaneTransferIntent,
74 ) -> Self {
75 build_plan(objects, planned_object_type, git_lane)
76 }
77}
78
79impl RepositoryTransferPlan<ObjectInfo> {
80 pub fn from_object_infos(
81 objects: impl IntoIterator<Item = ObjectInfo>,
82 git_lane: GitLaneTransferIntent,
83 ) -> Self {
84 build_plan(objects, object_info_type, git_lane)
85 }
86}
87
88impl<T> RepositoryTransferPlan<T> {
89 pub fn requires_native_pack(&self, include_full_closure: bool) -> bool {
90 include_full_closure || self.stats.packable_objects > 0
91 }
92
93 pub fn is_heddle_only(&self) -> bool {
94 self.git_lane == GitLaneTransferIntent::HeddleObjectsOnly
95 }
96}
97
98impl<T> TransferPartitions<T> {
99 pub fn is_empty(&self) -> bool {
100 self.packable_objects.is_empty() && self.sidecar_objects.is_empty()
101 }
102
103 pub fn len(&self) -> usize {
104 self.packable_objects.len() + self.sidecar_objects.len()
105 }
106
107 pub fn iter(&self) -> impl Iterator<Item = &T> {
108 self.packable_objects
109 .iter()
110 .chain(self.sidecar_objects.iter())
111 }
112
113 pub fn is_sidecar_object_type(obj_type: ObjectType) -> bool {
114 !obj_type.packable()
115 }
116}
117
118impl<T> Default for TransferPartitions<T> {
119 fn default() -> Self {
120 Self {
121 packable_objects: Vec::new(),
122 sidecar_objects: Vec::new(),
123 }
124 }
125}
126
127impl TransferPlanStats {
128 fn record(&mut self, obj_type: ObjectType) {
129 self.total_objects += 1;
130 if TransferPartitions::<()>::is_sidecar_object_type(obj_type) {
131 self.sidecar_objects += 1;
132 } else {
133 self.packable_objects += 1;
134 }
135 match obj_type.bucket() {
136 ObjectTypeBucket::Blob => self.blobs += 1,
137 ObjectTypeBucket::Tree => self.trees += 1,
138 ObjectTypeBucket::State => self.states += 1,
139 ObjectTypeBucket::Action => self.actions += 1,
140 ObjectTypeBucket::Redaction => self.redactions += 1,
141 ObjectTypeBucket::StateVisibility => self.state_visibilities += 1,
142 ObjectTypeBucket::StateAttachment => self.state_attachments += 1,
143 ObjectTypeBucket::KeyBinding => self.key_bindings += 1,
144 }
145 }
146}
147
148fn build_plan<T>(
149 objects: impl IntoIterator<Item = T>,
150 object_type: fn(&T) -> ObjectType,
151 git_lane: GitLaneTransferIntent,
152) -> RepositoryTransferPlan<T> {
153 let mut partitions = TransferPartitions::default();
154 let mut stats = TransferPlanStats::default();
155
156 for object in objects {
157 let obj_type = object_type(&object);
158 stats.record(obj_type);
159 if TransferPartitions::<T>::is_sidecar_object_type(obj_type) {
160 partitions.sidecar_objects.push(object);
161 } else {
162 partitions.packable_objects.push(object);
163 }
164 }
165
166 RepositoryTransferPlan {
167 partitions,
168 stats,
169 git_lane,
170 }
171}
172
173fn planned_object_type(object: &PlannedObject) -> ObjectType {
174 object.obj_type
175}
176
177fn object_info_type(object: &ObjectInfo) -> ObjectType {
178 object.obj_type
179}
180
181#[cfg(test)]
182mod tests {
183 use objects::object::{ContentHash, StateId};
184
185 use super::*;
186 use crate::ObjectId;
187
188 fn hash(byte: u8) -> ContentHash {
189 ContentHash::from_bytes([byte; 32])
190 }
191
192 #[test]
193 fn partitions_split_native_pack_objects_from_sidecars() {
194 let state = StateId::from_bytes([9; 32]);
195 let plan = RepositoryTransferPlan::from_planned_objects(
196 vec![
197 PlannedObject {
198 id: ObjectId::Hash(hash(1)),
199 obj_type: ObjectType::Blob,
200 },
201 PlannedObject {
202 id: ObjectId::Hash(hash(2)),
203 obj_type: ObjectType::Tree,
204 },
205 PlannedObject {
206 id: ObjectId::Hash(hash(1)),
207 obj_type: ObjectType::Redaction,
208 },
209 PlannedObject {
210 id: ObjectId::StateId(state),
211 obj_type: ObjectType::StateVisibility,
212 },
213 PlannedObject {
214 id: ObjectId::Hash(hash(3)),
215 obj_type: ObjectType::KeyBinding,
216 },
217 ],
218 GitLaneTransferIntent::HeddleObjectsOnly,
219 );
220
221 assert_eq!(plan.partitions.packable_objects.len(), 2);
222 assert_eq!(plan.partitions.sidecar_objects.len(), 3);
223 assert_eq!(plan.stats.total_objects, 5);
224 assert_eq!(plan.stats.packable_objects, 2);
225 assert_eq!(plan.stats.sidecar_objects, 3);
226 assert_eq!(plan.stats.blobs, 1);
227 assert_eq!(plan.stats.trees, 1);
228 assert_eq!(plan.stats.redactions, 1);
229 assert_eq!(plan.stats.state_visibilities, 1);
230 assert_eq!(plan.stats.key_bindings, 1);
231 assert!(plan.requires_native_pack(false));
232 }
233
234 #[test]
235 fn sidecar_only_plan_does_not_require_native_pack() {
236 let state = StateId::from_bytes([3; 32]);
237 let plan = RepositoryTransferPlan::from_object_infos(
238 vec![ObjectInfo {
239 id: ObjectId::StateId(state),
240 obj_type: ObjectType::StateVisibility,
241 size: 128,
242 delta_base: None,
243 }],
244 GitLaneTransferIntent::HeddleObjectsOnly,
245 );
246
247 assert!(!plan.requires_native_pack(false));
248 assert!(plan.requires_native_pack(true));
249 assert_eq!(plan.stats.packable_objects, 0);
250 assert_eq!(plan.stats.sidecar_objects, 1);
251 }
252
253 #[test]
254 fn git_lane_intents_name_current_and_sley_gated_paths() {
255 let hosted = RepositoryTransferPlan::from_planned_objects(
256 Vec::<PlannedObject>::new(),
257 GitLaneTransferIntent::ExistingImplementation,
258 );
259 let sley_blocked = RepositoryTransferPlan::from_planned_objects(
260 Vec::<PlannedObject>::new(),
261 GitLaneTransferIntent::BlockedOnSleyReachablePackPlanning,
262 );
263
264 assert_eq!(
265 hosted.git_lane,
266 GitLaneTransferIntent::ExistingImplementation
267 );
268 assert_eq!(
269 sley_blocked.git_lane,
270 GitLaneTransferIntent::BlockedOnSleyReachablePackPlanning
271 );
272 assert!(!hosted.is_heddle_only());
273 assert!(!sley_blocked.is_heddle_only());
274 }
275}