use std::path::Path;
use prikk_error::{PrikkError, Result};
use prikk_object::ObjectType;
use crate::checkout::prepare_snapshot_checkout_plan;
use crate::fsutil::{
ensure_directory_required, read_file_if_exists, set_regular_file_mode_required,
stat_file_state_if_exists, sync_directory_required, write_worktree_file_atomically,
};
use crate::layout::RepositoryLayout;
use crate::object_store::{FileObjectStore, ObjectReader};
use crate::patch_replay::{ReplayManifest, ReplayManifestEntry};
use crate::path::join_repo_path_to_root;
use crate::snapshot::{SnapshotEntry, SnapshotManifest};
use crate::worktree_marker::{clear_worktree_dirty, mark_worktree_dirty};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SnapshotMaterializationReport {
pub ref_name: String,
pub planned_files: usize,
pub written_files: usize,
pub unchanged_files: usize,
pub total_content_bytes: u64,
pub paths: Vec<String>,
}
pub fn materialize_snapshot_checkout(
layout: &RepositoryLayout,
ref_name: &str,
) -> Result<SnapshotMaterializationReport> {
layout.require_current_format()?;
let plan = prepare_snapshot_checkout_plan(layout, ref_name)?;
let manifest = load_snapshot_manifest(layout, plan.snapshot_blob_id)?;
mark_worktree_dirty(layout)?;
let write_report = materialize_manifest_entries(layout, &manifest)?;
clear_worktree_dirty(layout)?;
Ok(SnapshotMaterializationReport {
ref_name: ref_name.to_string(),
planned_files: manifest.files.len(),
written_files: write_report.written_files,
unchanged_files: write_report.unchanged_files,
total_content_bytes: manifest.total_content_bytes(),
paths: manifest
.files
.iter()
.map(|entry| entry.path.as_str().to_string())
.collect(),
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct ManifestMaterializationReport {
pub(crate) written_files: usize,
pub(crate) unchanged_files: usize,
}
pub(crate) fn materialize_manifest_entries(
layout: &RepositoryLayout,
manifest: &SnapshotManifest,
) -> Result<ManifestMaterializationReport> {
let mut written_files = 0_usize;
let mut unchanged_files = 0_usize;
for entry in &manifest.files {
match materialize_entry(layout, entry)? {
EntryWriteOutcome::Written => written_files += 1,
EntryWriteOutcome::Unchanged => unchanged_files += 1,
}
}
Ok(ManifestMaterializationReport {
written_files,
unchanged_files,
})
}
pub(crate) fn materialize_replay_manifest_entries(
layout: &RepositoryLayout,
manifest: &ReplayManifest,
) -> Result<ManifestMaterializationReport> {
let mut written_files = 0_usize;
let mut unchanged_files = 0_usize;
for entry in &manifest.files {
match materialize_replay_entry(layout, entry)? {
EntryWriteOutcome::Written => written_files += 1,
EntryWriteOutcome::Unchanged => unchanged_files += 1,
}
}
Ok(ManifestMaterializationReport {
written_files,
unchanged_files,
})
}
fn materialize_replay_entry(
layout: &RepositoryLayout,
entry: &ReplayManifestEntry,
) -> Result<EntryWriteOutcome> {
let root = layout.root();
let target = join_repo_path_to_root(&entry.path, root);
ensure_target_is_inside_root(root, &target)?;
ensure_parent_directory(layout, entry.path.as_str())?;
let relative = Path::new(entry.path.as_str());
if let Some(current) = read_file_if_exists(layout.worktree_mutation_root(), relative)? {
if current != entry.bytes {
return Err(PrikkError::Integrity(format!(
"refusing to overwrite existing file with different content: {}",
target.display()
)));
}
let current_mode = stat_file_state_if_exists(layout.worktree_mutation_root(), relative)?
.and_then(|stat| stat.mode)
.map(|mode| mode & 0o7777);
if current_mode == Some(entry.mode & 0o7777) {
sync_directory_required(layout.worktree_mutation_root(), relative)?;
return Ok(EntryWriteOutcome::Unchanged);
}
set_regular_file_mode_required(layout.worktree_mutation_root(), relative, entry.mode)?;
return Ok(EntryWriteOutcome::Written);
}
write_worktree_file_atomically(layout.worktree_mutation_root(), relative, &entry.bytes)?;
set_regular_file_mode_required(layout.worktree_mutation_root(), relative, entry.mode)?;
Ok(EntryWriteOutcome::Written)
}
fn load_snapshot_manifest(
layout: &RepositoryLayout,
snapshot_blob_id: prikk_object::ObjectId,
) -> Result<SnapshotManifest> {
let object_store = FileObjectStore::new(layout.clone());
let Some(envelope) = object_store.read_object(snapshot_blob_id)? else {
return Err(PrikkError::Integrity(format!(
"snapshot Blob {snapshot_blob_id} is missing"
)));
};
if envelope.object_type != ObjectType::Blob {
return Err(PrikkError::ObjectTypeMismatch {
expected: ObjectType::Blob.to_string(),
actual: envelope.object_type.to_string(),
});
}
let snapshot_content = crate::blob_access::decode_snapshot_blob(&envelope.canonical_payload)?;
SnapshotManifest::decode(&snapshot_content)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum EntryWriteOutcome {
Written,
Unchanged,
}
fn materialize_entry(
layout: &RepositoryLayout,
entry: &SnapshotEntry,
) -> Result<EntryWriteOutcome> {
let root = layout.root();
let target = join_repo_path_to_root(&entry.path, root);
ensure_target_is_inside_root(root, &target)?;
ensure_parent_directory(layout, entry.path.as_str())?;
let relative = Path::new(entry.path.as_str());
if let Some(current) = read_file_if_exists(layout.worktree_mutation_root(), relative)? {
if current == entry.bytes {
sync_directory_required(layout.worktree_mutation_root(), relative)?;
return Ok(EntryWriteOutcome::Unchanged);
}
return Err(PrikkError::Integrity(format!(
"refusing to overwrite existing file with different content: {}",
target.display()
)));
}
write_worktree_file_atomically(layout.worktree_mutation_root(), relative, &entry.bytes)?;
Ok(EntryWriteOutcome::Written)
}
fn ensure_parent_directory(layout: &RepositoryLayout, repo_path: &str) -> Result<()> {
let relative = Path::new(repo_path);
let parent = relative.parent().unwrap_or_else(|| Path::new(""));
ensure_directory_required(layout.worktree_mutation_root(), parent)
}
fn ensure_target_is_inside_root(root: &Path, target: &Path) -> Result<()> {
if !target.starts_with(root) {
return Err(PrikkError::Integrity(format!(
"materialization target escaped repository root: {}",
target.display()
)));
}
Ok(())
}