1use prikk_error::{PrikkError, Result};
18use prikk_object::{
19 BlockKind, BlockPayload, CanonicalEncode, ObjectEnvelope, ObjectId, ObjectType, RefKind,
20 RefStatePayload, RefUpdatePayload,
21};
22
23use crate::merge_evidence::{
24 MergeEvidenceTarget, candidate_patch_ids, prepare_merge_evidence,
25 verify_candidate_blocks_trusted,
26};
27use crate::received::validate_received_ref;
28use crate::{
29 MaintainerSigner, ObjectReader, ObjectWriteSession, ObjectWriter, RefPublication, RefStore,
30 RepositoryLayout, derive_next_state_root, maintainer_signature, validate_local_branch_ref,
31 verify_signer_trusted,
32};
33
34#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct MergeExecutionReport {
37 pub into_ref: String,
39 pub from_ref: String,
41 pub baseline_block_id: ObjectId,
43 pub parent_block_id: ObjectId,
45 pub adopted_target_block_id: ObjectId,
47 pub adopted_patch_ids: Vec<ObjectId>,
49 pub block_id: ObjectId,
51 pub ref_state_id: ObjectId,
53}
54
55pub fn execute_merge(
62 layout: &RepositoryLayout,
63 baseline_block_id: ObjectId,
64 into_ref: &str,
65 from_ref: &str,
66 signer: &impl MaintainerSigner,
67) -> Result<MergeExecutionReport> {
68 layout.require_current_format()?;
69 let into_ref = validate_local_branch_ref(into_ref)?;
70 let from_is_received = from_ref.starts_with("remotes/");
76 let (from_target, from_ref) = if from_is_received {
83 validate_received_ref(from_ref)?;
84 (
85 MergeEvidenceTarget::ReceivedRef(from_ref.to_string()),
86 from_ref.to_string(),
87 )
88 } else {
89 let canonical = validate_local_branch_ref(from_ref)?;
90 (MergeEvidenceTarget::Ref(canonical.clone()), canonical)
91 };
92 if into_ref == from_ref {
93 return Err(PrikkError::InvalidName(
94 "merge into_ref and from_ref must differ".to_string(),
95 ));
96 }
97 let evidence = prepare_merge_evidence(
100 layout,
101 baseline_block_id,
102 MergeEvidenceTarget::Ref(into_ref.clone()),
103 from_target,
104 )?;
105 if !evidence.is_confluent() {
106 return Err(PrikkError::Integrity(format!(
107 "merge refused: {from_ref} is not confluent with {into_ref} from baseline \
108 {baseline_block_id} (outcome: {}{})",
109 evidence.outcome,
110 evidence
111 .reason
112 .map(|reason| format!(", reason: {reason}"))
113 .unwrap_or_default(),
114 )));
115 }
116
117 let policy = verify_signer_trusted(layout, signer)?;
119
120 let mut object_store = ObjectWriteSession::open(layout)?;
121
122 if from_is_received {
126 verify_candidate_blocks_trusted(
127 &object_store,
128 &policy,
129 baseline_block_id,
130 evidence.right_selector.target_block_id,
131 )?;
132 }
133
134 let adopted_patch_ids = candidate_patch_ids(
135 &object_store,
136 baseline_block_id,
137 evidence.right_selector.target_block_id,
138 )?;
139 if adopted_patch_ids.is_empty() {
140 return Err(PrikkError::Integrity(format!(
141 "{from_ref} has no patches to adopt since baseline {baseline_block_id}"
142 )));
143 }
144
145 let ref_store = RefStore::new(layout.clone());
146 let into_ref_state_id = ref_store
147 .read_current_ref_state_id(&into_ref)?
148 .ok_or_else(|| PrikkError::Integrity(format!("ref {into_ref} is not published")))?;
149 let into_ref_state_envelope = object_store
150 .read_typed(into_ref_state_id, ObjectType::RefState)?
151 .ok_or_else(|| {
152 PrikkError::Integrity(format!("ref {into_ref} points to missing RefState"))
153 })?;
154 let into_ref_state = RefStatePayload::decode_canonical(
155 &into_ref_state_envelope.canonical_payload,
156 into_ref_state_envelope.schema_version,
157 )?;
158 let parent_block_id = into_ref_state.target_object_id;
159 if parent_block_id != evidence.left_selector.target_block_id {
160 return Err(PrikkError::Integrity(format!(
161 "ref {into_ref} advanced during merge evidence gathering; retry"
162 )));
163 }
164
165 let state_merkle_root =
166 derive_next_state_root(&object_store, Some(parent_block_id), &adopted_patch_ids)?;
167 let adopted_target_block_id = evidence.right_selector.target_block_id;
168 let mut parent_block_ids = vec![parent_block_id, adopted_target_block_id];
169 parent_block_ids.sort();
170 let block_payload = BlockPayload {
171 parent_block_ids,
172 kind: BlockKind::Merge,
173 patch_ids: adopted_patch_ids.clone(),
174 state_merkle_root,
175 snapshot_blob_ref: None,
176 mainline_parent_id: Some(parent_block_id),
177 merge_baseline_block_id: Some(baseline_block_id),
178 };
179 let block_envelope = signed_envelope(
180 ObjectType::Block,
181 2,
182 block_payload.to_canonical_bytes()?,
183 signer,
184 )?;
185 let block_id = object_store.write_object(&block_envelope)?;
186
187 let update_seq = into_ref_state.update_seq + 1;
188 let ref_state_payload = RefStatePayload {
189 ref_name: into_ref.clone(),
190 kind: RefKind::Branch,
191 target_object_id: block_id,
192 update_seq,
193 previous_ref_state_id: Some(into_ref_state_id),
194 required_attestation_ids: Vec::new(),
195 closed: false,
196 };
197 let ref_state_envelope = signed_envelope(
198 ObjectType::RefState,
199 1,
200 ref_state_payload.to_canonical_bytes()?,
201 signer,
202 )?;
203 let ref_state_id = ref_state_envelope.object_id();
204
205 let ref_update_payload = RefUpdatePayload {
206 ref_name: into_ref.clone(),
207 old_ref_state_id: Some(into_ref_state_id),
208 new_ref_state_id: ref_state_id,
209 new_target_object_id: block_id,
210 update_seq,
211 created_at: 0,
212 author_key_id: signer.key_id().to_string(),
213 };
214 let ref_update_envelope = signed_envelope(
215 ObjectType::RefUpdate,
216 1,
217 ref_update_payload.to_canonical_bytes()?,
218 signer,
219 )?;
220
221 let publication = RefPublication {
222 ref_name: into_ref.clone(),
223 expected_previous_ref_state_id: Some(into_ref_state_id),
224 ref_state: ref_state_envelope,
225 ref_update: ref_update_envelope,
226 };
227 let published_ref_state_id =
228 ref_store.publish_with_object_store(&mut object_store, &publication)?;
229
230 Ok(MergeExecutionReport {
231 into_ref,
232 from_ref,
233 baseline_block_id,
234 parent_block_id,
235 adopted_target_block_id,
236 adopted_patch_ids,
237 block_id,
238 ref_state_id: published_ref_state_id,
239 })
240}
241
242fn signed_envelope(
243 object_type: ObjectType,
244 schema_version: u32,
245 canonical_payload: Vec<u8>,
246 signer: &impl MaintainerSigner,
247) -> Result<ObjectEnvelope> {
248 let mut envelope = ObjectEnvelope::unsigned(object_type, schema_version, canonical_payload);
249 let object_id = envelope.object_id();
250 envelope.add_signature(maintainer_signature(signer, object_type, object_id)?)?;
251 Ok(envelope)
252}
253
254#[cfg(test)]
255mod tests;