Skip to main content

git_claw/core/
branch.rs

1//! Branch type definitions and kebab-case name validation.
2
3use crate::core::error::BranchError;
4use serde::{Deserialize, Serialize};
5use std::fmt;
6use std::str::FromStr;
7
8/// Allowed worktree branch types.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(rename_all = "lowercase")]
11pub enum BranchType {
12    Feature,
13    Bugfix,
14    Hotfix,
15    Spike,
16}
17
18impl BranchType {
19    /// Returns the branch type prefix string.
20    pub fn as_str(&self) -> &'static str {
21        match self {
22            Self::Feature => "feature",
23            Self::Bugfix => "bugfix",
24            Self::Hotfix => "hotfix",
25            Self::Spike => "spike",
26        }
27    }
28
29    /// Formats the full Git branch name: `<type>/<name>`.
30    pub fn format_branch_name(&self, name: &str) -> Result<String, BranchError> {
31        validate_leaf_name(name)?;
32        Ok(format!("{}/{}", self.as_str(), name))
33    }
34}
35
36impl fmt::Display for BranchType {
37    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38        write!(f, "{}", self.as_str())
39    }
40}
41
42impl FromStr for BranchType {
43    type Err = BranchError;
44
45    fn from_str(s: &str) -> Result<Self, Self::Err> {
46        match s.to_lowercase().as_str() {
47            "feature" => Ok(Self::Feature),
48            "bugfix" => Ok(Self::Bugfix),
49            "hotfix" => Ok(Self::Hotfix),
50            "spike" => Ok(Self::Spike),
51            _ => Err(BranchError::InvalidBranchType(s.to_string())),
52        }
53    }
54}
55
56/// Validates that a leaf worktree name conforms to kebab-case convention.
57pub fn validate_leaf_name(name: &str) -> Result<(), BranchError> {
58    if name.is_empty() {
59        return Err(BranchError::EmptyName);
60    }
61
62    if name.starts_with('-') || name.ends_with('-') {
63        return Err(BranchError::InvalidKebabCase(name.to_string()));
64    }
65
66    let is_valid = name
67        .chars()
68        .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-');
69
70    if !is_valid {
71        return Err(BranchError::InvalidKebabCase(name.to_string()));
72    }
73
74    Ok(())
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn test_valid_leaf_names() {
83        assert!(validate_leaf_name("auth").is_ok());
84        assert!(validate_leaf_name("user-profile-v2").is_ok());
85        assert!(validate_leaf_name("fix123").is_ok());
86    }
87
88    #[test]
89    fn test_invalid_leaf_names() {
90        assert!(matches!(
91            validate_leaf_name(""),
92            Err(BranchError::EmptyName)
93        ));
94        assert!(matches!(
95            validate_leaf_name("-leading"),
96            Err(BranchError::InvalidKebabCase(_))
97        ));
98        assert!(matches!(
99            validate_leaf_name("trailing-"),
100            Err(BranchError::InvalidKebabCase(_))
101        ));
102        assert!(matches!(
103            validate_leaf_name("Uppercase"),
104            Err(BranchError::InvalidKebabCase(_))
105        ));
106        assert!(matches!(
107            validate_leaf_name("with_underscore"),
108            Err(BranchError::InvalidKebabCase(_))
109        ));
110    }
111
112    #[test]
113    fn test_branch_formatting() {
114        assert_eq!(
115            BranchType::Feature.format_branch_name("auth").unwrap(),
116            "feature/auth"
117        );
118        assert_eq!(
119            BranchType::Bugfix.format_branch_name("null-ptr").unwrap(),
120            "bugfix/null-ptr"
121        );
122    }
123}