1use prikk_error::{PrikkError, Result};
8use prikk_object::{BlockKind, BlockPayload, ObjectId, ObjectType, RefStatePayload};
9
10use crate::layout::RepositoryLayout;
11use crate::object_store::{ObjectReadSnapshot, ObjectReader};
12use crate::refs::RefStore;
13use crate::snapshot::SnapshotManifest;
14
15pub const DEFAULT_CHECKOUT_REF: &str = "heads/main";
17
18#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct CheckoutPlan {
21 pub ref_name: String,
23 pub ref_state_id: Option<ObjectId>,
25 pub block_id: Option<ObjectId>,
27 pub block_kind: Option<BlockKind>,
29 pub parent_count: usize,
31 pub patch_count: usize,
33 pub snapshot_blob_ref: Option<ObjectId>,
35 pub materialization: CheckoutMaterialization,
37}
38
39impl CheckoutPlan {
40 #[must_use]
42 pub const fn has_target_block(&self) -> bool {
43 self.block_id.is_some()
44 }
45}
46
47#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct SnapshotCheckoutPlan {
50 pub checkout: CheckoutPlan,
52 pub snapshot_blob_id: ObjectId,
54 pub file_count: usize,
56 pub total_content_bytes: u64,
58 pub paths: Vec<String>,
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum CheckoutMaterialization {
65 UnpublishedRef,
67 NoWorktreeChanges,
69 RequiresSnapshotMaterialization,
71 RequiresPatchEngine,
73}
74
75impl CheckoutMaterialization {
76 #[must_use]
78 pub const fn as_str(self) -> &'static str {
79 match self {
80 Self::UnpublishedRef => "unpublished-ref",
81 Self::NoWorktreeChanges => "no-worktree-changes",
82 Self::RequiresSnapshotMaterialization => "requires-snapshot-materialization",
83 Self::RequiresPatchEngine => "requires-patch-engine",
84 }
85 }
86}
87
88pub fn prepare_snapshot_checkout_plan(
90 layout: &RepositoryLayout,
91 ref_name: &str,
92) -> Result<SnapshotCheckoutPlan> {
93 let checkout = prepare_checkout_plan(layout, ref_name)?;
94 let Some(snapshot_blob_id) = checkout.snapshot_blob_ref else {
95 return Err(PrikkError::Integrity(format!(
96 "checkout target for {ref_name} does not contain a snapshot blob"
97 )));
98 };
99 let object_store = ObjectReadSnapshot::open(layout)?;
100 let Some(envelope) = object_store.read_typed(snapshot_blob_id, ObjectType::Blob)? else {
101 return Err(PrikkError::Integrity(format!(
102 "snapshot Blob {snapshot_blob_id} is missing"
103 )));
104 };
105 let snapshot_content = crate::blob_access::decode_snapshot_blob(&envelope.canonical_payload)?;
106 let manifest = SnapshotManifest::decode(&snapshot_content)?;
107 let paths = manifest
108 .files
109 .iter()
110 .map(|entry| entry.path.as_str().to_string())
111 .collect();
112 Ok(SnapshotCheckoutPlan {
113 checkout,
114 snapshot_blob_id,
115 file_count: manifest.files.len(),
116 total_content_bytes: manifest.total_content_bytes(),
117 paths,
118 })
119}
120
121pub fn prepare_checkout_plan(layout: &RepositoryLayout, ref_name: &str) -> Result<CheckoutPlan> {
123 let ref_store = RefStore::new(layout.clone());
124 let object_store = ObjectReadSnapshot::open(layout)?;
125 let Some(ref_state_id) = ref_store.read_current_ref_state_id(ref_name)? else {
126 return Ok(CheckoutPlan {
127 ref_name: ref_name.to_string(),
128 ref_state_id: None,
129 block_id: None,
130 block_kind: None,
131 parent_count: 0,
132 patch_count: 0,
133 snapshot_blob_ref: None,
134 materialization: CheckoutMaterialization::UnpublishedRef,
135 });
136 };
137
138 let ref_state = load_ref_state(&object_store, ref_state_id, ref_name)?;
139 let block_id = ref_state.target_object_id;
140 let block = load_block(&object_store, block_id)?;
141 validate_block_references(&object_store, &block)?;
142 let materialization = materialization_status(&block);
143 Ok(CheckoutPlan {
144 ref_name: ref_name.to_string(),
145 ref_state_id: Some(ref_state_id),
146 block_id: Some(block_id),
147 block_kind: Some(block.kind),
148 parent_count: block.parent_block_ids.len(),
149 patch_count: block.patch_ids.len(),
150 snapshot_blob_ref: block.snapshot_blob_ref,
151 materialization,
152 })
153}
154
155fn load_ref_state(
156 object_store: &impl ObjectReader,
157 ref_state_id: ObjectId,
158 ref_name: &str,
159) -> Result<RefStatePayload> {
160 let Some(envelope) = object_store.read_typed(ref_state_id, ObjectType::RefState)? else {
161 return Err(PrikkError::Integrity(format!(
162 "checkout ref {ref_name} points to missing RefState {ref_state_id}"
163 )));
164 };
165 let payload =
166 RefStatePayload::decode_canonical(&envelope.canonical_payload, envelope.schema_version)?;
167 if payload.ref_name != ref_name {
168 return Err(PrikkError::Integrity(format!(
169 "checkout RefState name mismatch: expected {ref_name}, got {}",
170 payload.ref_name
171 )));
172 }
173 Ok(payload)
174}
175
176fn load_block(object_store: &impl ObjectReader, block_id: ObjectId) -> Result<BlockPayload> {
177 let Some(envelope) = object_store.read_typed(block_id, ObjectType::Block)? else {
178 return Err(PrikkError::Integrity(format!(
179 "checkout target Block {block_id} is missing"
180 )));
181 };
182 BlockPayload::decode_canonical(&envelope.canonical_payload)
183}
184
185fn validate_block_references(object_store: &impl ObjectReader, block: &BlockPayload) -> Result<()> {
186 for parent in &block.parent_block_ids {
187 if object_store
188 .read_typed(*parent, ObjectType::Block)?
189 .is_none()
190 {
191 return Err(PrikkError::Integrity(format!(
192 "checkout target references missing parent Block {parent}"
193 )));
194 }
195 }
196 for patch in &block.patch_ids {
197 if object_store
198 .read_typed(*patch, ObjectType::Patch)?
199 .is_none()
200 {
201 return Err(PrikkError::Integrity(format!(
202 "checkout target references missing Patch {patch}"
203 )));
204 }
205 }
206 if let Some(snapshot) = block.snapshot_blob_ref {
207 if object_store
208 .read_typed(snapshot, ObjectType::Blob)?
209 .is_none()
210 {
211 return Err(PrikkError::Integrity(format!(
212 "checkout target references missing snapshot Blob {snapshot}"
213 )));
214 }
215 }
216 Ok(())
217}
218
219fn materialization_status(block: &BlockPayload) -> CheckoutMaterialization {
220 if block.patch_ids.is_empty() && block.snapshot_blob_ref.is_none() {
221 return CheckoutMaterialization::NoWorktreeChanges;
222 }
223 if block.snapshot_blob_ref.is_some() {
224 return CheckoutMaterialization::RequiresSnapshotMaterialization;
225 }
226 CheckoutMaterialization::RequiresPatchEngine
227}
228
229#[cfg(test)]
230mod tests;