1use thiserror::Error;
2
3#[derive(Debug, Error)]
4pub enum HashError {
5 #[error("no match for short hash '{0}'")]
6 NoMatch(String),
7 #[error("ambiguous short hash '{0}': matches {1:?}")]
8 Ambiguous(String, Vec<String>),
9 #[error("invalid hex: '{0}'")]
10 InvalidHex(String),
11}
12
13pub fn normalize_commit_ref(s: &str) -> String {
16 if let Some(idx) = s.find("/commit/") {
18 s[idx + "/commit/".len()..].to_string()
19 } else {
20 s.to_string()
21 }
22}
23
24pub fn is_valid_hex_hash(s: &str) -> bool {
26 s.len() == 40
27 && s.chars()
28 .all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase())
29}
30
31pub fn strip_carriage_returns(s: &str) -> String {
33 s.replace('\r', "")
34}
35
36pub fn expand_short_hash(short: &str, candidates: &[String]) -> Result<String, HashError> {
39 if short.len() == 40 {
41 if is_valid_hex_hash(short) {
42 return Ok(short.to_string());
43 }
44 return Err(HashError::InvalidHex(short.to_string()));
45 }
46
47 if !short.chars().all(|c| c.is_ascii_hexdigit()) {
49 return Err(HashError::InvalidHex(short.to_string()));
50 }
51
52 let matches: Vec<String> = candidates
53 .iter()
54 .filter(|c| c.starts_with(short))
55 .cloned()
56 .collect();
57
58 match matches.len() {
59 0 => Err(HashError::NoMatch(short.to_string())),
60 1 => Ok(matches.into_iter().next().unwrap()),
61 _ => Err(HashError::Ambiguous(short.to_string(), matches)),
62 }
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68
69 #[test]
70 fn test_expand_short_hash_unique_match() {
71 let candidates = vec![
72 "204e93abf18ab00e339d92787c6f807269517cdf".to_string(),
73 "b012110bedc7f0ffca3ae37f38915afbc229c26e".to_string(),
74 ];
75 let result = expand_short_hash("204e93ab", &candidates).unwrap();
76 assert_eq!(result, "204e93abf18ab00e339d92787c6f807269517cdf");
77 }
78
79 #[test]
80 fn test_expand_short_hash_no_match() {
81 let candidates = vec!["204e93abf18ab00e339d92787c6f807269517cdf".to_string()];
82 let result = expand_short_hash("deadbeef", &candidates);
83 assert!(matches!(result, Err(HashError::NoMatch(_))));
84 }
85
86 #[test]
87 fn test_expand_short_hash_ambiguous() {
88 let candidates = vec![
89 "204e93abf18ab00e339d92787c6f807269517cdf".to_string(),
90 "204e93ab0000000000000000000000000000000000".to_string(),
91 ];
92 let result = expand_short_hash("204e93ab", &candidates);
93 assert!(matches!(result, Err(HashError::Ambiguous(_, _))));
94 }
95
96 #[test]
97 fn test_expand_full_hash_passthrough() {
98 let candidates = vec![];
99 let hash = "204e93abf18ab00e339d92787c6f807269517cdf";
100 let result = expand_short_hash(hash, &candidates).unwrap();
101 assert_eq!(result, hash);
102 }
103
104 #[test]
105 fn test_expand_invalid_hex() {
106 let candidates = vec![];
107 let result = expand_short_hash("not_hex!", &candidates);
108 assert!(matches!(result, Err(HashError::InvalidHex(_))));
109 }
110
111 #[test]
112 fn test_normalize_commit_ref_url() {
113 let url = "https://github.com/jarchain/jar/commit/204e93abf18ab00e339d92787c6f807269517cdf";
114 assert_eq!(
115 normalize_commit_ref(url),
116 "204e93abf18ab00e339d92787c6f807269517cdf"
117 );
118 }
119
120 #[test]
121 fn test_normalize_commit_ref_bare_hash() {
122 let hash = "204e93abf18ab00e339d92787c6f807269517cdf";
123 assert_eq!(normalize_commit_ref(hash), hash);
124 }
125
126 #[test]
127 fn test_is_valid_hex_hash() {
128 assert!(is_valid_hex_hash(
129 "204e93abf18ab00e339d92787c6f807269517cdf"
130 ));
131 assert!(!is_valid_hex_hash("too_short"));
132 assert!(!is_valid_hex_hash(
133 "204E93ABF18AB00E339D92787C6F807269517CDF"
134 )); assert!(!is_valid_hex_hash(
136 "204e93abf18ab00e339d92787c6f807269517cdX"
137 )); }
139
140 #[test]
141 fn test_strip_carriage_returns() {
142 assert_eq!(strip_carriage_returns("merge\r"), "merge");
143 assert_eq!(strip_carriage_returns("merge"), "merge");
144 assert_eq!(strip_carriage_returns("a\rb\rc\r"), "abc");
145 }
146
147 #[test]
148 fn test_normalize_different_repo_url() {
149 let url = "https://github.com/other-org/other-repo/commit/abcdef1234567890abcdef1234567890abcdef12";
150 assert_eq!(
151 normalize_commit_ref(url),
152 "abcdef1234567890abcdef1234567890abcdef12"
153 );
154 }
155
156 #[test]
157 fn test_normalize_no_commit_path() {
158 let url = "https://github.com/jarchain/jar/pull/95";
160 assert_eq!(normalize_commit_ref(url), url);
161 }
162
163 #[test]
164 fn test_expand_empty_short_hash() {
165 let candidates = vec!["abcdef1234567890abcdef1234567890abcdef12".to_string()];
166 let result = expand_short_hash("", &candidates);
169 assert!(result.is_ok() || matches!(result, Err(HashError::Ambiguous(_, _))));
171 }
172
173 #[test]
174 fn test_expand_full_hash_invalid() {
175 let candidates = vec![];
177 let hash = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
178 let result = expand_short_hash(hash, &candidates);
179 assert!(matches!(result, Err(HashError::InvalidHex(_))));
180 }
181
182 #[test]
183 fn test_is_valid_hex_hash_empty() {
184 assert!(!is_valid_hex_hash(""));
185 }
186
187 #[test]
188 fn test_is_valid_hex_hash_39_chars() {
189 assert!(!is_valid_hex_hash(
190 "204e93abf18ab00e339d92787c6f807269517cd"
191 ));
192 }
193}