use std::collections::BTreeMap;
use prikk_error::{PrikkError, Result};
use prikk_hash::sha256;
use prikk_object::{
BlobKind, BlobPayload, CanonicalEncode, ObjectEnvelope, ObjectId, ObjectType, Signature,
SignatureAlgorithm, SignerRole,
};
use crate::checkout::prepare_snapshot_checkout_plan;
use crate::layout::RepositoryLayout;
use crate::object_store::{FileObjectStore, ObjectReader, ObjectWriter};
use crate::snapshot::SnapshotManifest;
use crate::worktree_status::{WorktreeChangeKind, worktree_status};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorktreePatchCommitReport {
pub ref_name: String,
pub patch_id: ObjectId,
pub wal_sequence: u64,
pub operation_count: usize,
pub referenced_blob_count: usize,
pub text_edit_count: usize,
pub changes: Vec<WorktreePatchOperationSummary>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorktreePatchOperationSummary {
pub path: String,
pub operation: WorktreePatchOperationKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WorktreePatchOperationKind {
CreateFile,
DeleteFile,
ReplaceBinary,
EditText,
}
impl WorktreePatchOperationKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::CreateFile => "create-file",
Self::DeleteFile => "delete-file",
Self::ReplaceBinary => "replace-binary",
Self::EditText => "edit-text",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WorktreePatchCommitOptions {
pub prefer_text_edits: bool,
}
impl WorktreePatchCommitOptions {
#[must_use]
pub const fn file_level() -> Self {
Self {
prefer_text_edits: false,
}
}
#[must_use]
pub const fn prefer_text_edits() -> Self {
Self {
prefer_text_edits: true,
}
}
}
impl Default for WorktreePatchCommitOptions {
fn default() -> Self {
Self::file_level()
}
}
pub fn commit_worktree_changes(
layout: &RepositoryLayout,
ref_name: &str,
message: &str,
) -> Result<WorktreePatchCommitReport> {
commit_worktree_changes_with_options(
layout,
ref_name,
message,
WorktreePatchCommitOptions::file_level(),
)
}
pub fn commit_worktree_changes_with_options(
layout: &RepositoryLayout,
ref_name: &str,
message: &str,
_options: WorktreePatchCommitOptions,
) -> Result<WorktreePatchCommitReport> {
if message.trim().is_empty() {
return Err(PrikkError::InvalidName(
"commit message must not be empty".to_string(),
));
}
let status = worktree_status(layout, ref_name)?;
if status.is_clean() {
return Err(PrikkError::InvalidName(
"worktree has no snapshot-baseline changes to commit".to_string(),
));
}
if status.count_kind(WorktreeChangeKind::UnsupportedPath) > 0 {
return Err(PrikkError::InvalidName(
"worktree contains paths that cannot be represented safely".to_string(),
));
}
let change = status.changes.first().ok_or_else(|| {
PrikkError::Integrity("worktree change set unexpectedly empty".to_string())
})?;
match change.kind {
WorktreeChangeKind::Missing => Err(PrikkError::Integrity(format!(
"worktree delete authoring is pending the node model \
(increment 4.4 path->node_id tracking): {}",
change.path
))),
WorktreeChangeKind::Modified => Err(PrikkError::Integrity(format!(
"worktree modified-file authoring is pending the node model \
(increment 4.4 path->node_id tracking; ReplaceBinary binary-only blob \
check; EditText needs FDD-01 §7.2.1): {}",
change.path
))),
WorktreeChangeKind::Untracked => Err(PrikkError::Integrity(format!(
"worktree create authoring is pending the node model \
(increment 4.4a node_id minting): {}",
change.path
))),
WorktreeChangeKind::UnsupportedPath => Err(PrikkError::InvalidName(format!(
"unsupported worktree path cannot become a patch operation: {}",
change.path
))),
}
}
#[allow(dead_code)] fn load_snapshot_baseline(
layout: &RepositoryLayout,
ref_name: &str,
) -> Result<BTreeMap<String, Vec<u8>>> {
let plan = prepare_snapshot_checkout_plan(layout, ref_name)?;
let object_store = FileObjectStore::new(layout.clone());
let Some(envelope) = object_store.read_object(plan.snapshot_blob_id)? else {
return Err(PrikkError::Integrity(format!(
"snapshot Blob {} is missing",
plan.snapshot_blob_id
)));
};
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)?;
let manifest = SnapshotManifest::decode(&snapshot_content)?;
let mut out = BTreeMap::new();
for entry in manifest.files {
out.insert(entry.path.as_str().to_string(), entry.bytes);
}
Ok(out)
}
#[allow(dead_code)] fn write_blob(object_store: &mut FileObjectStore, bytes: &[u8]) -> Result<ObjectId> {
let payload = BlobPayload::new(BlobKind::Text, bytes.to_vec());
let canonical_payload = payload.to_canonical_bytes()?;
let envelope = ObjectEnvelope::unsigned(ObjectType::Blob, 1, canonical_payload);
object_store.write_object(&envelope)
}
#[allow(dead_code)] fn next_op_seq(index: usize) -> Result<u32> {
let next = index
.checked_add(1)
.ok_or_else(|| PrikkError::CanonicalEncoding("operation count overflow".to_string()))?;
u32::try_from(next)
.map_err(|_| PrikkError::CanonicalEncoding("operation count exceeds u32".to_string()))
}
#[allow(dead_code)] fn dev_author_signature(message: &str) -> Signature {
let mut signature_preimage = Vec::new();
signature_preimage.extend_from_slice(b"prikk.dev.placeholder-signature.v1");
signature_preimage.extend_from_slice(message.as_bytes());
Signature {
algorithm: SignatureAlgorithm::Ed25519,
key_id: "dev-placeholder-author".to_string(),
signature_bytes: sha256(&signature_preimage).to_vec(),
created_at: 0,
signer_role: SignerRole::Author,
}
}
#[cfg(test)]
mod tests;