use std::io::Write;
use std::time::{SystemTime, UNIX_EPOCH};
use clap::{Parser, ValueEnum};
use mkit_core::layout::RepoLayout;
use mkit_core::object::{Object, ObjectType, Tag};
use mkit_core::refs;
use mkit_core::serialize;
use mkit_core::store::ObjectStore;
use crate::clap_shim;
use crate::editor::spawn_editor;
use crate::exit;
use crate::format::{self, JsonObject};
const TAG_EDITMSG_TEMPLATE: &str =
"\n# Write a message for tag.\n# Lines starting with '#' are ignored.\n";
#[derive(Debug, Clone, Copy, ValueEnum)]
enum TagFormat {
Default,
Json,
}
#[derive(Debug, Parser)]
#[command(name = "mkit tag", about = "List, create, or delete tags.")]
#[allow(clippy::struct_excessive_bools)] struct TagOpts {
#[arg(short = 'd', long)]
delete: bool,
#[arg(short = 'l', long = "list")]
list: bool,
#[arg(short = 'a', long)]
annotate: bool,
#[arg(short = 's', long)]
sign: bool,
#[arg(short = 'm', long)]
message: Option<String>,
#[arg(long = "author", value_name = "SPEC")]
author_spec: Option<String>,
#[arg(long, value_enum, default_value = "default")]
format: TagFormat,
name: Option<String>,
target: Option<String>,
}
fn emit_err_json(msg: &str, code: u8, json: bool) -> u8 {
if json {
let mut obj = JsonObject::new();
obj.field_bool("ok", false).field_str("error", msg);
let mut stdout = std::io::stdout().lock();
let _ = writeln!(stdout, "{}", obj.finish());
}
emit_err(msg, code)
}
#[must_use]
pub fn run(args: &[String]) -> u8 {
let opts = match clap_shim::parse::<TagOpts>("mkit tag", args) {
Ok(o) => o,
Err(code) => return code,
};
let json = matches!(opts.format, TagFormat::Json);
let cwd = match std::env::current_dir() {
Ok(p) => p,
Err(e) => return emit_err(&format!("cwd: {e}"), exit::NOINPUT),
};
let layout = match super::resolve_layout(&cwd) {
Ok(layout) => layout,
Err(code) => return code,
};
let annotated = opts.annotate || opts.sign;
if opts.list {
if opts.delete {
return super::usage_error("mkit tag: -l and -d are mutually exclusive");
}
return list(&layout, opts.name.as_deref(), json);
}
match (opts.delete, opts.name.as_deref()) {
(true, Some(name)) => {
let was = refs::read_tag(&layout, name).ok().flatten();
match refs::delete_tag(&layout, name) {
Ok(()) => {
let mut stderr = std::io::stderr().lock();
match was {
Some(h) => {
let _ = writeln!(
stderr,
"Deleted tag '{name}' (was {})",
format::short_hash(&h, format::SUMMARY_ABBREV)
);
}
None => {
let _ = writeln!(stderr, "Deleted tag '{name}'");
}
}
drop(stderr);
if json {
let mut obj = JsonObject::new();
obj.field_bool("ok", true)
.field_str("kind", "deleted")
.field_str("name", name)
.field_opt_hash("hash", was.as_ref());
let mut stdout = std::io::stdout().lock();
let _ = writeln!(stdout, "{}", obj.finish());
}
exit::OK
}
Err(e) => emit_err_json(
&format!("delete tag {name}: {e}"),
exit::GENERAL_ERROR,
json,
),
}
}
(true, None) => super::usage_error("usage: mkit tag -d <name>"),
(false, None) => {
if annotated || opts.message.is_some() {
return super::usage_error("usage: mkit tag -a|-s <name> [-m <msg>] [<commit>]");
}
list(&layout, None, json)
}
(false, Some(name)) => {
if annotated {
create_annotated(&layout, &opts, name, json)
} else {
if opts.message.is_some() {
return super::usage_error(
"the -m flag requires -a or -s (annotated/signed tag)",
);
}
create_lightweight(&layout, name, opts.target.as_deref(), json)
}
}
}
}
fn list(layout: &RepoLayout, pattern: Option<&str>, json: bool) -> u8 {
let mut tags = match refs::list_tags(layout) {
Ok(t) => t,
Err(e) => return emit_err_json(&format!("list tags: {e}"), exit::GENERAL_ERROR, json),
};
if let Some(pat) = pattern {
tags.retain(|t| super::branch::glob_match(pat, &t.name));
}
let store = ObjectStore::open(layout).ok();
let mut stdout = std::io::stdout().lock();
for t in tags {
let annotation = t.hash.and_then(|h| {
let store = store.as_ref()?;
match store.read_object(&h) {
Ok(Object::Tag(tag)) => Some(tag.signature != [0u8; 64]),
_ => None,
}
});
if json {
let mut obj = JsonObject::new();
obj.field_str("name", &t.name)
.field_opt_hash("hash", t.hash.as_ref())
.field_bool("annotated", annotation.is_some())
.field_bool("signed", annotation.unwrap_or(false));
let _ = writeln!(stdout, "{}", obj.finish());
continue;
}
let short = t
.hash
.map(|h| format::short_hash(&h, 8))
.unwrap_or_default();
let suffix = match annotation {
Some(true) => "\tsigned",
Some(false) => "\tannotated",
None => "",
};
let _ = writeln!(stdout, "{} {short}{suffix}", t.name);
}
exit::OK
}
fn resolve_target(
store: &ObjectStore,
layout: &RepoLayout,
target_spec: Option<&str>,
) -> Result<mkit_core::hash::Hash, (String, u8)> {
match target_spec {
Some(spec) => super::revspec::resolve_revision(store, layout, spec)
.map_err(|e| (format!("{e}"), exit::DATAERR)),
None => match refs::resolve_head(layout) {
Ok(Some(h)) => Ok(h),
_ => Err(("no HEAD commit to tag".to_string(), exit::GENERAL_ERROR)),
},
}
}
fn create_lightweight(
layout: &RepoLayout,
name: &str,
target_spec: Option<&str>,
json: bool,
) -> u8 {
let emit_err = |msg: &str, code: u8| emit_err_json(msg, code, json);
let store = match ObjectStore::open(layout) {
Ok(s) => s,
Err(e) => return emit_err(&format!("not a mkit repo: {e}"), exit::GENERAL_ERROR),
};
let h = match resolve_target(&store, layout, target_spec) {
Ok(h) => h,
Err((m, c)) => return emit_err(&m, c),
};
let _lock = match super::acquire_worktree_lock(layout) {
Ok(l) => l,
Err(code) => return code,
};
if !store.contains(&h) {
return emit_err(
&format!(
"tag target {} no longer exists (pruned concurrently?); aborting",
format::short_hash(&h, 8)
),
exit::GENERAL_ERROR,
);
}
match refs::update_tag(layout, name, refs::RefWriteCondition::Missing, &h) {
Ok(()) => {
if json {
let mut obj = JsonObject::new();
obj.field_bool("ok", true)
.field_str("kind", "lightweight")
.field_str("name", name)
.field_hash("target", &h);
let mut stdout = std::io::stdout().lock();
let _ = writeln!(stdout, "{}", obj.finish());
}
exit::OK
}
Err(refs::RefError::Conflict(_)) => {
emit_err(&format!("tag '{name}' already exists"), exit::CANTCREAT)
}
Err(e) => emit_err(&format!("write tag {name}: {e}"), exit::CANTCREAT),
}
}
#[allow(clippy::too_many_lines)] fn create_annotated(layout: &RepoLayout, opts: &TagOpts, name: &str, json: bool) -> u8 {
let emit_err = |msg: &str, code: u8| emit_err_json(msg, code, json);
let store = match ObjectStore::open(layout) {
Ok(s) => s,
Err(e) => return emit_err(&format!("not a mkit repo: {e}"), exit::GENERAL_ERROR),
};
let cfg = match crate::config::read_or_default(layout) {
Ok(c) => c,
Err(e) => return emit_err(&format!("config: {e}"), exit::CONFIG_ERROR),
};
let target = match resolve_target(&store, layout, opts.target.as_deref()) {
Ok(h) => h,
Err((m, c)) => return emit_err(&m, c),
};
let target_type = match store.read_object(&target) {
Ok(o) => o.object_type(),
Err(e) => return emit_err(&format!("read target: {e}"), exit::NOINPUT),
};
let msg = match &opts.message {
Some(m) => m.clone(),
None => match spawn_editor(TAG_EDITMSG_TEMPLATE) {
Ok(m) if !m.is_empty() => m,
Ok(_) => return emit_err("empty tag message — aborting", exit::USAGE),
Err(e) => return emit_err(&format!("editor: {e}"), exit::GENERAL_ERROR),
},
};
let mut signer = match super::commit::load_commit_signer(layout, &cfg) {
Ok(s) => s,
Err((m, c)) => return emit_err(&m, c),
};
let signer_public = match signer.public_key() {
Ok(p) => p,
Err((m, c)) => return emit_err(&m, c),
};
let tagger = match super::commit::resolve_author(
opts.author_spec.as_deref(),
&cfg.user_identity,
&signer_public,
) {
Ok(id) => id,
Err(e) => return emit_err(&format!("tagger: {e}"), exit::CONFIG_ERROR),
};
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| d.as_secs());
let mut tag = Tag {
target,
target_type,
name: name.as_bytes().to_vec(),
tagger,
signer: signer_public,
message: msg.as_bytes().to_vec(),
timestamp,
signature: [0u8; 64],
};
if opts.sign {
match signer.sign_tag(&tag) {
Ok(sig) => tag.signature = sig,
Err((m, c)) => return emit_err(&m, c),
}
}
let _lock = match super::acquire_worktree_lock(layout) {
Ok(l) => l,
Err(code) => return code,
};
if !store.contains(&target) {
return emit_err(
&format!(
"tag target {} no longer exists (pruned concurrently?); aborting",
format::short_hash(&target, 8)
),
exit::GENERAL_ERROR,
);
}
let bytes = match serialize::serialize(&Object::Tag(tag)) {
Ok(b) => b,
Err(e) => return emit_err(&format!("serialize tag: {e}"), exit::DATAERR),
};
let tag_hash = match store.write(&bytes) {
Ok(h) => h,
Err(e) => return emit_err(&format!("store tag: {e}"), exit::CANTCREAT),
};
match refs::update_tag(layout, name, refs::RefWriteCondition::Missing, &tag_hash) {
Ok(()) => {}
Err(refs::RefError::Conflict(_)) => {
return emit_err(&format!("tag '{name}' already exists"), exit::CANTCREAT);
}
Err(e) => return emit_err(&format!("write tag {name}: {e}"), exit::CANTCREAT),
}
let mut stderr = std::io::stderr().lock();
let kind = if opts.sign { "signed" } else { "annotated" };
let _ = writeln!(
stderr,
"created {kind} tag {name} -> {} ({})",
format::short_hash(&target, 8),
ObjectType::name(target_type),
);
drop(stderr);
if json {
let mut obj = JsonObject::new();
obj.field_bool("ok", true)
.field_str("kind", kind)
.field_str("name", name)
.field_hash("hash", &tag_hash)
.field_hash("target", &target)
.field_str("target_type", ObjectType::name(target_type));
let mut stdout = std::io::stdout().lock();
let _ = writeln!(stdout, "{}", obj.finish());
}
exit::OK
}
use super::error as emit_err;