use std::path::PathBuf;
use prikk_error::PrikkError;
use prikk_object::{
CanonicalEncode, ObjectEnvelope, ObjectId, ObjectType, RefKind, RefStatePayload,
RefUpdatePayload,
};
use prikk_store::{
DEFAULT_CHECKOUT_REF, FileObjectStore, MaintainerSigner, RefPublication, RefStore, Wal,
maintainer_signature, require_active_ref_for_non_empty_wal, validate_local_branch_ref,
};
const REF_STATE_SCHEMA_OPEN: u32 = 1;
const REF_STATE_SCHEMA_CLOSED: u32 = 2;
pub fn run_branch(root: PathBuf, args: Vec<String>) -> std::result::Result<(), String> {
let mut iter = args.into_iter();
let first = iter.next();
match first.as_deref() {
None | Some("list") => run_list(root, iter.collect()),
Some("create") => run_create(root, iter.collect()),
Some("close") => run_close(root, iter.collect()),
Some(flag) if flag.starts_with('-') => {
let mut rest = vec![flag.to_string()];
rest.extend(iter);
run_list(root, rest)
}
Some(other) => Err(format!(
"unknown branch subcommand: {other} (expected list, create, or close)"
)),
}
}
fn run_list(root: PathBuf, args: Vec<String>) -> std::result::Result<(), String> {
let mut show_all = false;
for arg in args {
match arg.as_str() {
"--all" => show_all = true,
other => return Err(format!("unknown branch list argument: {other}")),
}
}
let layout = crate::open_repository(root)?;
let ref_store = RefStore::new(layout.clone());
let object_store = FileObjectStore::new(layout);
let entries = ref_store
.list_ref_pointers()
.map_err(|err| err.to_string())?;
let mut printed_any = false;
for entry in entries {
let envelope = object_store
.read_typed(entry.ref_state_id, ObjectType::RefState)
.map_err(|err| err.to_string())?
.ok_or_else(|| {
format!(
"ref {} RefState {} is missing",
entry.ref_name, entry.ref_state_id
)
})?;
let payload =
RefStatePayload::decode_canonical(&envelope.canonical_payload, envelope.schema_version)
.map_err(|err| err.to_string())?;
if payload.closed && !show_all {
continue;
}
if payload.closed {
println!("{} {} (closed)", entry.ref_name, entry.ref_state_id);
} else {
println!("{} {}", entry.ref_name, entry.ref_state_id);
}
printed_any = true;
}
if !printed_any {
println!("no branches");
}
Ok(())
}
fn run_create(root: PathBuf, args: Vec<String>) -> std::result::Result<(), String> {
let parsed = parse_create_args(args)?;
let layout = crate::open_repository(root)?;
layout
.require_current_format()
.map_err(|err| err.to_string())?;
let canonical = validate_local_branch_ref(&parsed.name).map_err(|err| err.to_string())?;
let ref_store = RefStore::new(layout.clone());
let object_store = FileObjectStore::new(layout.clone());
if ref_store
.read_current_ref_state_id(&canonical)
.map_err(|err| err.to_string())?
.is_some()
{
return Err(format!("branch {canonical} already exists"));
}
if ref_store
.recoverable_missing_ref(&canonical)
.map_err(|err| err.to_string())?
.is_some()
{
return Err(format!(
"branch {canonical} has a surviving ref log with no live pointer; resuming it is not \
yet supported (see DC-61), and creating over it would produce a corrupt state"
));
}
let from_ref = parsed
.from
.unwrap_or_else(|| DEFAULT_CHECKOUT_REF.to_string());
let target_object_id = resolve_published_target(&ref_store, &object_store, &from_ref)?;
let signer = crate::maintainer_signer_from_env()?;
let ref_state_payload = RefStatePayload {
ref_name: canonical.clone(),
kind: RefKind::Branch,
target_object_id,
update_seq: 1,
previous_ref_state_id: None,
required_attestation_ids: Vec::new(),
closed: false,
};
let ref_state_envelope = signed_envelope(
ObjectType::RefState,
REF_STATE_SCHEMA_OPEN,
ref_state_payload
.to_canonical_bytes()
.map_err(|err| err.to_string())?,
&signer,
)?;
let ref_state_id = ref_state_envelope.object_id();
let ref_update_payload = RefUpdatePayload {
ref_name: canonical.clone(),
old_ref_state_id: None,
new_ref_state_id: ref_state_id,
new_target_object_id: target_object_id,
update_seq: 1,
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()
.map_err(|err| err.to_string())?,
&signer,
)?;
let publication = RefPublication {
ref_name: canonical.clone(),
expected_previous_ref_state_id: None,
ref_state: ref_state_envelope,
ref_update: ref_update_envelope,
};
let published_ref_state_id = ref_store
.publish(&publication)
.map_err(|err| err.to_string())?;
println!("created branch {canonical}");
println!("target block: {target_object_id}");
println!("RefState: {published_ref_state_id}");
println!("update_seq: 1");
Ok(())
}
fn run_close(root: PathBuf, args: Vec<String>) -> std::result::Result<(), String> {
let mut name = None;
for arg in args {
if name.is_some() {
return Err(format!(
"branch close accepts at most one name, got extra: {arg}"
));
}
name = Some(arg);
}
let Some(name) = name else {
return Err("branch close requires <name>".to_string());
};
let layout = crate::open_repository(root)?;
layout
.require_current_format()
.map_err(|err| err.to_string())?;
let canonical = validate_local_branch_ref(&name).map_err(|err| err.to_string())?;
let ref_store = RefStore::new(layout.clone());
let object_store = FileObjectStore::new(layout.clone());
let Some(current_ref_state_id) = ref_store
.read_current_ref_state_id(&canonical)
.map_err(|err| err.to_string())?
else {
return Err(format!("branch {canonical} does not exist"));
};
let current_envelope = object_store
.read_typed(current_ref_state_id, ObjectType::RefState)
.map_err(|err| err.to_string())?
.ok_or_else(|| format!("branch {canonical} RefState {current_ref_state_id} is missing"))?;
let current_payload = RefStatePayload::decode_canonical(
¤t_envelope.canonical_payload,
current_envelope.schema_version,
)
.map_err(|err| err.to_string())?;
if current_payload.closed {
return Err(format!("branch {canonical} is already closed"));
}
let replay = Wal::for_layout(&layout)
.replay()
.map_err(|err| err.to_string())?;
if !replay.records.is_empty() {
match require_active_ref_for_non_empty_wal(&layout, &canonical) {
Ok(_) => {
return Err(format!(
"cannot close {canonical}: it owns a non-empty active WAL; seal it before closing"
));
}
Err(PrikkError::LockConflict(_)) => {}
Err(err) => return Err(err.to_string()),
}
}
let next_seq = current_payload
.update_seq
.checked_add(1)
.ok_or_else(|| "ref-state update sequence overflow".to_string())?;
let signer = crate::maintainer_signer_from_env()?;
let ref_state_payload = RefStatePayload {
ref_name: canonical.clone(),
kind: current_payload.kind,
target_object_id: current_payload.target_object_id,
update_seq: next_seq,
previous_ref_state_id: Some(current_ref_state_id),
required_attestation_ids: current_payload.required_attestation_ids.clone(),
closed: true,
};
let ref_state_envelope = signed_envelope(
ObjectType::RefState,
REF_STATE_SCHEMA_CLOSED,
ref_state_payload
.to_canonical_bytes()
.map_err(|err| err.to_string())?,
&signer,
)?;
let ref_state_id = ref_state_envelope.object_id();
let ref_update_payload = RefUpdatePayload {
ref_name: canonical.clone(),
old_ref_state_id: Some(current_ref_state_id),
new_ref_state_id: ref_state_id,
new_target_object_id: current_payload.target_object_id,
update_seq: next_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()
.map_err(|err| err.to_string())?,
&signer,
)?;
let publication = RefPublication {
ref_name: canonical.clone(),
expected_previous_ref_state_id: Some(current_ref_state_id),
ref_state: ref_state_envelope,
ref_update: ref_update_envelope,
};
let published_ref_state_id = ref_store
.publish(&publication)
.map_err(|err| err.to_string())?;
println!("closed branch {canonical}");
println!("RefState: {published_ref_state_id}");
println!(
"nothing was reclaimed; the pointer, its history, and every object remain, and the branch is recoverable"
);
Ok(())
}
fn resolve_published_target(
ref_store: &RefStore,
object_store: &FileObjectStore,
from_ref: &str,
) -> std::result::Result<ObjectId, String> {
let from_ref_state_id = ref_store
.read_current_ref_state_id(from_ref)
.map_err(|err| err.to_string())?
.ok_or_else(|| format!("--from ref {from_ref} does not resolve to a published ref"))?;
let from_envelope = object_store
.read_typed(from_ref_state_id, ObjectType::RefState)
.map_err(|err| err.to_string())?
.ok_or_else(|| format!("--from ref {from_ref} RefState {from_ref_state_id} is missing"))?;
let from_payload = RefStatePayload::decode_canonical(
&from_envelope.canonical_payload,
from_envelope.schema_version,
)
.map_err(|err| err.to_string())?;
if from_payload.ref_name != from_ref {
return Err(format!(
"--from RefState name mismatch: expected {from_ref}, got {}",
from_payload.ref_name
));
}
if object_store
.read_typed(from_payload.target_object_id, ObjectType::Block)
.map_err(|err| err.to_string())?
.is_none()
{
return Err(format!(
"--from ref {from_ref} targets missing block {}",
from_payload.target_object_id
));
}
Ok(from_payload.target_object_id)
}
fn signed_envelope(
object_type: ObjectType,
schema_version: u32,
canonical_payload: Vec<u8>,
signer: &impl MaintainerSigner,
) -> std::result::Result<ObjectEnvelope, String> {
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).map_err(|err| err.to_string())?,
)
.map_err(|err| err.to_string())?;
Ok(envelope)
}
struct CreateArgs {
name: String,
from: Option<String>,
}
fn parse_create_args(args: Vec<String>) -> std::result::Result<CreateArgs, String> {
let mut name = None;
let mut from = None;
let mut iter = args.into_iter();
while let Some(arg) = iter.next() {
match arg.as_str() {
"--from" => {
let Some(value) = iter.next() else {
return Err("branch create --from requires a value".to_string());
};
from = Some(value);
}
other if other.starts_with('-') => {
return Err(format!("unknown branch create argument: {other}"));
}
_ => {
if name.is_some() {
return Err("branch create accepts at most one name".to_string());
}
name = Some(arg);
}
}
}
let Some(name) = name else {
return Err("branch create requires <name>".to_string());
};
Ok(CreateArgs { name, from })
}