git_atomic/core/
refspec.rs1#[derive(Debug, Clone, PartialEq, Eq)]
3pub enum RefSpec {
4 Single(String),
6 Range { start: String, end: String },
8}
9
10impl RefSpec {
11 pub fn parse(input: &str) -> Result<Self, String> {
17 if input.contains("...") {
18 return Err("triple-dot syntax is not supported; use `A..B`".into());
19 }
20 match input.split_once("..") {
21 Some((start, end)) => {
22 let start = if start.is_empty() { "HEAD" } else { start };
23 let end = if end.is_empty() { "HEAD" } else { end };
24 Ok(RefSpec::Range {
25 start: start.to_string(),
26 end: end.to_string(),
27 })
28 }
29 None => Ok(RefSpec::Single(input.to_string())),
30 }
31 }
32}
33
34#[cfg(test)]
35mod tests {
36 use super::*;
37
38 #[test]
39 fn parse_single_ref() {
40 assert_eq!(
41 RefSpec::parse("HEAD").unwrap(),
42 RefSpec::Single("HEAD".into())
43 );
44 assert_eq!(
45 RefSpec::parse("abc123").unwrap(),
46 RefSpec::Single("abc123".into())
47 );
48 assert_eq!(
49 RefSpec::parse("HEAD~3").unwrap(),
50 RefSpec::Single("HEAD~3".into())
51 );
52 }
53
54 #[test]
55 fn parse_range() {
56 assert_eq!(
57 RefSpec::parse("main..feature").unwrap(),
58 RefSpec::Range {
59 start: "main".into(),
60 end: "feature".into()
61 }
62 );
63 assert_eq!(
64 RefSpec::parse("HEAD~3..HEAD").unwrap(),
65 RefSpec::Range {
66 start: "HEAD~3".into(),
67 end: "HEAD".into()
68 }
69 );
70 }
71
72 #[test]
73 fn parse_empty_sides_default_to_head() {
74 assert_eq!(
75 RefSpec::parse("..feature").unwrap(),
76 RefSpec::Range {
77 start: "HEAD".into(),
78 end: "feature".into()
79 }
80 );
81 assert_eq!(
82 RefSpec::parse("main..").unwrap(),
83 RefSpec::Range {
84 start: "main".into(),
85 end: "HEAD".into()
86 }
87 );
88 assert_eq!(
89 RefSpec::parse("..").unwrap(),
90 RefSpec::Range {
91 start: "HEAD".into(),
92 end: "HEAD".into()
93 }
94 );
95 }
96
97 #[test]
98 fn parse_triple_dot_errors() {
99 let err = RefSpec::parse("main...feature").unwrap_err();
100 assert!(err.contains("triple-dot"));
101 }
102
103 #[test]
104 fn parse_unicode_refs() {
105 assert_eq!(
107 RefSpec::parse("\u{1F680}").unwrap(),
108 RefSpec::Single("\u{1F680}".into())
109 );
110 assert_eq!(
112 RefSpec::parse("\u{4E16}\u{754C}").unwrap(),
113 RefSpec::Single("\u{4E16}\u{754C}".into())
114 );
115 assert_eq!(
117 RefSpec::parse("\u{1F680}..\u{4E16}\u{754C}").unwrap(),
118 RefSpec::Range {
119 start: "\u{1F680}".into(),
120 end: "\u{4E16}\u{754C}".into(),
121 }
122 );
123 }
124
125 #[test]
126 fn parse_special_chars() {
127 assert_eq!(
129 RefSpec::parse("my branch").unwrap(),
130 RefSpec::Single("my branch".into())
131 );
132 assert_eq!(
134 RefSpec::parse("refs\\heads\\main").unwrap(),
135 RefSpec::Single("refs\\heads\\main".into())
136 );
137 }
138
139 #[test]
140 fn parse_whitespace_only() {
141 assert_eq!(
142 RefSpec::parse(" ").unwrap(),
143 RefSpec::Single(" ".into())
144 );
145 assert_eq!(RefSpec::parse("\t").unwrap(), RefSpec::Single("\t".into()));
146 }
147
148 #[test]
149 fn parse_very_long_string() {
150 let long = "a".repeat(250);
151 assert_eq!(
152 RefSpec::parse(&long).unwrap(),
153 RefSpec::Single(long.clone())
154 );
155 let long_range = format!("{}..{}", "b".repeat(200), "c".repeat(200));
157 assert_eq!(
158 RefSpec::parse(&long_range).unwrap(),
159 RefSpec::Range {
160 start: "b".repeat(200),
161 end: "c".repeat(200),
162 }
163 );
164 }
165
166 #[test]
167 fn parse_empty_string() {
168 assert_eq!(RefSpec::parse("").unwrap(), RefSpec::Single("".into()));
169 }
170}