1use crate::object::ObjectId;
9use crate::Result;
10use crate::Error;
11
12#[derive(Debug, Clone)]
14pub enum Ref {
15 Direct(ObjectId),
17 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#[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
47pub fn validate_ref_name(name: &str) -> Result<()> {
49 if name.is_empty() {
50 return Err(Error::InvalidRefName("empty ref name".into()));
51 }
52
53 if name.starts_with('/') || name.ends_with('/') {
55 return Err(Error::InvalidRefName("cannot start or end with /".into()));
56 }
57
58 if name.contains("//") {
60 return Err(Error::InvalidRefName("cannot contain //".into()));
61 }
62
63 if name.contains("..") {
65 return Err(Error::InvalidRefName("cannot contain ..".into()));
66 }
67
68 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 if name.ends_with(".lock") {
77 return Err(Error::InvalidRefName("cannot end with .lock".into()));
78 }
79
80 if name.contains("@{") {
82 return Err(Error::InvalidRefName("cannot contain @{".into()));
83 }
84
85 if name == "@" {
87 return Err(Error::InvalidRefName("cannot be @".into()));
88 }
89
90 if name.ends_with('.') {
92 return Err(Error::InvalidRefName("cannot end with .".into()));
93 }
94
95 Ok(())
96}
97
98pub const HEAD: &str = "HEAD";
100pub const REFS_HEADS: &str = "refs/heads/";
101pub const REFS_TAGS: &str = "refs/tags/";
102
103pub fn branch_ref(name: &str) -> String {
105 format!("{}{}", REFS_HEADS, name)
106}
107
108pub fn tag_ref(name: &str) -> String {
110 format!("{}{}", REFS_TAGS, name)
111}
112
113pub fn branch_name(full_ref: &str) -> Option<&str> {
115 full_ref.strip_prefix(REFS_HEADS)
116}
117
118pub 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}