1use std::path::Path;
8
9use prikk_error::{PrikkError, Result};
10use prikk_object::ObjectType;
11
12use crate::checkout::prepare_snapshot_checkout_plan;
13use crate::fsutil::{
14 ensure_directory_required, read_file_if_exists, set_regular_file_mode_required,
15 stat_file_state_if_exists, sync_directory_required, write_worktree_file_atomically,
16};
17use crate::layout::RepositoryLayout;
18use crate::object_store::{FileObjectStore, ObjectReader};
19use crate::patch_replay::{ReplayManifest, ReplayManifestEntry};
20use crate::path::join_repo_path_to_root;
21use crate::snapshot::{SnapshotEntry, SnapshotManifest};
22use crate::worktree_marker::{clear_worktree_dirty, mark_worktree_dirty};
23
24#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct SnapshotMaterializationReport {
27 pub ref_name: String,
29 pub planned_files: usize,
31 pub written_files: usize,
33 pub unchanged_files: usize,
35 pub total_content_bytes: u64,
37 pub paths: Vec<String>,
39}
40
41pub fn materialize_snapshot_checkout(
51 layout: &RepositoryLayout,
52 ref_name: &str,
53) -> Result<SnapshotMaterializationReport> {
54 layout.require_current_format()?;
55 let plan = prepare_snapshot_checkout_plan(layout, ref_name)?;
56 let manifest = load_snapshot_manifest(layout, plan.snapshot_blob_id)?;
57 mark_worktree_dirty(layout)?;
61 let write_report = materialize_manifest_entries(layout, &manifest)?;
62 clear_worktree_dirty(layout)?;
63 Ok(SnapshotMaterializationReport {
64 ref_name: ref_name.to_string(),
65 planned_files: manifest.files.len(),
66 written_files: write_report.written_files,
67 unchanged_files: write_report.unchanged_files,
68 total_content_bytes: manifest.total_content_bytes(),
69 paths: manifest
70 .files
71 .iter()
72 .map(|entry| entry.path.as_str().to_string())
73 .collect(),
74 })
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub(crate) struct ManifestMaterializationReport {
80 pub(crate) written_files: usize,
82 pub(crate) unchanged_files: usize,
84}
85
86pub(crate) fn materialize_manifest_entries(
88 layout: &RepositoryLayout,
89 manifest: &SnapshotManifest,
90) -> Result<ManifestMaterializationReport> {
91 let mut written_files = 0_usize;
92 let mut unchanged_files = 0_usize;
93 for entry in &manifest.files {
94 match materialize_entry(layout, entry)? {
95 EntryWriteOutcome::Written => written_files += 1,
96 EntryWriteOutcome::Unchanged => unchanged_files += 1,
97 }
98 }
99 Ok(ManifestMaterializationReport {
100 written_files,
101 unchanged_files,
102 })
103}
104
105pub(crate) fn materialize_replay_manifest_entries(
110 layout: &RepositoryLayout,
111 manifest: &ReplayManifest,
112) -> Result<ManifestMaterializationReport> {
113 let mut written_files = 0_usize;
114 let mut unchanged_files = 0_usize;
115 for entry in &manifest.files {
116 match materialize_replay_entry(layout, entry)? {
117 EntryWriteOutcome::Written => written_files += 1,
118 EntryWriteOutcome::Unchanged => unchanged_files += 1,
119 }
120 }
121 Ok(ManifestMaterializationReport {
122 written_files,
123 unchanged_files,
124 })
125}
126
127fn materialize_replay_entry(
128 layout: &RepositoryLayout,
129 entry: &ReplayManifestEntry,
130) -> Result<EntryWriteOutcome> {
131 let root = layout.root();
132 let target = join_repo_path_to_root(&entry.path, root);
133 ensure_target_is_inside_root(root, &target)?;
134 ensure_parent_directory(layout, entry.path.as_str())?;
135 let relative = Path::new(entry.path.as_str());
136 if let Some(current) = read_file_if_exists(layout.worktree_mutation_root(), relative)? {
137 if current != entry.bytes {
138 return Err(PrikkError::Integrity(format!(
139 "refusing to overwrite existing file with different content: {}",
140 target.display()
141 )));
142 }
143 let current_mode = stat_file_state_if_exists(layout.worktree_mutation_root(), relative)?
148 .and_then(|stat| stat.mode)
149 .map(|mode| mode & 0o7777);
150 if current_mode == Some(entry.mode & 0o7777) {
151 sync_directory_required(layout.worktree_mutation_root(), relative)?;
152 return Ok(EntryWriteOutcome::Unchanged);
153 }
154 set_regular_file_mode_required(layout.worktree_mutation_root(), relative, entry.mode)?;
155 return Ok(EntryWriteOutcome::Written);
156 }
157 write_worktree_file_atomically(layout.worktree_mutation_root(), relative, &entry.bytes)?;
158 set_regular_file_mode_required(layout.worktree_mutation_root(), relative, entry.mode)?;
159 Ok(EntryWriteOutcome::Written)
160}
161
162fn load_snapshot_manifest(
163 layout: &RepositoryLayout,
164 snapshot_blob_id: prikk_object::ObjectId,
165) -> Result<SnapshotManifest> {
166 let object_store = FileObjectStore::new(layout.clone());
167 let Some(envelope) = object_store.read_object(snapshot_blob_id)? else {
168 return Err(PrikkError::Integrity(format!(
169 "snapshot Blob {snapshot_blob_id} is missing"
170 )));
171 };
172 if envelope.object_type != ObjectType::Blob {
173 return Err(PrikkError::ObjectTypeMismatch {
174 expected: ObjectType::Blob.to_string(),
175 actual: envelope.object_type.to_string(),
176 });
177 }
178 let snapshot_content = crate::blob_access::decode_snapshot_blob(&envelope.canonical_payload)?;
179 SnapshotManifest::decode(&snapshot_content)
180}
181
182#[derive(Debug, Clone, Copy, PartialEq, Eq)]
183enum EntryWriteOutcome {
184 Written,
185 Unchanged,
186}
187
188fn materialize_entry(
189 layout: &RepositoryLayout,
190 entry: &SnapshotEntry,
191) -> Result<EntryWriteOutcome> {
192 let root = layout.root();
193 let target = join_repo_path_to_root(&entry.path, root);
194 ensure_target_is_inside_root(root, &target)?;
195 ensure_parent_directory(layout, entry.path.as_str())?;
196 let relative = Path::new(entry.path.as_str());
197 if let Some(current) = read_file_if_exists(layout.worktree_mutation_root(), relative)? {
198 if current == entry.bytes {
199 sync_directory_required(layout.worktree_mutation_root(), relative)?;
200 return Ok(EntryWriteOutcome::Unchanged);
201 }
202 return Err(PrikkError::Integrity(format!(
203 "refusing to overwrite existing file with different content: {}",
204 target.display()
205 )));
206 }
207 write_worktree_file_atomically(layout.worktree_mutation_root(), relative, &entry.bytes)?;
208 Ok(EntryWriteOutcome::Written)
209}
210
211fn ensure_parent_directory(layout: &RepositoryLayout, repo_path: &str) -> Result<()> {
212 let relative = Path::new(repo_path);
213 let parent = relative.parent().unwrap_or_else(|| Path::new(""));
214 ensure_directory_required(layout.worktree_mutation_root(), parent)
215}
216
217fn ensure_target_is_inside_root(root: &Path, target: &Path) -> Result<()> {
218 if !target.starts_with(root) {
219 return Err(PrikkError::Integrity(format!(
220 "materialization target escaped repository root: {}",
221 target.display()
222 )));
223 }
224 Ok(())
225}