use std::path::Path;
use prikk_error::{PrikkError, Result};
use crate::fsutil::{EntryKind, inspect_entry, read_file_required, remove_worktree_file_required};
use crate::layout::RepositoryLayout;
use crate::patch_replay::{PatchReplayDeletedFile, replay_supported_patch_chain};
use crate::path::join_repo_path_to_root;
use crate::worktree::materialize_replay_manifest_entries;
use crate::worktree_marker::{clear_worktree_dirty, mark_worktree_dirty};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PatchMaterializationReport {
pub ref_name: String,
pub block_count: usize,
pub patch_count: usize,
pub applied_operation_count: usize,
pub planned_files: usize,
pub written_files: usize,
pub unchanged_files: usize,
pub deleted_files: usize,
pub already_absent_deleted_files: usize,
pub deletion_conflicts: usize,
pub total_content_bytes: u64,
pub paths: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PatchDeletionPlan {
pub ref_name: String,
pub planned_deletions: usize,
pub deletable_files: usize,
pub already_absent_files: usize,
pub conflicts: Vec<PatchDeletionConflict>,
pub deletable_paths: Vec<String>,
}
impl PatchDeletionPlan {
#[must_use]
pub fn is_safe_to_apply(&self) -> bool {
self.conflicts.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PatchDeletionConflict {
pub path: String,
pub reason: String,
}
pub fn materialize_patch_checkout(
layout: &RepositoryLayout,
ref_name: &str,
) -> Result<PatchMaterializationReport> {
layout.require_current_format()?;
materialize_patch_checkout_inner(layout, ref_name, false)
}
pub fn materialize_patch_checkout_with_deletions(
layout: &RepositoryLayout,
ref_name: &str,
) -> Result<PatchMaterializationReport> {
layout.require_current_format()?;
materialize_patch_checkout_inner(layout, ref_name, true)
}
pub fn plan_patch_checkout_deletions(
layout: &RepositoryLayout,
ref_name: &str,
) -> Result<PatchDeletionPlan> {
let snapshot = replay_supported_patch_chain(layout, ref_name)?;
let analysis = analyze_deletions(layout, &snapshot.deleted_files)?;
Ok(PatchDeletionPlan {
ref_name: snapshot.ref_name,
planned_deletions: snapshot.deleted_files.len(),
deletable_files: analysis.deletable.len(),
already_absent_files: analysis.already_absent,
conflicts: analysis.conflicts,
deletable_paths: analysis
.deletable
.iter()
.map(|entry| entry.path.path.as_str().to_string())
.collect(),
})
}
fn materialize_patch_checkout_inner(
layout: &RepositoryLayout,
ref_name: &str,
delete_removed: bool,
) -> Result<PatchMaterializationReport> {
let snapshot = replay_supported_patch_chain(layout, ref_name)?;
let deletion_analysis = analyze_deletions(layout, &snapshot.deleted_files)?;
if delete_removed && !deletion_analysis.conflicts.is_empty() {
return Err(PrikkError::Integrity(format!(
"refusing checkout deletion because {} candidate(s) are unsafe",
deletion_analysis.conflicts.len()
)));
}
mark_worktree_dirty(layout)?;
let write_report = materialize_replay_manifest_entries(layout, &snapshot.manifest)?;
let deleted_files = if delete_removed {
apply_deletions(
layout,
&deletion_analysis.deletable,
&deletion_analysis.already_absent_paths,
)?
} else {
0
};
clear_worktree_dirty(layout)?;
let paths = snapshot
.manifest
.files
.iter()
.map(|entry| entry.path.as_str().to_string())
.collect();
Ok(PatchMaterializationReport {
ref_name: snapshot.ref_name,
block_count: snapshot.block_count,
patch_count: snapshot.patch_count,
applied_operation_count: snapshot.applied_operation_count,
planned_files: snapshot.manifest.files.len(),
written_files: write_report.written_files,
unchanged_files: write_report.unchanged_files,
deleted_files,
already_absent_deleted_files: deletion_analysis.already_absent,
deletion_conflicts: deletion_analysis.conflicts.len(),
total_content_bytes: snapshot.manifest.total_content_bytes(),
paths,
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct DeletionAnalysis {
deletable: Vec<DeletableFile>,
already_absent: usize,
already_absent_paths: Vec<crate::path::RepoPath>,
conflicts: Vec<PatchDeletionConflict>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct DeletableFile {
path: PatchReplayDeletedFile,
target: std::path::PathBuf,
}
fn analyze_deletions(
layout: &RepositoryLayout,
deleted: &[PatchReplayDeletedFile],
) -> Result<DeletionAnalysis> {
let mut deletable = Vec::new();
let mut already_absent = 0_usize;
let mut already_absent_paths = Vec::new();
let mut conflicts = Vec::new();
for deleted_file in deleted {
let target = join_repo_path_to_root(&deleted_file.path, layout.root());
let relative = Path::new(deleted_file.path.as_str());
match inspect_entry(layout.worktree_mutation_root(), relative)? {
None => {
already_absent += 1;
already_absent_paths.push(deleted_file.path.clone());
continue;
}
Some(EntryKind::Regular) => {}
Some(EntryKind::Symlink) => {
conflicts.push(PatchDeletionConflict {
path: deleted_file.path.as_str().to_string(),
reason: "target is a symlink".to_string(),
});
continue;
}
Some(EntryKind::Directory | EntryKind::Other) => {
conflicts.push(PatchDeletionConflict {
path: deleted_file.path.as_str().to_string(),
reason: "target is not a regular file".to_string(),
});
continue;
}
}
let current = read_file_required(layout.worktree_mutation_root(), relative)?;
if current != deleted_file.old_bytes {
conflicts.push(PatchDeletionConflict {
path: deleted_file.path.as_str().to_string(),
reason: format!(
"current file bytes do not match delete precondition blob {}",
deleted_file.old_blob_id
),
});
continue;
}
deletable.push(DeletableFile {
path: deleted_file.clone(),
target,
});
}
Ok(DeletionAnalysis {
deletable,
already_absent,
already_absent_paths,
conflicts,
})
}
fn apply_deletions(
layout: &RepositoryLayout,
deletable: &[DeletableFile],
already_absent: &[crate::path::RepoPath],
) -> Result<usize> {
let mut removed = 0_usize;
for item in deletable {
remove_worktree_file_required(
layout.worktree_mutation_root(),
Path::new(item.path.path.as_str()),
)?;
removed += 1;
}
for path in already_absent {
remove_worktree_file_required(layout.worktree_mutation_root(), Path::new(path.as_str()))?;
}
Ok(removed)
}
#[cfg(all(test, target_os = "linux"))]
mod tests;