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