Skip to main content

prikk_store/
checkout.rs

1//! Checkout planning helpers.
2//!
3//! PR-017 keeps a read-only checkout planning. It validates the current ref-state target and the
4//! referenced block/patch objects, then reports what a future materializer would need to do. It
5//! deliberately does not write the worktree and does not apply patch algebra.
6
7use 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
15/// Default ref used by checkout planning.
16pub const DEFAULT_CHECKOUT_REF: &str = "heads/main";
17
18/// Read-only plan for a future checkout/materialization operation.
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct CheckoutPlan {
21    /// Human-readable ref name.
22    pub ref_name: String,
23    /// Current RefState ID, if the ref is published.
24    pub ref_state_id: Option<ObjectId>,
25    /// Target block ID, if the ref is published.
26    pub block_id: Option<ObjectId>,
27    /// Target block kind, if the target block exists and decodes.
28    pub block_kind: Option<BlockKind>,
29    /// Number of parent blocks referenced by the target block.
30    pub parent_count: usize,
31    /// Number of patches referenced by the target block.
32    pub patch_count: usize,
33    /// Optional snapshot blob reference from the target block.
34    pub snapshot_blob_ref: Option<ObjectId>,
35    /// Materialization status for this implementation stage.
36    pub materialization: CheckoutMaterialization,
37}
38
39impl CheckoutPlan {
40    /// Return true when the plan has a published target block.
41    #[must_use]
42    pub const fn has_target_block(&self) -> bool {
43        self.block_id.is_some()
44    }
45}
46
47/// Read-only plan for validating a snapshot-backed checkout.
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct SnapshotCheckoutPlan {
50    /// Base checkout plan.
51    pub checkout: CheckoutPlan,
52    /// Snapshot Blob object ID.
53    pub snapshot_blob_id: ObjectId,
54    /// Number of files in the snapshot manifest.
55    pub file_count: usize,
56    /// Total content bytes in the snapshot manifest.
57    pub total_content_bytes: u64,
58    /// Validated repository-relative paths in materialization order.
59    pub paths: Vec<String>,
60}
61
62/// What blocks a checkout from becoming a real worktree materialization in this stage.
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum CheckoutMaterialization {
65    /// The requested ref is not published yet.
66    UnpublishedRef,
67    /// The target block has no patches and no snapshot to materialize.
68    NoWorktreeChanges,
69    /// A snapshot blob exists, but snapshot materialization is not implemented yet.
70    RequiresSnapshotMaterialization,
71    /// Patch application/algebra is required and is intentionally deferred.
72    RequiresPatchEngine,
73}
74
75impl CheckoutMaterialization {
76    /// Human-readable status label.
77    #[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
88/// Prepare and validate a snapshot-backed checkout plan without writing the worktree.
89pub 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
121/// Prepare a checkout plan for a ref without modifying the worktree.
122pub 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;