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