1use std::io::Write;
17use std::time::{SystemTime, UNIX_EPOCH};
18
19use clap::{Parser, ValueEnum};
20use mkit_core::layout::RepoLayout;
21use mkit_core::object::{Object, ObjectType, Tag};
22use mkit_core::refs;
23use mkit_core::serialize;
24use mkit_core::store::ObjectStore;
25
26use crate::clap_shim;
27use crate::editor::spawn_editor;
28use crate::exit;
29use crate::format::{self, JsonObject};
30
31const TAG_EDITMSG_TEMPLATE: &str =
32 "\n# Write a message for tag.\n# Lines starting with '#' are ignored.\n";
33
34#[derive(Debug, Clone, Copy, ValueEnum)]
35enum TagFormat {
36 Default,
37 Json,
38}
39
40#[derive(Debug, Parser)]
41#[command(name = "mkit tag", about = "List, create, or delete tags.")]
42#[allow(clippy::struct_excessive_bools)] struct TagOpts {
44 #[arg(short = 'd', long)]
46 delete: bool,
47 #[arg(short = 'l', long = "list")]
50 list: bool,
51 #[arg(short = 'a', long)]
53 annotate: bool,
54 #[arg(short = 's', long)]
56 sign: bool,
57 #[arg(short = 'm', long)]
59 message: Option<String>,
60 #[arg(long = "author", value_name = "SPEC")]
62 author_spec: Option<String>,
63 #[arg(long, value_enum, default_value = "default")]
66 format: TagFormat,
67 name: Option<String>,
69 target: Option<String>,
71}
72
73fn emit_err_json(msg: &str, code: u8, json: bool) -> u8 {
76 if json {
77 let mut obj = JsonObject::new();
78 obj.field_bool("ok", false).field_str("error", msg);
79 let mut stdout = std::io::stdout().lock();
80 let _ = writeln!(stdout, "{}", obj.finish());
81 }
82 emit_err(msg, code)
83}
84
85#[must_use]
86pub fn run(args: &[String]) -> u8 {
87 let opts = match clap_shim::parse::<TagOpts>("mkit tag", args) {
88 Ok(o) => o,
89 Err(code) => return code,
90 };
91 let json = matches!(opts.format, TagFormat::Json);
92 let cwd = match std::env::current_dir() {
93 Ok(p) => p,
94 Err(e) => return emit_err(&format!("cwd: {e}"), exit::NOINPUT),
95 };
96 let layout = match super::resolve_layout(&cwd) {
97 Ok(layout) => layout,
98 Err(code) => return code,
99 };
100
101 let annotated = opts.annotate || opts.sign;
103
104 if opts.list {
108 if opts.delete {
109 return super::usage_error("mkit tag: -l and -d are mutually exclusive");
110 }
111 return list(&layout, opts.name.as_deref(), json);
112 }
113
114 match (opts.delete, opts.name.as_deref()) {
115 (true, Some(name)) => {
116 let was = refs::read_tag(&layout, name).ok().flatten();
117 match refs::delete_tag(&layout, name) {
118 Ok(()) => {
119 let mut stderr = std::io::stderr().lock();
120 match was {
121 Some(h) => {
122 let _ = writeln!(
123 stderr,
124 "Deleted tag '{name}' (was {})",
125 format::short_hash(&h, format::SUMMARY_ABBREV)
126 );
127 }
128 None => {
129 let _ = writeln!(stderr, "Deleted tag '{name}'");
130 }
131 }
132 drop(stderr);
133 if json {
134 let mut obj = JsonObject::new();
135 obj.field_bool("ok", true)
136 .field_str("kind", "deleted")
137 .field_str("name", name)
138 .field_opt_hash("hash", was.as_ref());
139 let mut stdout = std::io::stdout().lock();
140 let _ = writeln!(stdout, "{}", obj.finish());
141 }
142 exit::OK
143 }
144 Err(e) => emit_err_json(
145 &format!("delete tag {name}: {e}"),
146 exit::GENERAL_ERROR,
147 json,
148 ),
149 }
150 }
151 (true, None) => super::usage_error("usage: mkit tag -d <name>"),
152 (false, None) => {
153 if annotated || opts.message.is_some() {
154 return super::usage_error("usage: mkit tag -a|-s <name> [-m <msg>] [<commit>]");
155 }
156 list(&layout, None, json)
157 }
158 (false, Some(name)) => {
159 if annotated {
160 create_annotated(&layout, &opts, name, json)
161 } else {
162 if opts.message.is_some() {
163 return super::usage_error(
164 "the -m flag requires -a or -s (annotated/signed tag)",
165 );
166 }
167 create_lightweight(&layout, name, opts.target.as_deref(), json)
168 }
169 }
170 }
171}
172
173fn list(layout: &RepoLayout, pattern: Option<&str>, json: bool) -> u8 {
174 let mut tags = match refs::list_tags(layout) {
175 Ok(t) => t,
176 Err(e) => return emit_err_json(&format!("list tags: {e}"), exit::GENERAL_ERROR, json),
177 };
178 if let Some(pat) = pattern {
179 tags.retain(|t| super::branch::glob_match(pat, &t.name));
180 }
181 let store = ObjectStore::open(layout).ok();
184 let mut stdout = std::io::stdout().lock();
185 for t in tags {
186 let annotation = t.hash.and_then(|h| {
187 let store = store.as_ref()?;
188 match store.read_object(&h) {
189 Ok(Object::Tag(tag)) => Some(tag.signature != [0u8; 64]),
190 _ => None,
191 }
192 });
193 if json {
194 let mut obj = JsonObject::new();
195 obj.field_str("name", &t.name)
196 .field_opt_hash("hash", t.hash.as_ref())
197 .field_bool("annotated", annotation.is_some())
198 .field_bool("signed", annotation.unwrap_or(false));
199 let _ = writeln!(stdout, "{}", obj.finish());
200 continue;
201 }
202 let short = t
203 .hash
204 .map(|h| format::short_hash(&h, 8))
205 .unwrap_or_default();
206 let suffix = match annotation {
207 Some(true) => "\tsigned",
208 Some(false) => "\tannotated",
209 None => "",
210 };
211 let _ = writeln!(stdout, "{} {short}{suffix}", t.name);
212 }
213 exit::OK
214}
215
216fn resolve_target(
218 store: &ObjectStore,
219 layout: &RepoLayout,
220 target_spec: Option<&str>,
221) -> Result<mkit_core::hash::Hash, (String, u8)> {
222 match target_spec {
223 Some(spec) => super::revspec::resolve_revision(store, layout, spec)
224 .map_err(|e| (format!("{e}"), exit::DATAERR)),
225 None => match refs::resolve_head(layout) {
226 Ok(Some(h)) => Ok(h),
227 _ => Err(("no HEAD commit to tag".to_string(), exit::GENERAL_ERROR)),
228 },
229 }
230}
231
232fn create_lightweight(
233 layout: &RepoLayout,
234 name: &str,
235 target_spec: Option<&str>,
236 json: bool,
237) -> u8 {
238 let emit_err = |msg: &str, code: u8| emit_err_json(msg, code, json);
239 let store = match ObjectStore::open(layout) {
240 Ok(s) => s,
241 Err(e) => return emit_err(&format!("not a mkit repo: {e}"), exit::GENERAL_ERROR),
242 };
243 let h = match resolve_target(&store, layout, target_spec) {
244 Ok(h) => h,
245 Err((m, c)) => return emit_err(&m, c),
246 };
247 let _lock = match super::acquire_worktree_lock(layout) {
253 Ok(l) => l,
254 Err(code) => return code,
255 };
256 if !store.contains(&h) {
257 return emit_err(
258 &format!(
259 "tag target {} no longer exists (pruned concurrently?); aborting",
260 format::short_hash(&h, 8)
261 ),
262 exit::GENERAL_ERROR,
263 );
264 }
265 match refs::update_tag(layout, name, refs::RefWriteCondition::Missing, &h) {
268 Ok(()) => {
269 if json {
270 let mut obj = JsonObject::new();
271 obj.field_bool("ok", true)
272 .field_str("kind", "lightweight")
273 .field_str("name", name)
274 .field_hash("target", &h);
275 let mut stdout = std::io::stdout().lock();
276 let _ = writeln!(stdout, "{}", obj.finish());
277 }
278 exit::OK
279 }
280 Err(refs::RefError::Conflict(_)) => {
281 emit_err(&format!("tag '{name}' already exists"), exit::CANTCREAT)
282 }
283 Err(e) => emit_err(&format!("write tag {name}: {e}"), exit::CANTCREAT),
284 }
285}
286
287#[allow(clippy::too_many_lines)] fn create_annotated(layout: &RepoLayout, opts: &TagOpts, name: &str, json: bool) -> u8 {
289 let emit_err = |msg: &str, code: u8| emit_err_json(msg, code, json);
290 let store = match ObjectStore::open(layout) {
291 Ok(s) => s,
292 Err(e) => return emit_err(&format!("not a mkit repo: {e}"), exit::GENERAL_ERROR),
293 };
294 let cfg = match crate::config::read_or_default(layout) {
295 Ok(c) => c,
296 Err(e) => return emit_err(&format!("config: {e}"), exit::CONFIG_ERROR),
297 };
298
299 let target = match resolve_target(&store, layout, opts.target.as_deref()) {
301 Ok(h) => h,
302 Err((m, c)) => return emit_err(&m, c),
303 };
304 let target_type = match store.read_object(&target) {
305 Ok(o) => o.object_type(),
306 Err(e) => return emit_err(&format!("read target: {e}"), exit::NOINPUT),
307 };
308
309 let msg = match &opts.message {
311 Some(m) => m.clone(),
312 None => match spawn_editor(TAG_EDITMSG_TEMPLATE) {
313 Ok(m) if !m.is_empty() => m,
314 Ok(_) => return emit_err("empty tag message โ aborting", exit::USAGE),
315 Err(e) => return emit_err(&format!("editor: {e}"), exit::GENERAL_ERROR),
316 },
317 };
318
319 let mut signer = match super::commit::load_commit_signer(layout, &cfg) {
321 Ok(s) => s,
322 Err((m, c)) => return emit_err(&m, c),
323 };
324 let signer_public = match signer.public_key() {
325 Ok(p) => p,
326 Err((m, c)) => return emit_err(&m, c),
327 };
328 let tagger = match super::commit::resolve_author(
329 opts.author_spec.as_deref(),
330 &cfg.user_identity,
331 &signer_public,
332 ) {
333 Ok(id) => id,
334 Err(e) => return emit_err(&format!("tagger: {e}"), exit::CONFIG_ERROR),
335 };
336
337 let timestamp = SystemTime::now()
338 .duration_since(UNIX_EPOCH)
339 .map_or(0, |d| d.as_secs());
340
341 let mut tag = Tag {
342 target,
343 target_type,
344 name: name.as_bytes().to_vec(),
345 tagger,
346 signer: signer_public,
347 message: msg.as_bytes().to_vec(),
348 timestamp,
349 signature: [0u8; 64],
350 };
351
352 if opts.sign {
353 match signer.sign_tag(&tag) {
354 Ok(sig) => tag.signature = sig,
355 Err((m, c)) => return emit_err(&m, c),
356 }
357 }
358 let _lock = match super::acquire_worktree_lock(layout) {
366 Ok(l) => l,
367 Err(code) => return code,
368 };
369 if !store.contains(&target) {
373 return emit_err(
374 &format!(
375 "tag target {} no longer exists (pruned concurrently?); aborting",
376 format::short_hash(&target, 8)
377 ),
378 exit::GENERAL_ERROR,
379 );
380 }
381
382 let bytes = match serialize::serialize(&Object::Tag(tag)) {
383 Ok(b) => b,
384 Err(e) => return emit_err(&format!("serialize tag: {e}"), exit::DATAERR),
385 };
386 let tag_hash = match store.write(&bytes) {
387 Ok(h) => h,
388 Err(e) => return emit_err(&format!("store tag: {e}"), exit::CANTCREAT),
389 };
390 match refs::update_tag(layout, name, refs::RefWriteCondition::Missing, &tag_hash) {
391 Ok(()) => {}
392 Err(refs::RefError::Conflict(_)) => {
393 return emit_err(&format!("tag '{name}' already exists"), exit::CANTCREAT);
394 }
395 Err(e) => return emit_err(&format!("write tag {name}: {e}"), exit::CANTCREAT),
396 }
397 let mut stderr = std::io::stderr().lock();
398 let kind = if opts.sign { "signed" } else { "annotated" };
399 let _ = writeln!(
400 stderr,
401 "created {kind} tag {name} -> {} ({})",
402 format::short_hash(&target, 8),
403 ObjectType::name(target_type),
404 );
405 drop(stderr);
406 if json {
407 let mut obj = JsonObject::new();
408 obj.field_bool("ok", true)
409 .field_str("kind", kind)
410 .field_str("name", name)
411 .field_hash("hash", &tag_hash)
412 .field_hash("target", &target)
413 .field_str("target_type", ObjectType::name(target_type));
414 let mut stdout = std::io::stdout().lock();
415 let _ = writeln!(stdout, "{}", obj.finish());
416 }
417 exit::OK
418}
419
420use super::error as emit_err;