1use std::collections::{HashSet, VecDeque};
3
4use serde::{Deserialize, Serialize};
5
6use crate::{
7 error::{HeddleError, Result},
8 object::{
9 AnnotatedTag, BindingDelta, ContentHash, RedactionsBlob, ReverseDependencyIndex,
10 SemanticEntryKind, SemanticIndexRoot, SemanticTreeNode, State, StateAttachment,
11 StateAttachmentBody, StateAttachmentId, StateAttachmentKind, StateId, TreeEntryTarget,
12 },
13 store::{ObjectStore, pack::ObjectType as PackObjectType},
14};
15
16#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
17pub enum ObjectId {
18 Hash(ContentHash),
19 StateId(StateId),
20 StateAttachment {
21 state: StateId,
22 id: StateAttachmentId,
23 kind: StateAttachmentKind,
29 },
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct ObjectInfo {
34 pub id: ObjectId,
35 pub obj_type: ObjectType,
36 pub size: u64,
37 pub delta_base: Option<ContentHash>,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
41pub struct PlannedObject {
42 pub id: ObjectId,
43 pub obj_type: ObjectType,
44}
45
46#[derive(Debug, Clone)]
47pub struct StateClosureTransferObjects {
48 pub planned_objects: Vec<PlannedObject>,
49 pub full_objects: Option<Vec<ObjectInfo>>,
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
53pub enum ObjectType {
54 Blob,
55 Tree,
56 State,
57 Action,
58 AnnotatedTag,
59 Redaction,
65 Purge,
69 StateVisibility,
76 StateAttachment,
77 KeyBinding,
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
85pub enum ObjectTypeBucket {
86 Blob,
87 Tree,
88 State,
89 Action,
90 AnnotatedTag,
91 Redaction,
92 Purge,
93 StateVisibility,
94 StateAttachment,
95 KeyBinding,
96}
97
98impl ObjectType {
99 pub fn wire_name(self) -> &'static str {
100 match self {
101 ObjectType::Blob => "blob",
102 ObjectType::Tree => "tree",
103 ObjectType::State => "state",
104 ObjectType::Action => "action",
105 ObjectType::AnnotatedTag => "annotated_tag",
106 ObjectType::Redaction => "redaction",
107 ObjectType::Purge => "purge",
108 ObjectType::StateVisibility => "state_visibility",
109 ObjectType::StateAttachment => "state_attachment",
110 ObjectType::KeyBinding => "key_binding",
111 }
112 }
113
114 pub fn from_wire(value: &str) -> Result<Self> {
115 match value {
116 "blob" => Ok(ObjectType::Blob),
117 "tree" => Ok(ObjectType::Tree),
118 "state" => Ok(ObjectType::State),
119 "action" => Ok(ObjectType::Action),
120 "annotated_tag" => Ok(ObjectType::AnnotatedTag),
121 "redaction" => Ok(ObjectType::Redaction),
122 "purge" => Ok(ObjectType::Purge),
123 "state_visibility" => Ok(ObjectType::StateVisibility),
124 "state_attachment" => Ok(ObjectType::StateAttachment),
125 "key_binding" => Ok(ObjectType::KeyBinding),
126 _ => Err(HeddleError::InvalidObject(format!(
127 "unknown object type: {value}"
128 ))),
129 }
130 }
131
132 pub fn packable(self) -> bool {
143 !matches!(
144 self,
145 ObjectType::Redaction
146 | ObjectType::Purge
147 | ObjectType::StateVisibility
148 | ObjectType::KeyBinding
149 )
150 }
151
152 pub fn packable_for_push(self) -> bool {
162 self.packable() && !matches!(self, ObjectType::StateAttachment)
163 }
164
165 pub fn packable_for_pull(self) -> bool {
172 self.packable()
173 }
174
175 pub fn pack_object_type(self) -> Result<PackObjectType> {
176 match self {
177 ObjectType::Blob => Ok(PackObjectType::Blob),
178 ObjectType::Tree => Ok(PackObjectType::Tree),
179 ObjectType::State => Ok(PackObjectType::State),
180 ObjectType::Action => Ok(PackObjectType::Action),
181 ObjectType::AnnotatedTag => Ok(PackObjectType::AnnotatedTag),
182 ObjectType::StateAttachment => Ok(PackObjectType::StateAttachment),
183 ObjectType::Redaction => Err(HeddleError::InvalidObject(
184 "Redaction sidecar records cannot be packed into the content-addressed object pack"
185 .to_string(),
186 )),
187 ObjectType::Purge => Err(HeddleError::InvalidObject(
188 "Purge sidecar records cannot be packed into the content-addressed object pack"
189 .to_string(),
190 )),
191 ObjectType::StateVisibility => Err(HeddleError::InvalidObject(
192 "StateVisibility sidecar records cannot be packed into the content-addressed object pack"
193 .to_string(),
194 )),
195 ObjectType::KeyBinding => Err(HeddleError::InvalidObject(
196 "KeyBinding registry objects cannot be packed into the content-addressed object pack"
197 .to_string(),
198 )),
199 }
200 }
201
202 pub fn bucket(self) -> ObjectTypeBucket {
203 match self {
204 ObjectType::Blob => ObjectTypeBucket::Blob,
205 ObjectType::Tree => ObjectTypeBucket::Tree,
206 ObjectType::State => ObjectTypeBucket::State,
207 ObjectType::Action => ObjectTypeBucket::Action,
208 ObjectType::AnnotatedTag => ObjectTypeBucket::AnnotatedTag,
209 ObjectType::Redaction => ObjectTypeBucket::Redaction,
210 ObjectType::Purge => ObjectTypeBucket::Purge,
211 ObjectType::StateVisibility => ObjectTypeBucket::StateVisibility,
212 ObjectType::StateAttachment => ObjectTypeBucket::StateAttachment,
213 ObjectType::KeyBinding => ObjectTypeBucket::KeyBinding,
214 }
215 }
216}
217
218#[derive(Debug, Clone, Default)]
219pub struct StateClosureOptions {
220 pub depth: Option<u32>,
221 pub exclude_states: Vec<StateId>,
222}
223
224pub fn enumerate_state_closure(
225 store: &impl ObjectStore,
226 state_id: StateId,
227) -> Result<Vec<ObjectInfo>> {
228 enumerate_state_closure_with_options(store, state_id, StateClosureOptions::default())
229}
230
231pub fn enumerate_state_closure_with_options(
232 store: &impl ObjectStore,
233 state_id: StateId,
234 options: StateClosureOptions,
235) -> Result<Vec<ObjectInfo>> {
236 let mut out = Vec::new();
237 walk_state_closure(store, state_id, options, |event| {
238 if let Some(info) = object_info_from_event(store, event)? {
239 out.push(info);
240 }
241 Ok(())
242 })?;
243 for (hash, tag) in annotated_tags_for_state(store, state_id)? {
244 out.push(annotated_tag_info(hash, &tag));
245 }
246
247 Ok(out)
248}
249
250pub fn enumerate_state_closure_plan(
251 store: &impl ObjectStore,
252 state_id: StateId,
253) -> Result<Vec<PlannedObject>> {
254 enumerate_state_closure_plan_with_options(store, state_id, StateClosureOptions::default())
255}
256
257pub fn enumerate_state_closure_plan_with_options(
258 store: &impl ObjectStore,
259 state_id: StateId,
260 options: StateClosureOptions,
261) -> Result<Vec<PlannedObject>> {
262 let mut out = Vec::new();
263 walk_state_closure(store, state_id, options, |event| {
264 if let Some(object) = planned_object_from_event(store, event)? {
265 out.push(object);
266 }
267 Ok(())
268 })?;
269 out.extend(
270 annotated_tags_for_state(store, state_id)?
271 .into_iter()
272 .map(|(hash, _)| PlannedObject {
273 id: ObjectId::Hash(hash),
274 obj_type: ObjectType::AnnotatedTag,
275 }),
276 );
277
278 Ok(out)
279}
280
281pub fn enumerate_state_closure_transfer_with_options(
282 store: &impl ObjectStore,
283 state_id: StateId,
284 options: StateClosureOptions,
285 full_descriptor_object_threshold: usize,
286) -> Result<StateClosureTransferObjects> {
287 let mut planned_objects = Vec::new();
288 let mut full_objects = Some(Vec::new());
289
290 walk_state_closure(store, state_id, options, |event| {
291 if let Some(object) = planned_object_from_event(store, event)? {
292 planned_objects.push(object);
293 }
294
295 if full_objects.is_some() && planned_objects.len() > full_descriptor_object_threshold {
296 full_objects = None;
297 }
298 if let Some(objects) = full_objects.as_mut()
299 && let Some(info) = object_info_from_event(store, event)?
300 {
301 objects.push(info);
302 }
303
304 Ok(())
305 })?;
306 let tags = annotated_tags_for_state(store, state_id)?;
307 planned_objects.extend(tags.iter().map(|(hash, _)| PlannedObject {
308 id: ObjectId::Hash(*hash),
309 obj_type: ObjectType::AnnotatedTag,
310 }));
311 if full_objects.is_some() && planned_objects.len() > full_descriptor_object_threshold {
312 full_objects = None;
313 }
314 if let Some(objects) = full_objects.as_mut() {
315 objects.extend(
316 tags.iter()
317 .map(|(hash, tag)| annotated_tag_info(*hash, tag)),
318 );
319 }
320
321 Ok(StateClosureTransferObjects {
322 planned_objects,
323 full_objects,
324 })
325}
326
327fn annotated_tags_for_state(
328 store: &impl ObjectStore,
329 state_id: StateId,
330) -> Result<Vec<(ContentHash, AnnotatedTag)>> {
331 let mut roots = Vec::new();
332 for hash in store.list_annotated_tags()? {
333 let Some(tag) = store.get_annotated_tag(&hash)? else {
334 continue;
335 };
336 if tag
337 .marker()
338 .is_some_and(|marker| marker.peeled_state == state_id)
339 {
340 roots.push((hash, tag));
341 }
342 }
343
344 let mut tags = Vec::new();
345 let mut seen = HashSet::new();
346 let mut stack = roots;
347 while let Some((hash, tag)) = stack.pop() {
348 if !seen.insert(hash) {
349 continue;
350 }
351 if let Some(inner_hash) = tag.target_tag() {
352 let inner = store.get_annotated_tag(&inner_hash)?.ok_or_else(|| {
353 HeddleError::NotFound(format!(
354 "annotated tag {hash} references missing inner tag {inner_hash}"
355 ))
356 })?;
357 stack.push((inner_hash, inner));
358 }
359 tags.push((hash, tag));
360 }
361 Ok(tags)
362}
363
364fn annotated_tag_info(hash: ContentHash, tag: &AnnotatedTag) -> ObjectInfo {
365 ObjectInfo {
366 id: ObjectId::Hash(hash),
367 obj_type: ObjectType::AnnotatedTag,
368 size: tag.encode_current_msgpack().len() as u64,
369 delta_base: None,
370 }
371}
372
373pub fn enumerate_state_closure_transfer_from_boundaries(
380 store: &impl ObjectStore,
381 state_id: StateId,
382 boundary_states: &[StateId],
383 full_descriptor_object_threshold: usize,
384) -> Result<StateClosureTransferObjects> {
385 let mut planned_objects = Vec::new();
386 let mut full_objects = Some(Vec::new());
387 let excluded_states = boundary_states.iter().copied().collect();
388
389 walk_state_closure_with_exclusions(
390 store,
391 state_id,
392 None,
393 excluded_states,
394 HashSet::new(),
395 |event| {
396 if let Some(object) = planned_object_from_event(store, event)? {
397 planned_objects.push(object);
398 }
399
400 if full_objects.is_some() && planned_objects.len() > full_descriptor_object_threshold {
401 full_objects = None;
402 }
403 if let Some(objects) = full_objects.as_mut()
404 && let Some(info) = object_info_from_event(store, event)?
405 {
406 objects.push(info);
407 }
408
409 Ok(())
410 },
411 )?;
412
413 Ok(StateClosureTransferObjects {
414 planned_objects,
415 full_objects,
416 })
417}
418
419#[derive(Debug, Clone, Copy)]
420enum StateClosureEvent<'a> {
421 State {
422 id: StateId,
423 state: &'a State,
424 },
425 Tree {
426 hash: ContentHash,
427 tree: &'a crate::object::Tree,
428 },
429 Blob {
430 hash: ContentHash,
431 },
432 Redaction {
433 blob: ContentHash,
434 },
435 StateVisibility {
436 state: StateId,
437 },
438 StateAttachment {
439 state: StateId,
440 attachment: &'a StateAttachment,
441 },
442 ExcludedState {
443 id: StateId,
444 },
445 ExcludedHash {
446 hash: ContentHash,
447 },
448}
449
450fn walk_state_closure(
451 store: &impl ObjectStore,
452 state_id: StateId,
453 options: StateClosureOptions,
454 visit: impl for<'event> FnMut(StateClosureEvent<'event>) -> Result<()>,
455) -> Result<()> {
456 let (excluded_states, excluded_hashes) = collect_excluded(store, &options.exclude_states)?;
457
458 walk_state_closure_with_exclusions(
459 store,
460 state_id,
461 options.depth,
462 excluded_states,
463 excluded_hashes,
464 visit,
465 )
466}
467
468fn walk_state_closure_with_exclusions(
469 store: &impl ObjectStore,
470 state_id: StateId,
471 max_depth: Option<u32>,
472 excluded_states: HashSet<StateId>,
473 excluded_hashes: HashSet<ContentHash>,
474 mut visit: impl for<'event> FnMut(StateClosureEvent<'event>) -> Result<()>,
475) -> Result<()> {
476 let mut seen_states: HashSet<StateId> = HashSet::new();
477 let mut seen_hashes: HashSet<ContentHash> = HashSet::new();
478 let mut queue: VecDeque<(StateId, u32)> = VecDeque::new();
479 queue.push_back((state_id, 0));
480
481 while let Some((id, depth)) = queue.pop_front() {
482 if excluded_states.contains(&id) {
483 visit(StateClosureEvent::ExcludedState { id })?;
484 continue;
485 }
486 if !seen_states.insert(id) {
487 continue;
488 }
489
490 let state = store
491 .get_state(&id)?
492 .ok_or_else(|| HeddleError::MissingObject {
493 object_type: "state".to_string(),
494 id: id.to_string(),
495 })?;
496
497 visit(StateClosureEvent::State { id, state: &state })?;
498 if store.has_state_visibility_for_state(&id)? {
499 visit(StateClosureEvent::StateVisibility { state: id })?;
500 }
501 for attachment in store.list_state_attachments(&id)? {
502 visit(StateClosureEvent::StateAttachment {
503 state: id,
504 attachment: &attachment,
505 })?;
506 match attachment.body {
507 StateAttachmentBody::Context(root) => walk_tree_closure_filtered(
508 store,
509 root,
510 &excluded_hashes,
511 &mut seen_hashes,
512 &mut visit,
513 )?,
514 StateAttachmentBody::RiskSignals(hash)
515 | StateAttachmentBody::ReviewSignatures(hash)
516 | StateAttachmentBody::Discussions(hash)
517 | StateAttachmentBody::StructuredConflicts(hash) => {
518 walk_blob_filtered(store, hash, &excluded_hashes, &mut seen_hashes, &mut visit)?
519 }
520 StateAttachmentBody::SemanticIndex(root) => walk_semantic_index_closure(
521 store,
522 root,
523 &excluded_hashes,
524 &mut seen_hashes,
525 &mut visit,
526 )?,
527 StateAttachmentBody::Signature(_) => {}
528 }
529 }
530
531 if max_depth.map(|max| depth < max).unwrap_or(true) {
532 for parent in &state.parents {
533 queue.push_back((*parent, depth + 1));
534 }
535 }
536
537 walk_tree_closure_filtered(
538 store,
539 state.tree,
540 &excluded_hashes,
541 &mut seen_hashes,
542 &mut visit,
543 )?;
544 if let Some(provenance_root) = state.provenance {
545 walk_tree_closure_filtered(
546 store,
547 provenance_root,
548 &excluded_hashes,
549 &mut seen_hashes,
550 &mut visit,
551 )?;
552 }
553 }
554
555 Ok(())
556}
557
558fn walk_tree_closure_filtered(
559 store: &impl ObjectStore,
560 tree_hash: ContentHash,
561 excluded: &HashSet<ContentHash>,
562 seen: &mut HashSet<ContentHash>,
563 visit: &mut impl for<'event> FnMut(StateClosureEvent<'event>) -> Result<()>,
564) -> Result<()> {
565 if excluded.contains(&tree_hash) {
566 visit(StateClosureEvent::ExcludedHash { hash: tree_hash })?;
567 return Ok(());
568 }
569 if !seen.insert(tree_hash) {
570 return Ok(());
571 }
572
573 let tree = store
574 .get_tree(&tree_hash)?
575 .ok_or_else(|| HeddleError::MissingObject {
576 object_type: "tree".to_string(),
577 id: tree_hash.to_hex(),
578 })?;
579
580 visit(StateClosureEvent::Tree {
581 hash: tree_hash,
582 tree: &tree,
583 })?;
584
585 for entry in tree.entries() {
586 match entry.target() {
587 TreeEntryTarget::Blob { hash, .. } | TreeEntryTarget::Symlink { hash } => {
588 walk_blob_filtered(store, *hash, excluded, seen, visit)?;
589 }
590 TreeEntryTarget::Tree { hash } => {
591 walk_tree_closure_filtered(store, *hash, excluded, seen, visit)?;
592 }
593 TreeEntryTarget::Gitlink { .. } => {}
594 TreeEntryTarget::Spoollink { .. } => {}
597 }
598 }
599
600 Ok(())
601}
602
603fn walk_blob_filtered(
604 store: &impl ObjectStore,
605 blob_hash: ContentHash,
606 excluded: &HashSet<ContentHash>,
607 seen: &mut HashSet<ContentHash>,
608 visit: &mut impl for<'event> FnMut(StateClosureEvent<'event>) -> Result<()>,
609) -> Result<()> {
610 if excluded.contains(&blob_hash) {
611 visit(StateClosureEvent::ExcludedHash { hash: blob_hash })?;
612 return Ok(());
613 }
614 if !seen.insert(blob_hash) {
615 return Ok(());
616 }
617 visit(StateClosureEvent::Blob { hash: blob_hash })?;
618 if store.has_redactions_for_blob(&blob_hash)? {
619 visit(StateClosureEvent::Redaction { blob: blob_hash })?;
620 }
621 Ok(())
622}
623
624fn walk_semantic_index_closure(
637 store: &impl ObjectStore,
638 root_hash: ContentHash,
639 excluded: &HashSet<ContentHash>,
640 seen: &mut HashSet<ContentHash>,
641 visit: &mut impl for<'event> FnMut(StateClosureEvent<'event>) -> Result<()>,
642) -> Result<()> {
643 let mut stack: Vec<ContentHash> = vec![root_hash];
646 while let Some(node_hash) = stack.pop() {
647 if !emit_semantic_blob(store, node_hash, excluded, seen, visit)? {
648 continue; }
650 let blob = store
651 .get_blob(&node_hash)?
652 .ok_or_else(|| missing_blob(node_hash))?;
653 let node = decode_semantic_container(&blob, node_hash)?;
657 for child in node {
658 match child {
659 SemanticChild::Interior(hash) => stack.push(hash),
660 SemanticChild::Leaf(hash) => {
661 emit_semantic_blob(store, hash, excluded, seen, visit)?;
663 }
664 SemanticChild::BindingDelta(hash) => {
665 emit_binding_delta(store, hash, excluded, seen, visit)?;
671 }
672 SemanticChild::ImporterIndex(hash) => {
673 emit_importer_index(store, hash, excluded, seen, visit)?;
674 }
675 }
676 }
677 }
678 Ok(())
679}
680
681enum SemanticChild {
683 Interior(ContentHash),
684 Leaf(ContentHash),
685 BindingDelta(ContentHash),
686 ImporterIndex(ContentHash),
687}
688
689fn decode_semantic_container(
692 blob: &crate::object::Blob,
693 node_hash: ContentHash,
694) -> Result<Vec<SemanticChild>> {
695 if let Ok(root) = SemanticIndexRoot::decode(blob.content()) {
699 let mut children = vec![SemanticChild::Interior(root.tree)];
700 if let Some(binding_delta) = root.binding_delta {
701 children.push(SemanticChild::BindingDelta(binding_delta));
702 }
703 if let Some(importer_index) = root.importer_index {
704 children.push(SemanticChild::ImporterIndex(importer_index));
705 }
706 return Ok(children);
707 }
708 let node = SemanticTreeNode::decode(blob.content())
709 .map_err(|err| HeddleError::Serialization(format!("semantic node {node_hash}: {err}")))?;
710 Ok(node
711 .entries
712 .iter()
713 .filter_map(|entry| match entry.kind {
714 SemanticEntryKind::Dir => Some(SemanticChild::Interior(entry.node)),
715 SemanticEntryKind::File => Some(SemanticChild::Leaf(entry.node)),
716 SemanticEntryKind::Opaque => None,
718 })
719 .collect())
720}
721
722fn emit_binding_delta(
723 store: &impl ObjectStore,
724 hash: ContentHash,
725 excluded: &HashSet<ContentHash>,
726 seen: &mut HashSet<ContentHash>,
727 visit: &mut impl for<'event> FnMut(StateClosureEvent<'event>) -> Result<()>,
728) -> Result<()> {
729 if !emit_semantic_blob(store, hash, excluded, seen, visit)? {
730 return Ok(());
731 }
732 let blob = store.get_blob(&hash)?.ok_or_else(|| missing_blob(hash))?;
733 BindingDelta::decode(blob.content()).map_err(|err| {
734 HeddleError::Serialization(format!("semantic binding delta {hash}: {err}"))
735 })?;
736 Ok(())
737}
738
739fn emit_importer_index(
740 store: &impl ObjectStore,
741 hash: ContentHash,
742 excluded: &HashSet<ContentHash>,
743 seen: &mut HashSet<ContentHash>,
744 visit: &mut impl for<'event> FnMut(StateClosureEvent<'event>) -> Result<()>,
745) -> Result<()> {
746 if !emit_semantic_blob(store, hash, excluded, seen, visit)? {
747 return Ok(());
748 }
749 let blob = store.get_blob(&hash)?.ok_or_else(|| missing_blob(hash))?;
750 ReverseDependencyIndex::decode(blob.content()).map_err(|err| {
751 HeddleError::Serialization(format!("semantic reverse-dependency index {hash}: {err}"))
752 })?;
753 Ok(())
754}
755
756fn emit_semantic_blob(
761 store: &impl ObjectStore,
762 hash: ContentHash,
763 excluded: &HashSet<ContentHash>,
764 seen: &mut HashSet<ContentHash>,
765 visit: &mut impl for<'event> FnMut(StateClosureEvent<'event>) -> Result<()>,
766) -> Result<bool> {
767 if excluded.contains(&hash) {
768 visit(StateClosureEvent::ExcludedHash { hash })?;
769 return Ok(false);
770 }
771 if !seen.insert(hash) {
772 return Ok(false);
773 }
774 if store.get_blob(&hash)?.is_none() {
775 return Err(missing_blob(hash));
776 }
777 visit(StateClosureEvent::Blob { hash })?;
778 Ok(true)
779}
780
781fn collect_semantic_hashes(
786 store: &impl ObjectStore,
787 root_hash: ContentHash,
788 excluded: &mut HashSet<ContentHash>,
789) -> Result<()> {
790 let mut stack: Vec<ContentHash> = vec![root_hash];
791 while let Some(node_hash) = stack.pop() {
792 if !excluded.insert(node_hash) {
793 continue;
794 }
795 let Some(blob) = store.get_blob(&node_hash)? else {
796 continue;
797 };
798 let children = match decode_semantic_container(&blob, node_hash) {
799 Ok(children) => children,
800 Err(_) => continue,
801 };
802 for child in children {
803 match child {
804 SemanticChild::Interior(hash) => stack.push(hash),
805 SemanticChild::Leaf(hash) => {
806 excluded.insert(hash);
807 }
808 SemanticChild::BindingDelta(hash) | SemanticChild::ImporterIndex(hash) => {
809 excluded.insert(hash);
810 }
811 }
812 }
813 }
814 Ok(())
815}
816
817fn object_info_from_event(
818 store: &impl ObjectStore,
819 event: StateClosureEvent<'_>,
820) -> Result<Option<ObjectInfo>> {
821 match event {
822 StateClosureEvent::State { id, state } => {
823 let state_bytes = rmp_serde::to_vec_named(state)?;
824 Ok(Some(ObjectInfo {
825 id: ObjectId::StateId(id),
826 obj_type: ObjectType::State,
827 size: state_bytes.len() as u64,
828 delta_base: None,
829 }))
830 }
831 StateClosureEvent::Tree { hash, tree } => {
832 let tree_bytes = tree.encode_canonical().map_err(HeddleError::from)?;
833 Ok(Some(ObjectInfo {
834 id: ObjectId::Hash(hash),
835 obj_type: ObjectType::Tree,
836 size: tree_bytes.len() as u64,
837 delta_base: None,
838 }))
839 }
840 StateClosureEvent::Blob { hash, .. } => {
841 let Some(blob) = store.get_blob(&hash)? else {
842 if blob_has_purge_evidence(store, &hash)? {
843 return Ok(None);
844 }
845 return Err(missing_blob(hash));
846 };
847 Ok(Some(ObjectInfo {
848 id: ObjectId::Hash(hash),
849 obj_type: ObjectType::Blob,
850 size: blob.size() as u64,
851 delta_base: None,
852 }))
853 }
854 StateClosureEvent::Redaction { blob } => Ok(store
855 .get_redactions_bytes_for_blob(&blob)?
856 .map(|bytes| ObjectInfo {
857 id: ObjectId::Hash(blob),
858 obj_type: ObjectType::Redaction,
859 size: bytes.len() as u64,
860 delta_base: None,
861 })),
862 StateClosureEvent::StateVisibility { state } => Ok(store
863 .get_state_visibility_bytes_for_state(&state)?
864 .map(|bytes| ObjectInfo {
865 id: ObjectId::StateId(state),
866 obj_type: ObjectType::StateVisibility,
867 size: bytes.len() as u64,
868 delta_base: None,
869 })),
870 StateClosureEvent::StateAttachment { state, attachment } => {
871 let bytes = rmp_serde::to_vec_named(attachment)?;
872 Ok(Some(ObjectInfo {
873 id: ObjectId::StateAttachment {
874 state,
875 id: attachment.id(),
876 kind: attachment.body.kind(),
877 },
878 obj_type: ObjectType::StateAttachment,
879 size: bytes.len() as u64,
880 delta_base: None,
881 }))
882 }
883 StateClosureEvent::ExcludedState { id } => {
884 let _ = id;
885 Ok(None)
886 }
887 StateClosureEvent::ExcludedHash { hash } => {
888 let _ = hash;
889 Ok(None)
890 }
891 }
892}
893
894fn planned_object_from_event(
895 store: &impl ObjectStore,
896 event: StateClosureEvent<'_>,
897) -> Result<Option<PlannedObject>> {
898 match event {
899 StateClosureEvent::State { id, .. } => Ok(Some(PlannedObject {
900 id: ObjectId::StateId(id),
901 obj_type: ObjectType::State,
902 })),
903 StateClosureEvent::Tree { hash, .. } => Ok(Some(PlannedObject {
904 id: ObjectId::Hash(hash),
905 obj_type: ObjectType::Tree,
906 })),
907 StateClosureEvent::Blob { hash, .. } => {
908 if store.get_blob(&hash)?.is_none() {
909 if blob_has_purge_evidence(store, &hash)? {
910 return Ok(None);
911 }
912 return Err(missing_blob(hash));
913 }
914 Ok(Some(PlannedObject {
915 id: ObjectId::Hash(hash),
916 obj_type: ObjectType::Blob,
917 }))
918 }
919 StateClosureEvent::Redaction { blob } => Ok(Some(PlannedObject {
920 id: ObjectId::Hash(blob),
921 obj_type: ObjectType::Redaction,
922 })),
923 StateClosureEvent::StateVisibility { state } => Ok(Some(PlannedObject {
924 id: ObjectId::StateId(state),
925 obj_type: ObjectType::StateVisibility,
926 })),
927 StateClosureEvent::StateAttachment { state, attachment } => Ok(Some(PlannedObject {
928 id: ObjectId::StateAttachment {
929 state,
930 id: attachment.id(),
931 kind: attachment.body.kind(),
932 },
933 obj_type: ObjectType::StateAttachment,
934 })),
935 StateClosureEvent::ExcludedState { id } => {
936 let _ = id;
937 Ok(None)
938 }
939 StateClosureEvent::ExcludedHash { hash } => {
940 let _ = hash;
941 Ok(None)
942 }
943 }
944}
945
946fn missing_blob(hash: ContentHash) -> HeddleError {
950 HeddleError::MissingObject {
951 object_type: "blob".to_string(),
952 id: hash.to_hex(),
953 }
954}
955
956fn blob_has_purge_evidence(
957 store: &impl ObjectStore,
958 hash: &ContentHash,
959) -> Result<bool> {
960 let Some(bytes) = store.get_redactions_bytes_for_blob(hash)? else {
961 return Ok(false);
962 };
963 let redactions = RedactionsBlob::decode(&bytes).map_err(|error| {
964 HeddleError::InvalidObject(format!(
965 "invalid redaction sidecar for missing blob {}: {error}",
966 hash.to_hex()
967 ))
968 })?;
969 Ok(redactions
970 .redactions
971 .iter()
972 .any(|redaction| redaction.redacted_blob == *hash && redaction.is_purged()))
973}
974
975pub fn missing_blobs_in_tree(
976 store: &impl ObjectStore,
977 tree_hash: ContentHash,
978) -> Result<Vec<ContentHash>> {
979 let mut missing = Vec::new();
980 collect_missing_blobs_recursive(store, &tree_hash, &mut missing)?;
981 Ok(missing)
982}
983
984fn collect_missing_blobs_recursive(
985 store: &impl ObjectStore,
986 tree_hash: &ContentHash,
987 missing: &mut Vec<ContentHash>,
988) -> Result<()> {
989 let Some(tree) = store.get_tree(tree_hash).map_err(|err| {
990 HeddleError::InvalidObject(format!(
991 "load tree {} while collecting lazy hydration missing blobs: {err}",
992 tree_hash.to_hex()
993 ))
994 })?
995 else {
996 return Ok(());
997 };
998
999 for entry in tree.entries() {
1000 match entry.target() {
1001 TreeEntryTarget::Blob { hash, .. } | TreeEntryTarget::Symlink { hash } => {
1002 if !store.has_blob(hash).map_err(|err| {
1003 HeddleError::InvalidObject(format!(
1004 "check blob {} while collecting lazy hydration missing blobs: {err}",
1005 hash.to_hex()
1006 ))
1007 })? {
1008 missing.push(*hash);
1009 }
1010 }
1011 TreeEntryTarget::Tree { hash } => {
1012 collect_missing_blobs_recursive(store, hash, missing)?;
1013 }
1014 TreeEntryTarget::Gitlink { .. } => {}
1015 TreeEntryTarget::Spoollink { .. } => {}
1018 }
1019 }
1020 Ok(())
1021}
1022
1023fn collect_excluded(
1024 store: &impl ObjectStore,
1025 roots: &[StateId],
1026) -> Result<(HashSet<StateId>, HashSet<ContentHash>)> {
1027 if roots.is_empty() {
1028 return Ok((HashSet::new(), HashSet::new()));
1029 }
1030
1031 let mut excluded_states: HashSet<StateId> = HashSet::new();
1032 let mut excluded_hashes: HashSet<ContentHash> = HashSet::new();
1033 let mut queue: VecDeque<StateId> = VecDeque::new();
1034
1035 for id in roots {
1036 queue.push_back(*id);
1037 }
1038
1039 while let Some(id) = queue.pop_front() {
1040 if !excluded_states.insert(id) {
1041 continue;
1042 }
1043
1044 let state = match store.get_state(&id)? {
1045 Some(state) => state,
1046 None => continue,
1047 };
1048
1049 for parent in &state.parents {
1050 queue.push_back(*parent);
1051 }
1052
1053 collect_tree_hashes(store, state.tree, &mut excluded_hashes)?;
1054 if let Some(provenance_root) = state.provenance {
1055 collect_tree_hashes(store, provenance_root, &mut excluded_hashes)?;
1056 }
1057 for attachment in store.list_state_attachments(&id)? {
1058 match attachment.body {
1059 StateAttachmentBody::Context(root) => {
1060 collect_tree_hashes(store, root, &mut excluded_hashes)?
1061 }
1062 StateAttachmentBody::RiskSignals(hash)
1063 | StateAttachmentBody::ReviewSignatures(hash)
1064 | StateAttachmentBody::Discussions(hash)
1065 | StateAttachmentBody::StructuredConflicts(hash) => {
1066 excluded_hashes.insert(hash);
1067 }
1068 StateAttachmentBody::SemanticIndex(root) => {
1069 collect_semantic_hashes(store, root, &mut excluded_hashes)?;
1070 }
1071 StateAttachmentBody::Signature(_) => {}
1072 }
1073 }
1074 }
1075
1076 Ok((excluded_states, excluded_hashes))
1077}
1078
1079fn collect_tree_hashes(
1080 store: &impl ObjectStore,
1081 tree_hash: ContentHash,
1082 excluded: &mut HashSet<ContentHash>,
1083) -> Result<()> {
1084 if !excluded.insert(tree_hash) {
1085 return Ok(());
1086 }
1087
1088 let tree = match store.get_tree(&tree_hash)? {
1089 Some(tree) => tree,
1090 None => return Ok(()),
1091 };
1092
1093 for entry in tree.entries() {
1094 match entry.target() {
1095 TreeEntryTarget::Blob { hash, .. } | TreeEntryTarget::Symlink { hash } => {
1096 excluded.insert(*hash);
1097 }
1098 TreeEntryTarget::Tree { hash } => {
1099 collect_tree_hashes(store, *hash, excluded)?;
1100 }
1101 TreeEntryTarget::Gitlink { .. } => {}
1102 TreeEntryTarget::Spoollink { .. } => {}
1105 }
1106 }
1107
1108 Ok(())
1109}
1110
1111pub fn is_ancestor(
1112 store: &impl ObjectStore,
1113 ancestor: StateId,
1114 descendant: StateId,
1115) -> Result<bool> {
1116 if ancestor == descendant {
1117 return Ok(true);
1118 }
1119
1120 let mut seen: HashSet<StateId> = HashSet::new();
1121 let mut queue: VecDeque<StateId> = VecDeque::new();
1122 queue.push_back(descendant);
1123
1124 while let Some(id) = queue.pop_front() {
1125 if !seen.insert(id) {
1126 continue;
1127 }
1128 let state = match store.get_state(&id)? {
1129 Some(s) => s,
1130 None => return Ok(false),
1131 };
1132 for parent in state.parents {
1133 if parent == ancestor {
1134 return Ok(true);
1135 }
1136 queue.push_back(parent);
1137 }
1138 }
1139
1140 Ok(false)
1141}