Skip to main content

governor_git/
tag.rs

1//! Git tag wrapper
2
3use governor_core::domain::version::SemanticVersion;
4use serde::{Deserialize, Serialize};
5
6/// Wrapper for Git tags
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct GitTag {
9    /// Tag name
10    pub name: String,
11    /// Target commit hash
12    pub target: String,
13    /// Tag message
14    pub message: Option<String>,
15    /// Tagger
16    pub tagger: Option<TaggerInfo>,
17    /// Parsed version (if applicable)
18    pub version: Option<SemanticVersion>,
19}
20
21impl GitTag {
22    /// Create a new Git tag
23    #[must_use]
24    pub fn new(name: String, target: String) -> Self {
25        let version = Self::parse_version(&name);
26
27        Self {
28            name,
29            target,
30            message: None,
31            tagger: None,
32            version,
33        }
34    }
35
36    /// Parse version from tag name
37    fn parse_version(name: &str) -> Option<SemanticVersion> {
38        let version_str = name.trim_start_matches('v');
39        SemanticVersion::parse(version_str).ok()
40    }
41
42    /// Set tag message
43    #[must_use]
44    pub fn with_message(mut self, message: String) -> Self {
45        self.message = Some(message);
46        self
47    }
48
49    /// Set tagger info
50    #[must_use]
51    pub fn with_tagger(mut self, tagger: TaggerInfo) -> Self {
52        self.tagger = Some(tagger);
53        self
54    }
55
56    /// Check if this is a version tag
57    #[must_use]
58    pub const fn is_version_tag(&self) -> bool {
59        self.version.is_some()
60    }
61
62    /// Get the version
63    #[must_use]
64    pub const fn version(&self) -> Option<&SemanticVersion> {
65        self.version.as_ref()
66    }
67}
68
69/// Tagger information
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct TaggerInfo {
72    /// Name
73    pub name: String,
74    /// Email
75    pub email: String,
76    /// Timestamp
77    pub timestamp: i64,
78}
79
80impl TaggerInfo {
81    /// Create new tagger info
82    #[must_use]
83    pub const fn new(name: String, email: String, timestamp: i64) -> Self {
84        Self {
85            name,
86            email,
87            timestamp,
88        }
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn test_new() {
98        let tag = GitTag::new("v1.0.0".to_string(), "abc123".to_string());
99        assert_eq!(tag.name, "v1.0.0");
100        assert_eq!(tag.target, "abc123");
101        assert!(tag.message.is_none());
102        assert!(tag.tagger.is_none());
103    }
104
105    #[test]
106    fn test_parse_version_with_v() {
107        let tag = GitTag::new("v1.2.3".to_string(), "abc123".to_string());
108        assert!(tag.version.is_some());
109        assert_eq!(tag.version.as_ref().unwrap().to_string(), "1.2.3");
110    }
111
112    #[test]
113    fn test_parse_version_without_v() {
114        let tag = GitTag::new("2.0.0".to_string(), "abc123".to_string());
115        assert!(tag.version.is_some());
116        assert_eq!(tag.version.as_ref().unwrap().to_string(), "2.0.0");
117    }
118
119    #[test]
120    fn test_parse_version_invalid() {
121        let tag = GitTag::new("latest".to_string(), "abc123".to_string());
122        assert!(tag.version.is_none());
123    }
124
125    #[test]
126    fn test_parse_version_multiple_v() {
127        let tag = GitTag::new("vv1.0.0".to_string(), "abc123".to_string());
128        // trim_start_matches removes all 'v' from start
129        assert!(tag.version.is_some());
130        assert_eq!(tag.version.as_ref().unwrap().to_string(), "1.0.0");
131    }
132
133    #[test]
134    fn test_with_message() {
135        let tag = GitTag::new("v1.0.0".to_string(), "abc123".to_string())
136            .with_message("Release".to_string());
137        assert_eq!(tag.message, Some("Release".to_string()));
138    }
139
140    #[test]
141    fn test_with_tagger() {
142        let tagger = TaggerInfo::new("John".to_string(), "john@test.com".to_string(), 12345);
143        let tag = GitTag::new("v1.0.0".to_string(), "abc123".to_string()).with_tagger(tagger);
144        assert!(tag.tagger.is_some());
145        assert_eq!(tag.tagger.as_ref().unwrap().name, "John");
146    }
147
148    #[test]
149    fn test_is_version_tag_true() {
150        let tag = GitTag::new("v1.0.0".to_string(), "abc123".to_string());
151        assert!(tag.is_version_tag());
152    }
153
154    #[test]
155    fn test_is_version_tag_false() {
156        let tag = GitTag::new("stable".to_string(), "abc123".to_string());
157        assert!(!tag.is_version_tag());
158    }
159
160    #[test]
161    fn test_version_some() {
162        let tag = GitTag::new("v3.2.1".to_string(), "abc123".to_string());
163        assert!(tag.version().is_some());
164        assert_eq!(tag.version().unwrap().to_string(), "3.2.1");
165    }
166
167    #[test]
168    fn test_version_none() {
169        let tag = GitTag::new("latest".to_string(), "abc123".to_string());
170        assert!(tag.version().is_none());
171    }
172
173    #[test]
174    fn test_tagger_info_new() {
175        let tagger = TaggerInfo::new(
176            "Alice".to_string(),
177            "alice@test.com".to_string(),
178            9_876_543_210,
179        );
180        assert_eq!(tagger.name, "Alice");
181        assert_eq!(tagger.email, "alice@test.com");
182        assert_eq!(tagger.timestamp, 9_876_543_210);
183    }
184
185    #[test]
186    fn test_chained_builders() {
187        let tagger = TaggerInfo::new("Bob".to_string(), "bob@test.com".to_string(), 1_111_111_111);
188        let tag = GitTag::new("v2.0.0".to_string(), "def456".to_string())
189            .with_message("Version 2.0.0".to_string())
190            .with_tagger(tagger);
191
192        assert_eq!(tag.name, "v2.0.0");
193        assert_eq!(tag.message, Some("Version 2.0.0".to_string()));
194        assert!(tag.tagger.is_some());
195        assert_eq!(tag.tagger.as_ref().unwrap().name, "Bob");
196    }
197
198    #[test]
199    fn test_serialize() {
200        let tag = GitTag::new("v1.0.0".to_string(), "abc123".to_string());
201        let json = serde_json::to_string(&tag);
202        assert!(json.is_ok());
203    }
204
205    #[test]
206    fn test_deserialize() {
207        let json =
208            r#"{"name":"v1.0.0","target":"abc123","message":null,"tagger":null,"version":null}"#;
209        let tag: Result<GitTag, _> = serde_json::from_str(json);
210        assert!(tag.is_ok());
211        let tag = tag.unwrap();
212        assert_eq!(tag.name, "v1.0.0");
213        assert_eq!(tag.target, "abc123");
214    }
215}