1use std::io::Write;
17use std::time::{SystemTime, UNIX_EPOCH};
18
19use clap::Parser;
20use mkit_core::object::{Object, ObjectType, Tag};
21use mkit_core::refs;
22use mkit_core::serialize;
23use mkit_core::store::ObjectStore;
24
25use crate::clap_shim;
26use crate::editor::spawn_editor;
27use crate::exit;
28use crate::format;
29
30const TAG_EDITMSG_TEMPLATE: &str =
31 "\n# Write a message for tag.\n# Lines starting with '#' are ignored.\n";
32
33#[derive(Debug, Parser)]
34#[command(name = "mkit tag", about = "List, create, or delete tags.")]
35struct TagOpts {
36 #[arg(short = 'd', long)]
38 delete: bool,
39 #[arg(short = 'a', long)]
41 annotate: bool,
42 #[arg(short = 's', long)]
44 sign: bool,
45 #[arg(short = 'm', long)]
47 message: Option<String>,
48 #[arg(long = "author", value_name = "SPEC")]
50 author_spec: Option<String>,
51 name: Option<String>,
53 target: Option<String>,
55}
56
57#[must_use]
58pub fn run(args: &[String]) -> u8 {
59 let opts = match clap_shim::parse::<TagOpts>("mkit tag", args) {
60 Ok(o) => o,
61 Err(code) => return code,
62 };
63 let cwd = match std::env::current_dir() {
64 Ok(p) => p,
65 Err(e) => return emit_err(&format!("cwd: {e}"), exit::NOINPUT),
66 };
67 let mkit_dir = cwd.join(mkit_core::MKIT_DIR);
68
69 let annotated = opts.annotate || opts.sign;
71
72 match (opts.delete, opts.name.as_deref()) {
73 (true, Some(name)) => match refs::delete_tag(&mkit_dir, name) {
74 Ok(()) => exit::OK,
75 Err(e) => emit_err(&format!("delete tag {name}: {e}"), exit::GENERAL_ERROR),
76 },
77 (true, None) => super::usage_error("usage: mkit tag -d <name>"),
78 (false, None) => {
79 if annotated || opts.message.is_some() {
80 return super::usage_error("usage: mkit tag -a|-s <name> [-m <msg>] [<commit>]");
81 }
82 list(&mkit_dir)
83 }
84 (false, Some(name)) => {
85 if annotated {
86 create_annotated(&cwd, &mkit_dir, &opts, name)
87 } else {
88 if opts.message.is_some() {
89 return super::usage_error(
90 "the -m flag requires -a or -s (annotated/signed tag)",
91 );
92 }
93 create_lightweight(&cwd, &mkit_dir, name, opts.target.as_deref())
94 }
95 }
96 }
97}
98
99fn list(mkit_dir: &std::path::Path) -> u8 {
100 let tags = match refs::list_tags(mkit_dir) {
101 Ok(t) => t,
102 Err(e) => return emit_err(&format!("list tags: {e}"), exit::GENERAL_ERROR),
103 };
104 let store = mkit_dir
107 .parent()
108 .and_then(|root| ObjectStore::open(root).ok());
109 let mut stdout = std::io::stdout().lock();
110 for t in tags {
111 let short = t
112 .hash
113 .map(|h| format::short_hash(&h, 8))
114 .unwrap_or_default();
115 let annotation = t.hash.and_then(|h| {
118 let store = store.as_ref()?;
119 match store.read_object(&h) {
120 Ok(Object::Tag(tag)) => Some(if tag.signature == [0u8; 64] {
121 "\tannotated".to_string()
122 } else {
123 "\tsigned".to_string()
124 }),
125 _ => None,
126 }
127 });
128 let _ = writeln!(
129 stdout,
130 "{} {short}{}",
131 t.name,
132 annotation.unwrap_or_default()
133 );
134 }
135 exit::OK
136}
137
138fn resolve_target(
140 store: &ObjectStore,
141 mkit_dir: &std::path::Path,
142 target_spec: Option<&str>,
143) -> Result<mkit_core::hash::Hash, (String, u8)> {
144 match target_spec {
145 Some(spec) => super::revspec::resolve_revision(store, mkit_dir, spec)
146 .map_err(|e| (format!("{e}"), exit::DATAERR)),
147 None => match refs::resolve_head(mkit_dir) {
148 Ok(Some(h)) => Ok(h),
149 _ => Err(("no HEAD commit to tag".to_string(), exit::GENERAL_ERROR)),
150 },
151 }
152}
153
154fn create_lightweight(
155 cwd: &std::path::Path,
156 mkit_dir: &std::path::Path,
157 name: &str,
158 target_spec: Option<&str>,
159) -> u8 {
160 let store = match ObjectStore::open(cwd) {
161 Ok(s) => s,
162 Err(e) => return emit_err(&format!("not a mkit repo: {e}"), exit::GENERAL_ERROR),
163 };
164 let h = match resolve_target(&store, mkit_dir, target_spec) {
165 Ok(h) => h,
166 Err((m, c)) => return emit_err(&m, c),
167 };
168 let _lock = match super::acquire_worktree_lock(cwd) {
174 Ok(l) => l,
175 Err(code) => return code,
176 };
177 if !store.contains(&h) {
178 return emit_err(
179 &format!(
180 "tag target {} no longer exists (pruned concurrently?); aborting",
181 format::short_hash(&h, 8)
182 ),
183 exit::GENERAL_ERROR,
184 );
185 }
186 match refs::update_tag(mkit_dir, name, refs::RefWriteCondition::Missing, &h) {
189 Ok(()) => exit::OK,
190 Err(refs::RefError::Conflict(_)) => {
191 emit_err(&format!("tag '{name}' already exists"), exit::CANTCREAT)
192 }
193 Err(e) => emit_err(&format!("write tag {name}: {e}"), exit::CANTCREAT),
194 }
195}
196
197fn create_annotated(
198 cwd: &std::path::Path,
199 mkit_dir: &std::path::Path,
200 opts: &TagOpts,
201 name: &str,
202) -> u8 {
203 let store = match ObjectStore::open(cwd) {
204 Ok(s) => s,
205 Err(e) => return emit_err(&format!("not a mkit repo: {e}"), exit::GENERAL_ERROR),
206 };
207 let cfg = match crate::config::read_or_default(cwd) {
208 Ok(c) => c,
209 Err(e) => return emit_err(&format!("config: {e}"), exit::CONFIG_ERROR),
210 };
211
212 let target = match resolve_target(&store, mkit_dir, opts.target.as_deref()) {
214 Ok(h) => h,
215 Err((m, c)) => return emit_err(&m, c),
216 };
217 let target_type = match store.read_object(&target) {
218 Ok(o) => o.object_type(),
219 Err(e) => return emit_err(&format!("read target: {e}"), exit::NOINPUT),
220 };
221
222 let msg = match &opts.message {
224 Some(m) => m.clone(),
225 None => match spawn_editor(TAG_EDITMSG_TEMPLATE) {
226 Ok(m) if !m.is_empty() => m,
227 Ok(_) => return emit_err("empty tag message โ aborting", exit::USAGE),
228 Err(e) => return emit_err(&format!("editor: {e}"), exit::GENERAL_ERROR),
229 },
230 };
231
232 let mut signer = match super::commit::load_commit_signer(cwd, &cfg) {
234 Ok(s) => s,
235 Err((m, c)) => return emit_err(&m, c),
236 };
237 let signer_public = match signer.public_key() {
238 Ok(p) => p,
239 Err((m, c)) => return emit_err(&m, c),
240 };
241 let tagger = match super::commit::resolve_author(
242 opts.author_spec.as_deref(),
243 &cfg.user_identity,
244 &signer_public,
245 ) {
246 Ok(id) => id,
247 Err(e) => return emit_err(&format!("tagger: {e}"), exit::CONFIG_ERROR),
248 };
249
250 let timestamp = SystemTime::now()
251 .duration_since(UNIX_EPOCH)
252 .map_or(0, |d| d.as_secs());
253
254 let mut tag = Tag {
255 target,
256 target_type,
257 name: name.as_bytes().to_vec(),
258 tagger,
259 signer: signer_public,
260 message: msg.as_bytes().to_vec(),
261 timestamp,
262 signature: [0u8; 64],
263 };
264
265 if opts.sign {
266 match signer.sign_tag(&tag) {
267 Ok(sig) => tag.signature = sig,
268 Err((m, c)) => return emit_err(&m, c),
269 }
270 }
271 let _lock = match super::acquire_worktree_lock(cwd) {
279 Ok(l) => l,
280 Err(code) => return code,
281 };
282 if !store.contains(&target) {
286 return emit_err(
287 &format!(
288 "tag target {} no longer exists (pruned concurrently?); aborting",
289 format::short_hash(&target, 8)
290 ),
291 exit::GENERAL_ERROR,
292 );
293 }
294
295 let bytes = match serialize::serialize(&Object::Tag(tag)) {
296 Ok(b) => b,
297 Err(e) => return emit_err(&format!("serialize tag: {e}"), exit::DATAERR),
298 };
299 let tag_hash = match store.write(&bytes) {
300 Ok(h) => h,
301 Err(e) => return emit_err(&format!("store tag: {e}"), exit::CANTCREAT),
302 };
303 match refs::update_tag(mkit_dir, name, refs::RefWriteCondition::Missing, &tag_hash) {
304 Ok(()) => {}
305 Err(refs::RefError::Conflict(_)) => {
306 return emit_err(&format!("tag '{name}' already exists"), exit::CANTCREAT);
307 }
308 Err(e) => return emit_err(&format!("write tag {name}: {e}"), exit::CANTCREAT),
309 }
310 let mut stderr = std::io::stderr().lock();
311 let kind = if opts.sign { "signed" } else { "annotated" };
312 let _ = writeln!(
313 stderr,
314 "created {kind} tag {name} -> {} ({})",
315 format::short_hash(&target, 8),
316 ObjectType::name(target_type),
317 );
318 exit::OK
319}
320
321fn emit_err(msg: &str, code: u8) -> u8 {
322 let mut stderr = std::io::stderr().lock();
323 let _ = writeln!(stderr, "error: {msg}");
324 code
325}