Skip to main content

git_proc/
tag.rs

1//! Git tag name type with validation, plus `git tag` command builders.
2
3use std::path::Path;
4
5use crate::CommandError;
6use crate::ref_format::{self, RefFormatError};
7
8crate::cow_str_newtype! {
9    /// A validated git tag name.
10    ///
11    /// Tag names follow git's reference naming rules; see
12    /// [`crate::ref_format`] for the full ruleset.
13    pub struct Tag, TagError(RefFormatError), "invalid tag name"
14}
15
16impl Tag {
17    const fn validate(input: &str) -> Result<(), TagError> {
18        match ref_format::validate(input) {
19            Ok(()) => Ok(()),
20            Err(error) => Err(TagError(error)),
21        }
22    }
23}
24
25/// Create a new `git tag <name>` command builder.
26#[must_use]
27pub fn create(name: &Tag) -> Create<'_> {
28    Create::new(name)
29}
30
31/// Builder for `git tag <name>` (lightweight tag at HEAD).
32///
33/// See `git tag --help` for full documentation.
34#[derive(Debug)]
35pub struct Create<'a> {
36    repo_path: Option<&'a Path>,
37    name: &'a Tag,
38}
39
40crate::impl_repo_path!(Create);
41
42impl<'a> Create<'a> {
43    #[must_use]
44    fn new(name: &'a Tag) -> Self {
45        Self {
46            repo_path: None,
47            name,
48        }
49    }
50
51    /// Execute the command and return the exit status.
52    pub async fn status(self) -> Result<(), CommandError> {
53        crate::Build::build(self).status().await
54    }
55}
56
57impl crate::Build for Create<'_> {
58    fn build(self) -> cmd_proc::Command {
59        crate::base_command(self.repo_path)
60            .argument("tag")
61            .argument(self.name)
62    }
63}
64
65#[cfg(feature = "test-utils")]
66impl Create<'_> {
67    /// Compare the built command with another command using debug representation.
68    pub fn test_eq(&self, other: &cmd_proc::Command) {
69        let command = crate::Build::build(Self {
70            repo_path: self.repo_path,
71            name: self.name,
72        });
73        command.test_eq(other);
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn test_valid_tag() {
83        assert!("v1.0.0".parse::<Tag>().is_ok());
84        assert!("release-2024".parse::<Tag>().is_ok());
85        assert!("hotfix/v1.2.3".parse::<Tag>().is_ok());
86    }
87
88    #[test]
89    fn test_invalid_passes_through() {
90        assert!(matches!(
91            "".parse::<Tag>(),
92            Err(TagError(RefFormatError::Empty))
93        ));
94        assert!(matches!(
95            "-tag".parse::<Tag>(),
96            Err(TagError(RefFormatError::StartsWithDash))
97        ));
98        assert!(matches!(
99            "v1.0.0.lock".parse::<Tag>(),
100            Err(TagError(RefFormatError::EndsWithLock))
101        ));
102    }
103
104    #[test]
105    fn test_from_static_or_panic() {
106        let tag = Tag::from_static_or_panic("v1.0.0");
107        assert_eq!(tag.as_str(), "v1.0.0");
108    }
109
110    #[test]
111    fn test_display() {
112        let tag: Tag = "v1.0.0".parse().unwrap();
113        assert_eq!(format!("{tag}"), "v1.0.0");
114    }
115
116    #[test]
117    fn test_as_ref_os_str() {
118        use std::ffi::OsStr;
119        let tag: Tag = "v1.0.0".parse().unwrap();
120        let os_str: &OsStr = tag.as_ref();
121        assert_eq!(os_str, "v1.0.0");
122    }
123
124    #[test]
125    fn test_serialize() {
126        let tag: Tag = "v1.0.0".parse().unwrap();
127        assert_eq!(serde_json::to_string(&tag).unwrap(), "\"v1.0.0\"");
128    }
129
130    #[test]
131    fn test_deserialize() {
132        let tag: Tag = serde_json::from_str("\"v1.0.0\"").unwrap();
133        assert_eq!(tag.as_str(), "v1.0.0");
134    }
135
136    #[test]
137    fn test_deserialize_invalid() {
138        assert!(serde_json::from_str::<Tag>("\"-bad\"").is_err());
139    }
140}