use std::path::PathBuf;
use prikk_object::{ObjectId, ObjectType, RefKind, RefStatePayload, TagPayload};
use prikk_store::{
FileObjectStore, ObjectReader, ObjectWriteSession, RefStore,
compute_patch_set_digest_and_count_from_block, create_local_tag, validate_local_tag_ref,
};
pub fn run_tag(root: PathBuf, args: Vec<String>) -> std::result::Result<(), String> {
let mut iter = args.into_iter();
match iter.next().as_deref() {
None | Some("list") => run_list(root, iter.collect()),
Some("create") => run_create(root, iter.collect()),
Some(other) => Err(format!(
"unknown tag subcommand: {other} (expected list or create)"
)),
}
}
fn run_list(root: PathBuf, args: Vec<String>) -> std::result::Result<(), String> {
if let Some(arg) = args.into_iter().next() {
return Err(format!("unknown tag list argument: {arg}"));
}
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 ref_state_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 ref_state_payload = RefStatePayload::decode_canonical(
&ref_state_envelope.canonical_payload,
ref_state_envelope.schema_version,
)
.map_err(|err| err.to_string())?;
if ref_state_payload.kind != RefKind::Tag {
continue;
}
let tag_envelope = object_store
.read_typed(ref_state_payload.target_object_id, ObjectType::Tag)
.map_err(|err| err.to_string())?
.ok_or_else(|| {
format!(
"tag ref {} targets missing tag object {}",
entry.ref_name, ref_state_payload.target_object_id
)
})?;
let tag_payload = TagPayload::decode_canonical(&tag_envelope.canonical_payload)
.map_err(|err| err.to_string())?;
println!("{} {}", entry.ref_name, tag_payload.target_block_id);
printed_any = true;
}
if !printed_any {
println!("no tags");
}
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)?;
let ref_store = RefStore::new(layout.clone());
let mut object_store = ObjectWriteSession::open(&layout).map_err(|err| err.to_string())?;
let target_block_id = resolve_target_block(&ref_store, &object_store, &parsed.target)?;
let (patch_set_digest, patch_count) =
compute_patch_set_digest_and_count_from_block(&object_store, target_block_id)
.map_err(|err| err.to_string())?;
let signer = crate::maintainer_signer_from_env()?;
let created = create_local_tag(
&layout,
&mut object_store,
&parsed.name,
target_block_id,
parsed.message,
patch_set_digest,
patch_count,
&signer,
)
.map_err(|err| err.to_string())?;
let canonical = validate_local_tag_ref(&parsed.name).map_err(|err| err.to_string())?;
println!("created tag {canonical}");
println!("tag object: {}", created.tag_object_id);
println!("target block: {target_block_id}");
println!("RefState: {}", created.ref_state_id);
Ok(())
}
fn resolve_target_block(
ref_store: &RefStore,
object_store: &impl ObjectReader,
target: &str,
) -> std::result::Result<ObjectId, String> {
if let Ok(block_id) = target.parse::<ObjectId>() {
return object_store
.read_typed(block_id, ObjectType::Block)
.map_err(|err| err.to_string())?
.map(|_| block_id)
.ok_or_else(|| format!("--target block {block_id} does not exist"));
}
let target_ref_state_id = ref_store
.read_current_ref_state_id(target)
.map_err(|err| err.to_string())?
.ok_or_else(|| format!("--target ref {target} does not resolve to a published ref"))?;
let target_envelope = object_store
.read_typed(target_ref_state_id, ObjectType::RefState)
.map_err(|err| err.to_string())?
.ok_or_else(|| {
format!("--target ref {target} RefState {target_ref_state_id} is missing")
})?;
let target_payload = RefStatePayload::decode_canonical(
&target_envelope.canonical_payload,
target_envelope.schema_version,
)
.map_err(|err| err.to_string())?;
if target_payload.ref_name != target {
return Err(format!(
"--target RefState name mismatch: expected {target}, got {}",
target_payload.ref_name
));
}
if object_store
.read_typed(target_payload.target_object_id, ObjectType::Block)
.map_err(|err| err.to_string())?
.is_none()
{
return Err(format!(
"--target ref {target} targets missing block {}",
target_payload.target_object_id
));
}
Ok(target_payload.target_object_id)
}
struct CreateArgs {
name: String,
target: String,
message: Option<String>,
}
fn parse_create_args(args: Vec<String>) -> std::result::Result<CreateArgs, String> {
let mut name = None;
let mut target = None;
let mut message = None;
let mut iter = args.into_iter();
while let Some(arg) = iter.next() {
match arg.as_str() {
"--target" => {
let Some(value) = iter.next() else {
return Err("tag create --target requires a value".to_string());
};
target = Some(value);
}
"-m" | "--message" => {
let Some(value) = iter.next() else {
return Err("tag create message option requires a value".to_string());
};
message = Some(value);
}
other if other.starts_with('-') => {
return Err(format!("unknown tag create argument: {other}"));
}
_ => {
if name.is_some() {
return Err("tag create accepts at most one name".to_string());
}
name = Some(arg);
}
}
}
let Some(name) = name else {
return Err("tag create requires <name>".to_string());
};
let Some(target) = target else {
return Err("tag create requires --target <ref|block>".to_string());
};
if let Some(msg) = &message {
if msg.trim().is_empty() {
return Err("tag message must not be empty".to_string());
}
}
Ok(CreateArgs {
name,
target,
message,
})
}