1use crate::core::{find_repo_root, read_head, read_ref, write_ref, Object, ObjectHash, Tag};
2use crate::response::TagResponse;
3use crate::storage::ObjectStore;
4
5#[allow(clippy::too_many_arguments)]
6pub fn execute(
7 name: Option<String>,
8 message: Option<String>,
9 annotate: bool,
10 delete: bool,
11 sign: bool,
12 verify: bool,
13 list: bool,
14 commit: Option<String>,
15) -> Result<TagResponse, crate::errors::LitError> {
16 let repo_root = find_repo_root()?;
17
18 if list || (name.is_none() && !delete && !verify) {
20 return list_tags(&repo_root);
21 }
22
23 let tag_name = name.ok_or("Tag name is required")?;
24
25 if delete {
26 return delete_tag(&repo_root, &tag_name);
27 }
28
29 if verify {
30 return verify_tag(&repo_root, &tag_name);
31 }
32
33 let target_hash = resolve_target(&repo_root, commit)?;
35
36 if annotate || sign || message.is_some() {
37 create_annotated_tag(&repo_root, &tag_name, &target_hash, message, sign)
38 } else {
39 create_lightweight_tag(&repo_root, &tag_name, &target_hash)
40 }
41}
42
43fn resolve_target(
44 repo_root: &std::path::Path,
45 commit: Option<String>,
46) -> Result<String, crate::errors::LitError> {
47 match commit {
48 Some(rev) => {
49 read_ref(repo_root, &format!("heads/{}", rev))
51 .or_else(|_| read_ref(repo_root, &format!("tags/{}", rev)))
52 .or_else(|_| {
53 if rev.len() >= 16 && rev.chars().all(|c| c.is_ascii_hexdigit()) {
55 Ok(rev)
56 } else {
57 Err(format!("Cannot resolve '{}' to a commit", rev).into())
58 }
59 })
60 }
61 None => Ok(read_head(repo_root)?),
62 }
63}
64
65fn create_lightweight_tag(
66 repo_root: &std::path::Path,
67 tag_name: &str,
68 target_hash: &str,
69) -> Result<TagResponse, crate::errors::LitError> {
70 if crate::core::refs::read_ref(repo_root, &format!("tags/{}", tag_name)).is_ok() {
72 return Err(format!("tag '{}' already exists", tag_name).into());
73 }
74
75 write_ref(repo_root, &format!("tags/{}", tag_name), target_hash)?;
76
77 Ok(TagResponse::Create {
78 name: tag_name.to_string(),
79 hash: target_hash.to_string(),
80 annotated: false,
81 signed: false,
82 message: format!("Created lightweight tag '{}'", tag_name),
83 })
84}
85
86fn create_annotated_tag(
87 repo_root: &std::path::Path,
88 tag_name: &str,
89 target_hash: &str,
90 message: Option<String>,
91 sign: bool,
92) -> Result<TagResponse, crate::errors::LitError> {
93 if crate::core::refs::read_ref(repo_root, &format!("tags/{}", tag_name)).is_ok() {
95 return Err(format!("tag '{}' already exists", tag_name).into());
96 }
97
98 let tagger = std::env::var("USER")
99 .or_else(|_| std::env::var("USERNAME"))
100 .unwrap_or_else(|_| "Unknown".to_string());
101
102 let msg = message.unwrap_or_default();
103 let store = ObjectStore::new(repo_root);
104
105 let mut tag = Tag::new(
106 ObjectHash::from_hex(target_hash.to_string()),
107 "commit".to_string(),
108 tag_name.to_string(),
109 tagger,
110 msg,
111 );
112
113 if sign {
114 let keypair = crate::crypto::signatures::PQKeyPair::generate();
115 tag.sign(&keypair);
116 }
117
118 let tag_obj = Object::Tag(tag);
119 let tag_hash = store.write(&tag_obj)?;
120
121 write_ref(repo_root, &format!("tags/{}", tag_name), tag_hash.as_str())?;
122
123 Ok(TagResponse::Create {
124 name: tag_name.to_string(),
125 hash: tag_hash.as_str().to_string(),
126 annotated: true,
127 signed: sign,
128 message: if sign {
129 format!("Created signed tag '{}' (PQ: ML-DSA-87)", tag_name)
130 } else {
131 format!("Created annotated tag '{}'", tag_name)
132 },
133 })
134}
135
136fn list_tags(repo_root: &std::path::Path) -> Result<TagResponse, crate::errors::LitError> {
137 let refs = crate::core::refs::list_refs(repo_root, "tags")?;
138 let tags: Vec<String> = refs.into_iter().map(|r| r.name).collect();
139 Ok(TagResponse::List { tags })
140}
141
142fn delete_tag(
143 repo_root: &std::path::Path,
144 tag_name: &str,
145) -> Result<TagResponse, crate::errors::LitError> {
146 crate::core::refs::delete_ref(repo_root, &format!("tags/{}", tag_name))?;
147 Ok(TagResponse::Delete {
148 name: tag_name.to_string(),
149 message: format!("Deleted tag '{}'", tag_name),
150 })
151}
152
153fn verify_tag(
154 repo_root: &std::path::Path,
155 tag_name: &str,
156) -> Result<TagResponse, crate::errors::LitError> {
157 let hash_str = crate::core::refs::read_ref(repo_root, &format!("tags/{}", tag_name))?;
158 let hash = ObjectHash::from_hex(hash_str);
159 let store = ObjectStore::new(repo_root);
160 let obj = store.read(&hash)?;
161
162 match obj {
163 Object::Tag(tag) => {
164 let result = tag.verify_signature();
165 match result {
166 Ok(()) => Ok(TagResponse::Verify {
167 name: tag_name.to_string(),
168 valid: true,
169 algorithm: tag
170 .pq_signature
171 .as_ref()
172 .map(|s| s.algorithm.clone())
173 .unwrap_or_default(),
174 message: format!("Good signature on tag '{}' (PQ)", tag_name),
175 }),
176 Err(e) => Ok(TagResponse::Verify {
177 name: tag_name.to_string(),
178 valid: false,
179 algorithm: String::new(),
180 message: format!("Bad signature on tag '{}': {}", tag_name, e),
181 }),
182 }
183 }
184 _ => Err(format!("Tag '{}' is a lightweight tag (not signed)", tag_name).into()),
185 }
186}