Skip to main content

lit/commands/
tag.rs

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    // List tags: `lit tag` with no args, or `lit tag --list`
19    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    // Create tag
34    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            // Try as branch ref first, then as raw hash
50            read_ref(repo_root, &format!("heads/{}", rev))
51                .or_else(|_| read_ref(repo_root, &format!("tags/{}", rev)))
52                .or_else(|_| {
53                    // Verify it looks like a hash
54                    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    // Check tag doesn't already exist
71    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    // Check tag doesn't already exist
94    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}