use std::collections::BTreeMap;
mod apply;
pub(crate) mod decode;
mod read;
use prikk_error::{PrikkError, Result};
use prikk_object::ObjectId;
use crate::layout::RepositoryLayout;
use crate::object_store::FileObjectStore;
use crate::path::RepoPath;
use crate::refs::RefStore;
use crate::snapshot::SnapshotManifest;
use crate::validate_local_branch_ref;
use apply::apply_decoded_operation;
use decode::decode_patch_operations;
use read::{
current_target_block, files_to_manifest, files_to_replay_manifest, load_snapshot_files,
read_block, read_patch, single_parent_chain,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PatchReplayPlan {
pub ref_name: String,
pub target_block_id: ObjectId,
pub block_count: usize,
pub patch_count: usize,
pub applied_operation_count: usize,
pub file_count: usize,
pub total_content_bytes: u64,
pub paths: Vec<String>,
}
pub fn prepare_patch_replay_plan(
layout: &RepositoryLayout,
ref_name: &str,
) -> Result<PatchReplayPlan> {
let snapshot = replay_supported_patch_chain(layout, ref_name)?;
let paths = snapshot
.manifest
.files
.iter()
.map(|entry| entry.path.as_str().to_string())
.collect();
Ok(PatchReplayPlan {
ref_name: snapshot.ref_name,
target_block_id: snapshot.target_block_id,
block_count: snapshot.block_count,
patch_count: snapshot.patch_count,
applied_operation_count: snapshot.applied_operation_count,
file_count: snapshot.manifest.files.len(),
total_content_bytes: snapshot.manifest.total_content_bytes(),
paths,
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ReplayManifestEntry {
pub(crate) path: RepoPath,
pub(crate) bytes: Vec<u8>,
pub(crate) mode: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ReplayManifest {
pub(crate) files: Vec<ReplayManifestEntry>,
}
impl ReplayManifest {
pub(crate) fn total_content_bytes(&self) -> u64 {
self.files
.iter()
.map(|entry| entry.bytes.len() as u64)
.sum()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct PatchReplaySnapshot {
pub(crate) ref_name: String,
pub(crate) target_block_id: ObjectId,
pub(crate) block_count: usize,
pub(crate) patch_count: usize,
pub(crate) applied_operation_count: usize,
pub(crate) manifest: ReplayManifest,
pub(crate) deleted_files: Vec<PatchReplayDeletedFile>,
pub(crate) baseline_manifest: SnapshotManifest,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct PatchReplayDeletedFile {
pub(crate) path: RepoPath,
pub(crate) old_blob_id: ObjectId,
pub(crate) old_bytes: Vec<u8>,
}
pub(crate) fn replay_supported_patch_chain(
layout: &RepositoryLayout,
ref_name: &str,
) -> Result<PatchReplaySnapshot> {
let object_store = FileObjectStore::new(layout.clone());
let target_block_id = current_target_block(layout, &object_store, ref_name)?;
let block_ids = single_parent_chain(&object_store, target_block_id)?;
let mut files = BTreeMap::new();
let mut live_nodes = BTreeMap::new();
let mut deleted_files = BTreeMap::new();
let mut patch_count = 0_usize;
let mut applied_operation_count = 0_usize;
let mut baseline_files = BTreeMap::new();
for block_id in &block_ids {
let block = read_block(&object_store, *block_id)?;
if let Some(snapshot_blob_ref) = block.snapshot_blob_ref {
files = load_snapshot_files(&object_store, snapshot_blob_ref)?;
live_nodes.clear();
baseline_files = files.clone();
deleted_files.clear();
}
for patch_id in block.patch_ids {
let patch = read_patch(&object_store, patch_id)?;
let operations = decode_patch_operations(&patch.canonical_payload)?;
for operation in operations {
apply_decoded_operation(
&object_store,
&mut files,
&mut live_nodes,
&mut deleted_files,
operation,
)?;
applied_operation_count += 1;
}
patch_count += 1;
}
}
Ok(PatchReplaySnapshot {
ref_name: ref_name.to_string(),
target_block_id,
block_count: block_ids.len(),
patch_count,
applied_operation_count,
manifest: files_to_replay_manifest(files, &live_nodes)?,
deleted_files: deleted_files.into_values().collect(),
baseline_manifest: files_to_manifest(baseline_files)?,
})
}
pub(crate) fn resolve_node_lineage_bounds(
layout: &RepositoryLayout,
ref_name: &str,
) -> Result<(ObjectId, ObjectId)> {
let object_store = FileObjectStore::new(layout.clone());
let baseline = current_target_block(layout, &object_store, ref_name)?;
let chain = single_parent_chain(&object_store, baseline)?;
let horizon = *chain
.first()
.ok_or_else(|| PrikkError::Integrity(format!("ref {ref_name} lineage is empty")))?;
Ok((baseline, horizon))
}
pub(crate) enum WorktreeBaseline {
Published {
baseline_block: ObjectId,
horizon: ObjectId,
},
Genesis,
}
pub(crate) fn resolve_worktree_baseline(
layout: &RepositoryLayout,
ref_name: &str,
) -> Result<WorktreeBaseline> {
let canonical_ref = validate_local_branch_ref(ref_name)?;
let ref_store = RefStore::new(layout.clone());
if ref_store
.read_current_ref_state_id(&canonical_ref)?
.is_some()
{
let (baseline_block, horizon) = resolve_node_lineage_bounds(layout, &canonical_ref)?;
return Ok(WorktreeBaseline::Published {
baseline_block,
horizon,
});
}
let log = ref_store.replay_log(&canonical_ref).map_err(|err| {
PrikkError::Integrity(format!(
"ref {canonical_ref} log is unreadable; run `prikk doctor` before committing ({err})"
))
})?;
if log.trailing_partial_bytes != 0 {
return Err(PrikkError::Integrity(format!(
"ref {canonical_ref} pointer is missing and its log has trailing partial bytes; \
run `prikk doctor` (this is not a genesis repository)"
)));
}
if !log.records.is_empty() {
return Err(PrikkError::Integrity(format!(
"ref {canonical_ref} pointer is missing but ref-log history exists; \
preserve the repository and restore from backup (this is not a genesis repository)"
)));
}
Ok(WorktreeBaseline::Genesis)
}
#[cfg(test)]
mod tests;