Skip to main content

git_proc/
branch.rs

1//! Git branch name type with validation.
2
3use crate::ref_format::{self, RefFormatError};
4
5crate::cow_str_newtype! {
6    /// A validated git branch name.
7    ///
8    /// Branch names follow git's reference naming rules; see
9    /// [`crate::ref_format`] for the full ruleset.
10    pub struct Branch, BranchError(RefFormatError), "invalid branch name"
11}
12
13impl Branch {
14    /// Returns true if the branch name contains path separators.
15    #[must_use]
16    pub fn has_parents(&self) -> bool {
17        self.0.contains('/')
18    }
19
20    const fn validate(input: &str) -> Result<(), BranchError> {
21        match ref_format::validate(input) {
22            Ok(()) => Ok(()),
23            Err(error) => Err(BranchError(error)),
24        }
25    }
26}
27
28#[cfg(test)]
29mod tests {
30    use super::*;
31
32    #[test]
33    fn test_valid_branch() {
34        assert!("main".parse::<Branch>().is_ok());
35        assert!("feature/login".parse::<Branch>().is_ok());
36        assert!("feature/deeply/nested/branch".parse::<Branch>().is_ok());
37        assert!("fix-123".parse::<Branch>().is_ok());
38    }
39
40    #[test]
41    fn test_has_parents() {
42        assert!(!Branch::from_static_or_panic("main").has_parents());
43        assert!(Branch::from_static_or_panic("feature/login").has_parents());
44    }
45
46    #[test]
47    fn test_invalid_passes_through() {
48        // Spot-check that validation is wired up; full coverage lives in ref_format.
49        assert!(matches!(
50            "".parse::<Branch>(),
51            Err(BranchError(RefFormatError::Empty))
52        ));
53        assert!(matches!(
54            "-branch".parse::<Branch>(),
55            Err(BranchError(RefFormatError::StartsWithDash))
56        ));
57        assert!(matches!(
58            "feature/.hidden".parse::<Branch>(),
59            Err(BranchError(RefFormatError::ComponentStartsWithDot))
60        ));
61    }
62
63    #[test]
64    fn test_from_static_or_panic() {
65        let branch = Branch::from_static_or_panic("main");
66        assert_eq!(branch.as_str(), "main");
67    }
68
69    #[test]
70    fn test_display() {
71        let branch: Branch = "feature/test".parse().unwrap();
72        assert_eq!(format!("{branch}"), "feature/test");
73    }
74
75    #[test]
76    fn test_as_ref_os_str() {
77        use std::ffi::OsStr;
78        let branch: Branch = "main".parse().unwrap();
79        let os_str: &OsStr = branch.as_ref();
80        assert_eq!(os_str, "main");
81    }
82
83    #[test]
84    fn test_serialize() {
85        let branch: Branch = "feature/login".parse().unwrap();
86        assert_eq!(serde_json::to_string(&branch).unwrap(), "\"feature/login\"");
87    }
88
89    #[test]
90    fn test_deserialize() {
91        let branch: Branch = serde_json::from_str("\"feature/login\"").unwrap();
92        assert_eq!(branch.as_str(), "feature/login");
93    }
94
95    #[test]
96    fn test_deserialize_invalid() {
97        assert!(serde_json::from_str::<Branch>("\"-bad\"").is_err());
98    }
99}