hashtree_git/
refs.rs

1//! Git references (branches, tags, HEAD)
2//!
3//! Refs are named pointers to commits. They live in the refs namespace:
4//! - refs/heads/* - branches
5//! - refs/tags/* - tags
6//! - HEAD - symbolic ref or direct pointer
7
8use crate::object::ObjectId;
9use crate::Result;
10use crate::Error;
11
12/// A git reference
13#[derive(Debug, Clone)]
14pub enum Ref {
15    /// Direct reference to an object
16    Direct(ObjectId),
17    /// Symbolic reference to another ref (e.g., HEAD -> refs/heads/main)
18    Symbolic(String),
19}
20
21impl Ref {
22    pub fn direct(oid: ObjectId) -> Self {
23        Ref::Direct(oid)
24    }
25
26    pub fn symbolic(target: impl Into<String>) -> Self {
27        Ref::Symbolic(target.into())
28    }
29}
30
31/// Reference with its full name
32#[derive(Debug, Clone)]
33pub struct NamedRef {
34    pub name: String,
35    pub reference: Ref,
36}
37
38impl NamedRef {
39    pub fn new(name: impl Into<String>, reference: Ref) -> Self {
40        Self {
41            name: name.into(),
42            reference,
43        }
44    }
45}
46
47/// Validate a ref name according to git rules
48pub fn validate_ref_name(name: &str) -> Result<()> {
49    if name.is_empty() {
50        return Err(Error::InvalidRefName("empty ref name".into()));
51    }
52
53    // Must not start with / or end with /
54    if name.starts_with('/') || name.ends_with('/') {
55        return Err(Error::InvalidRefName("cannot start or end with /".into()));
56    }
57
58    // No double slashes
59    if name.contains("//") {
60        return Err(Error::InvalidRefName("cannot contain //".into()));
61    }
62
63    // No .. (path traversal)
64    if name.contains("..") {
65        return Err(Error::InvalidRefName("cannot contain ..".into()));
66    }
67
68    // No control chars or special chars
69    for c in name.chars() {
70        if c.is_control() || c == ' ' || c == '~' || c == '^' || c == ':' || c == '?' || c == '*' || c == '[' {
71            return Err(Error::InvalidRefName(format!("invalid character: {:?}", c)));
72        }
73    }
74
75    // Cannot end with .lock
76    if name.ends_with(".lock") {
77        return Err(Error::InvalidRefName("cannot end with .lock".into()));
78    }
79
80    // Cannot contain @{
81    if name.contains("@{") {
82        return Err(Error::InvalidRefName("cannot contain @{".into()));
83    }
84
85    // Cannot be just @
86    if name == "@" {
87        return Err(Error::InvalidRefName("cannot be @".into()));
88    }
89
90    // Cannot end with .
91    if name.ends_with('.') {
92        return Err(Error::InvalidRefName("cannot end with .".into()));
93    }
94
95    Ok(())
96}
97
98/// Common ref constants
99pub const HEAD: &str = "HEAD";
100pub const REFS_HEADS: &str = "refs/heads/";
101pub const REFS_TAGS: &str = "refs/tags/";
102
103/// Create a branch ref name
104pub fn branch_ref(name: &str) -> String {
105    format!("{}{}", REFS_HEADS, name)
106}
107
108/// Create a tag ref name
109pub fn tag_ref(name: &str) -> String {
110    format!("{}{}", REFS_TAGS, name)
111}
112
113/// Extract branch name from full ref
114pub fn branch_name(full_ref: &str) -> Option<&str> {
115    full_ref.strip_prefix(REFS_HEADS)
116}
117
118/// Extract tag name from full ref
119pub fn tag_name(full_ref: &str) -> Option<&str> {
120    full_ref.strip_prefix(REFS_TAGS)
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn test_valid_ref_names() {
129        assert!(validate_ref_name("refs/heads/main").is_ok());
130        assert!(validate_ref_name("refs/heads/feature/test").is_ok());
131        assert!(validate_ref_name("refs/tags/v1.0.0").is_ok());
132        assert!(validate_ref_name("HEAD").is_ok());
133    }
134
135    #[test]
136    fn test_invalid_ref_names() {
137        assert!(validate_ref_name("").is_err());
138        assert!(validate_ref_name("/refs/heads/main").is_err());
139        assert!(validate_ref_name("refs/heads/main/").is_err());
140        assert!(validate_ref_name("refs//heads").is_err());
141        assert!(validate_ref_name("refs/heads/..").is_err());
142        assert!(validate_ref_name("refs/heads/test.lock").is_err());
143        assert!(validate_ref_name("refs/heads/te st").is_err());
144    }
145
146    #[test]
147    fn test_branch_ref() {
148        assert_eq!(branch_ref("main"), "refs/heads/main");
149        assert_eq!(branch_name("refs/heads/main"), Some("main"));
150    }
151}