use std::collections::{BTreeMap, BTreeSet};
use prikk_error::{PrikkError, Result};
use prikk_object::{
CanonicalEncode, ObjectEnvelope, ObjectId, ObjectType, RecognitionClaimPayload, RefKind,
RefStatePayload, TagPayload,
};
use crate::layout::RepositoryLayout;
use crate::maintainer_signing::{MaintainerSigner, maintainer_signature};
use crate::merge_evidence::ancestors_inclusive;
use crate::object_store::{ObjectReadSnapshot, ObjectReader, ObjectWriteSession, ObjectWriter};
use crate::patch_exchange::{ExchangeExportReport, export_exchange_artifact};
use crate::refs::{RefStore, validate_local_branch_ref};
use crate::trust::{GatedOperation, verify_signer_trusted};
use super::have_list::{
DEFAULT_HAVE_LIST_MAX_PATCH_COUNT, DEFAULT_HAVE_LIST_MAX_TOTAL_BYTES, decode_have_list,
};
use super::{compute_sync_delta, resolve_branch_ref_tip};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SyncArtifactBuildReport {
pub ref_name: String,
pub delta_patch_count: usize,
pub claim_count: usize,
pub tag_count: usize,
pub export_report: ExchangeExportReport,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SyncArtifactOutcome {
AlreadyInSync {
ref_name: String,
},
Artifact {
report: SyncArtifactBuildReport,
bytes: Vec<u8>,
},
}
pub fn build_sync_artifact(
layout: &RepositoryLayout,
ref_name: &str,
have_list_bytes: &[u8],
signer: &impl MaintainerSigner,
) -> Result<SyncArtifactOutcome> {
let have_list = decode_have_list(
have_list_bytes,
DEFAULT_HAVE_LIST_MAX_TOTAL_BYTES,
DEFAULT_HAVE_LIST_MAX_PATCH_COUNT,
)?;
let canonical_ref = validate_local_branch_ref(ref_name)?;
if have_list.ref_name != canonical_ref {
return Err(PrikkError::Integrity(format!(
"have-list names ref {}, but {canonical_ref} was requested",
have_list.ref_name
)));
}
let delta_patch_ids = compute_sync_delta(layout, &have_list)?;
let read_snapshot = ObjectReadSnapshot::open(layout)?;
let ref_store = RefStore::new(layout.clone());
let tip_block_id = resolve_branch_ref_tip(&read_snapshot, &ref_store, &canonical_ref)?;
if !delta_patch_ids.is_empty() && tip_block_id.is_none() {
return Err(PrikkError::Integrity(format!(
"ref {canonical_ref} has a non-empty delta but no local tip -- inconsistent state"
)));
}
let ancestors = match tip_block_id {
Some(id) => ancestors_inclusive(&read_snapshot, id)?,
None => BTreeMap::new(),
};
let mut tag_ids: Vec<ObjectId> = Vec::new();
for pointer in ref_store.list_ref_pointers()? {
if !pointer.ref_name.starts_with("tags/") {
continue;
}
let ref_state_envelope = read_snapshot
.read_typed(pointer.ref_state_id, ObjectType::RefState)?
.ok_or_else(|| {
PrikkError::Integrity(format!(
"tag ref {} names missing RefState {}",
pointer.ref_name, pointer.ref_state_id
))
})?;
let ref_state_payload = RefStatePayload::decode_canonical(
&ref_state_envelope.canonical_payload,
ref_state_envelope.schema_version,
)?;
if ref_state_payload.kind != RefKind::Tag {
continue;
}
let tag_id = ref_state_payload.target_object_id;
let tag_envelope = read_snapshot
.read_typed(tag_id, ObjectType::Tag)?
.ok_or_else(|| PrikkError::Integrity(format!("missing Tag object: {tag_id}")))?;
let tag_payload = TagPayload::decode_canonical(&tag_envelope.canonical_payload)?;
if ancestors.contains_key(&tag_payload.target_block_id) {
tag_ids.push(tag_id);
}
}
tag_ids.sort_unstable();
if delta_patch_ids.is_empty() && tag_ids.is_empty() {
return Ok(SyncArtifactOutcome::AlreadyInSync {
ref_name: have_list.ref_name,
});
}
verify_signer_trusted(layout, signer, GatedOperation::SyncBuild)?;
let delta_set: BTreeSet<ObjectId> = delta_patch_ids.iter().copied().collect();
let mut qualifying_block_ids: Vec<ObjectId> = ancestors
.iter()
.filter(|(_, block)| block.patch_ids.iter().any(|id| delta_set.contains(id)))
.map(|(block_id, _)| *block_id)
.collect();
qualifying_block_ids.sort_unstable();
let mut write_session = ObjectWriteSession::open(layout)?;
let mut claim_ids = Vec::with_capacity(qualifying_block_ids.len());
for block_id in &qualifying_block_ids {
let block = ancestors.get(block_id).ok_or_else(|| {
PrikkError::Integrity(format!(
"block {block_id} vanished from its own ancestry walk"
))
})?;
let claim_payload = RecognitionClaimPayload {
block_id: *block_id,
patch_ids: block.patch_ids.clone(),
parent_block_ids: block.parent_block_ids.clone(),
};
let mut claim_envelope = ObjectEnvelope::unsigned(
ObjectType::RecognitionClaim,
1,
claim_payload.to_canonical_bytes()?,
);
let claim_id = claim_envelope.object_id();
claim_envelope.add_signature(maintainer_signature(
signer,
ObjectType::RecognitionClaim,
claim_id,
)?)?;
let written_id = write_session.write_object(&claim_envelope)?;
claim_ids.push(written_id);
}
let (export_report, bytes) =
export_exchange_artifact(layout, &delta_patch_ids, &claim_ids, &tag_ids)?;
Ok(SyncArtifactOutcome::Artifact {
report: SyncArtifactBuildReport {
ref_name: have_list.ref_name,
delta_patch_count: delta_patch_ids.len(),
claim_count: claim_ids.len(),
tag_count: tag_ids.len(),
export_report,
},
bytes,
})
}
#[cfg(test)]
mod sender_test_support;
#[cfg(test)]
mod tests;