use prikk_error::{PrikkError, Result};
use prikk_object::{
BlockKind, BlockPayload, CanonicalEncode, ObjectEnvelope, ObjectId, ObjectType, RefKind,
RefStatePayload, RefUpdatePayload,
};
use crate::merge_evidence::{
MergeEvidenceTarget, candidate_patch_ids, prepare_merge_evidence,
verify_candidate_blocks_trusted,
};
use crate::received::validate_received_ref;
use crate::{
GatedOperation, MaintainerSigner, ObjectReader, ObjectWriteSession, ObjectWriter,
RefPublication, RefStore, RepositoryLayout, derive_next_state_root, maintainer_signature,
validate_local_branch_ref, verify_signer_trusted,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MergeExecutionReport {
pub into_ref: String,
pub from_ref: String,
pub baseline_block_id: ObjectId,
pub parent_block_id: ObjectId,
pub adopted_target_block_id: ObjectId,
pub adopted_patch_ids: Vec<ObjectId>,
pub block_id: ObjectId,
pub ref_state_id: ObjectId,
}
pub fn execute_merge(
layout: &RepositoryLayout,
baseline_block_id: ObjectId,
into_ref: &str,
from_ref: &str,
signer: &impl MaintainerSigner,
) -> Result<MergeExecutionReport> {
layout.require_current_format()?;
let into_ref = validate_local_branch_ref(into_ref)?;
let from_is_received = from_ref.starts_with("remotes/");
let (from_target, from_ref) = if from_is_received {
validate_received_ref(from_ref)?;
(
MergeEvidenceTarget::ReceivedRef(from_ref.to_string()),
from_ref.to_string(),
)
} else {
let canonical = validate_local_branch_ref(from_ref)?;
(MergeEvidenceTarget::Ref(canonical.clone()), canonical)
};
if into_ref == from_ref {
return Err(PrikkError::InvalidName(
"merge into_ref and from_ref must differ".to_string(),
));
}
let evidence = prepare_merge_evidence(
layout,
baseline_block_id,
MergeEvidenceTarget::Ref(into_ref.clone()),
from_target,
)?;
if !evidence.is_confluent() {
return Err(PrikkError::Integrity(format!(
"merge refused: {from_ref} is not confluent with {into_ref} from baseline \
{baseline_block_id} (outcome: {}{})",
evidence.outcome,
evidence
.reason
.map(|reason| format!(", reason: {reason}"))
.unwrap_or_default(),
)));
}
let policy = verify_signer_trusted(layout, signer, GatedOperation::Merge)?;
let mut object_store = ObjectWriteSession::open(layout)?;
if from_is_received {
verify_candidate_blocks_trusted(
&object_store,
&policy,
baseline_block_id,
evidence.right_selector.target_block_id,
)?;
}
let adopted_patch_ids = candidate_patch_ids(
&object_store,
baseline_block_id,
evidence.right_selector.target_block_id,
)?;
if adopted_patch_ids.is_empty() {
return Err(PrikkError::Integrity(format!(
"{from_ref} has no patches to adopt since baseline {baseline_block_id}"
)));
}
let ref_store = RefStore::new(layout.clone());
let into_ref_state_id = ref_store
.read_current_ref_state_id(&into_ref)?
.ok_or_else(|| PrikkError::Integrity(format!("ref {into_ref} is not published")))?;
let into_ref_state_envelope = object_store
.read_typed(into_ref_state_id, ObjectType::RefState)?
.ok_or_else(|| {
PrikkError::Integrity(format!("ref {into_ref} points to missing RefState"))
})?;
let into_ref_state = RefStatePayload::decode_canonical(
&into_ref_state_envelope.canonical_payload,
into_ref_state_envelope.schema_version,
)?;
let parent_block_id = into_ref_state.target_object_id;
if parent_block_id != evidence.left_selector.target_block_id {
return Err(PrikkError::Integrity(format!(
"ref {into_ref} advanced during merge evidence gathering; retry"
)));
}
let state_merkle_root =
derive_next_state_root(&object_store, Some(parent_block_id), &adopted_patch_ids)?;
let adopted_target_block_id = evidence.right_selector.target_block_id;
let mut parent_block_ids = vec![parent_block_id, adopted_target_block_id];
parent_block_ids.sort();
let block_payload = BlockPayload {
parent_block_ids,
kind: BlockKind::Merge,
patch_ids: adopted_patch_ids.clone(),
state_merkle_root,
snapshot_blob_ref: None,
mainline_parent_id: Some(parent_block_id),
merge_baseline_block_id: Some(baseline_block_id),
};
let block_envelope = signed_envelope(
ObjectType::Block,
2,
block_payload.to_canonical_bytes()?,
signer,
)?;
let block_id = object_store.write_object(&block_envelope)?;
let update_seq = into_ref_state.update_seq + 1;
let ref_state_payload = RefStatePayload {
ref_name: into_ref.clone(),
kind: RefKind::Branch,
target_object_id: block_id,
update_seq,
previous_ref_state_id: Some(into_ref_state_id),
required_attestation_ids: Vec::new(),
closed: false,
};
let ref_state_envelope = signed_envelope(
ObjectType::RefState,
1,
ref_state_payload.to_canonical_bytes()?,
signer,
)?;
let ref_state_id = ref_state_envelope.object_id();
let ref_update_payload = RefUpdatePayload {
ref_name: into_ref.clone(),
old_ref_state_id: Some(into_ref_state_id),
new_ref_state_id: ref_state_id,
new_target_object_id: block_id,
update_seq,
created_at: 0,
author_key_id: signer.key_id().to_string(),
};
let ref_update_envelope = signed_envelope(
ObjectType::RefUpdate,
1,
ref_update_payload.to_canonical_bytes()?,
signer,
)?;
let publication = RefPublication {
ref_name: into_ref.clone(),
expected_previous_ref_state_id: Some(into_ref_state_id),
ref_state: ref_state_envelope,
ref_update: ref_update_envelope,
};
let published_ref_state_id =
ref_store.publish_with_object_store(&mut object_store, &publication)?;
Ok(MergeExecutionReport {
into_ref,
from_ref,
baseline_block_id,
parent_block_id,
adopted_target_block_id,
adopted_patch_ids,
block_id,
ref_state_id: published_ref_state_id,
})
}
fn signed_envelope(
object_type: ObjectType,
schema_version: u32,
canonical_payload: Vec<u8>,
signer: &impl MaintainerSigner,
) -> Result<ObjectEnvelope> {
let mut envelope = ObjectEnvelope::unsigned(object_type, schema_version, canonical_payload);
let object_id = envelope.object_id();
envelope.add_signature(maintainer_signature(signer, object_type, object_id)?)?;
Ok(envelope)
}
#[cfg(test)]
mod tests;