1use crate::check_ref_format::{check_refname_format, RefNameOptions};
17
18#[derive(Debug, Clone, Default, PartialEq, Eq)]
23pub struct RefspecItem {
24 pub force: bool,
26 pub negative: bool,
28 pub matching: bool,
30 pub pattern: bool,
32 pub exact_sha1: bool,
34 pub src: Option<String>,
36 pub dst: Option<String>,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
42pub enum RefspecError {
43 Invalid(String),
45}
46
47impl std::fmt::Display for RefspecError {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 match self {
50 RefspecError::Invalid(s) => write!(f, "invalid refspec '{s}'"),
51 }
52 }
53}
54
55impl std::error::Error for RefspecError {}
56
57const SHA1_HEXSZ: usize = 40;
59
60fn is_exact_sha1_hex(s: &str) -> bool {
62 s.len() == SHA1_HEXSZ && s.bytes().all(|b| b.is_ascii_hexdigit())
63}
64
65fn refname_ok(name: &str, is_glob: bool) -> bool {
71 let opts = RefNameOptions {
72 allow_onelevel: true,
73 refspec_pattern: is_glob,
74 normalize: false,
75 };
76 check_refname_format(name, &opts).is_ok()
77}
78
79fn parse_refspec(refspec: &str, fetch: bool) -> Result<RefspecItem, RefspecError> {
84 let bytes = refspec.as_bytes();
85 let invalid = || RefspecError::Invalid(refspec.to_owned());
86
87 let mut item = RefspecItem::default();
88 let mut is_glob = false;
89
90 let mut lhs_start = 0usize;
92 if let Some(&first) = bytes.first() {
93 if first == b'+' {
94 item.force = true;
95 lhs_start = 1;
96 } else if first == b'^' {
97 item.negative = true;
98 lhs_start = 1;
99 }
100 }
101
102 let lhs = &refspec[lhs_start..];
103
104 let colon_pos = lhs.rfind(':');
106
107 if item.negative && colon_pos.is_some() {
109 return Err(invalid());
110 }
111
112 if !fetch && colon_pos == Some(0) && lhs.len() == 1 {
116 item.matching = true;
117 return Ok(item);
118 }
119
120 let (lhs_str, rhs_opt): (&str, Option<&str>) = match colon_pos {
122 Some(pos) => (&lhs[..pos], Some(&lhs[pos + 1..])),
123 None => (lhs, None),
124 };
125
126 if let Some(rhs) = rhs_opt {
127 let rlen = rhs.len();
128 is_glob = rlen >= 1 && rhs.contains('*');
129 item.dst = Some(rhs.to_owned());
130 } else {
131 item.dst = None;
132 }
133
134 let llen = lhs_str.len();
135 if llen >= 1 && lhs_str.contains('*') {
136 let rhs_empty = rhs_opt.is_some_and(|r| r.is_empty());
138 if (rhs_opt.is_some() && !is_glob && !rhs_empty)
139 || (rhs_opt.is_none() && !item.negative && fetch)
140 {
141 return Err(invalid());
142 }
143 is_glob = true;
144 } else if rhs_opt.is_some() && is_glob {
145 return Err(invalid());
147 }
148
149 item.pattern = is_glob;
150 if llen == 1 && lhs_str == "@" {
151 item.src = Some("HEAD".to_owned());
152 } else {
153 item.src = Some(lhs_str.to_owned());
154 }
155 let src = item.src.as_deref().unwrap_or("");
156
157 if item.negative {
158 if src.is_empty() {
160 return Err(invalid()); } else if is_exact_sha1_hex(src) {
162 return Err(invalid()); } else if refname_ok(src, is_glob) {
164 } else {
166 return Err(invalid());
167 }
168 return Ok(item);
169 }
170
171 if fetch {
172 if src.is_empty() {
174 } else if is_exact_sha1_hex(src) {
176 item.exact_sha1 = true; } else if refname_ok(src, is_glob) {
178 } else {
180 return Err(invalid());
181 }
182 match item.dst.as_deref() {
184 None | Some("") => {} Some(dst) => {
186 if !refname_ok(dst, is_glob) {
187 return Err(invalid());
188 }
189 }
190 }
191 } else {
192 if src.is_empty() {
195 } else if is_glob {
197 if !refname_ok(src, is_glob) {
198 return Err(invalid());
199 }
200 } else {
201 }
203 match item.dst.as_deref() {
205 None => {
206 if !refname_ok(src, is_glob) {
208 return Err(invalid());
209 }
210 }
211 Some("") => {
212 return Err(invalid());
214 }
215 Some(dst) => {
216 if !refname_ok(dst, is_glob) {
217 return Err(invalid());
218 }
219 }
220 }
221 }
222
223 Ok(item)
224}
225
226pub fn parse_fetch_refspec(refspec: &str) -> Result<RefspecItem, RefspecError> {
228 parse_refspec(refspec, true)
229}
230
231pub fn parse_push_refspec(refspec: &str) -> Result<RefspecItem, RefspecError> {
233 parse_refspec(refspec, false)
234}
235
236pub fn valid_fetch_refspec(refspec: &str) -> bool {
238 parse_refspec(refspec, true).is_ok()
239}
240
241pub fn valid_push_refspec(refspec: &str) -> bool {
243 parse_refspec(refspec, false).is_ok()
244}
245
246fn translate_single_star(
248 from_template: &str,
249 into_template: &str,
250 from_ref: &str,
251) -> Option<String> {
252 let (from_pre, from_post) = split_one_star(from_template)?;
253 let (into_pre, into_post) = split_one_star(into_template)?;
254 if !from_ref.starts_with(from_pre) || !from_ref.ends_with(from_post) {
255 return None;
256 }
257 let middle_len = from_ref
258 .len()
259 .checked_sub(from_pre.len() + from_post.len())?;
260 let middle = &from_ref[from_pre.len()..from_pre.len() + middle_len];
261 Some(format!("{into_pre}{middle}{into_post}"))
262}
263
264fn split_one_star(template: &str) -> Option<(&str, &str)> {
265 let star = template.find('*')?;
266 if template[star + 1..].contains('*') {
267 return None;
268 }
269 Some((&template[..star], &template[star + 1..]))
270}
271
272fn refname_matches_template(template: &str, is_pattern: bool, refname: &str) -> bool {
273 if is_pattern {
274 translate_single_star(template, template, refname).is_some()
275 } else {
276 template == refname
277 }
278}
279
280#[derive(Debug, Clone, PartialEq, Eq)]
282pub(crate) struct FetchLocalBinding {
283 pub force: bool,
284 pub upstream_ref: String,
285}
286
287fn positive_local_binding(spec: &RefspecItem, local_ref: &str) -> Option<FetchLocalBinding> {
288 if spec.negative {
289 return None;
290 }
291 let dst = spec.dst.as_deref()?;
292 let src = spec.src.as_deref().unwrap_or("");
293 let upstream_ref = if spec.pattern {
294 translate_single_star(dst, src, local_ref)?
295 } else if dst == local_ref {
296 src.to_owned()
297 } else {
298 return None;
299 };
300 Some(FetchLocalBinding {
301 force: spec.force,
302 upstream_ref,
303 })
304}
305
306fn excluded_by_negative_specs(specs: &[RefspecItem], upstream_ref: &str) -> bool {
307 specs.iter().any(|spec| {
308 spec.negative
309 && spec
310 .src
311 .as_deref()
312 .is_some_and(|src| refname_matches_template(src, spec.pattern, upstream_ref))
313 })
314}
315
316#[must_use]
318pub(crate) fn fetch_specs_bind_local_ref(
319 specs: &[RefspecItem],
320 local_ref: &str,
321) -> Option<FetchLocalBinding> {
322 let binding = specs
323 .iter()
324 .find_map(|spec| positive_local_binding(spec, local_ref))?;
325 if excluded_by_negative_specs(specs, &binding.upstream_ref) {
326 return None;
327 }
328 Some(binding)
329}
330
331#[cfg(test)]
332mod tests {
333 use super::*;
334
335 fn fetch_valid(s: &str) {
338 assert!(valid_fetch_refspec(s), "expected fetch '{s}' to be valid");
339 }
340 fn fetch_invalid(s: &str) {
341 assert!(
342 !valid_fetch_refspec(s),
343 "expected fetch '{s}' to be invalid"
344 );
345 }
346 fn push_valid(s: &str) {
347 assert!(valid_push_refspec(s), "expected push '{s}' to be valid");
348 }
349 fn push_invalid(s: &str) {
350 assert!(!valid_push_refspec(s), "expected push '{s}' to be invalid");
351 }
352
353 #[test]
354 fn empty_and_colon() {
355 push_invalid("");
356 push_valid(":");
357 push_invalid("::");
358 push_valid("+:");
359 fetch_valid("");
360 fetch_valid(":");
361 fetch_invalid("::");
362 }
363
364 #[test]
365 fn glob_balance() {
366 push_valid("refs/heads/*:refs/remotes/frotz/*");
367 push_invalid("refs/heads/*:refs/remotes/frotz");
368 push_invalid("refs/heads:refs/remotes/frotz/*");
369 push_valid("refs/heads/main:refs/remotes/frotz/xyzzy");
370
371 fetch_valid("refs/heads/*:refs/remotes/frotz/*");
372 fetch_valid("refs/heads/*:");
373 fetch_invalid("refs/heads/*:refs/remotes/frotz");
374 fetch_invalid("refs/heads:refs/remotes/frotz/*");
375 fetch_valid("refs/heads/main:refs/remotes/frotz/xyzzy");
376 fetch_invalid("refs/heads/main::refs/remotes/frotz/xyzzy");
377 fetch_invalid("refs/heads/maste :refs/remotes/frotz/xyzzy");
378 }
379
380 #[test]
381 fn rev_expressions() {
382 push_valid("main~1:refs/remotes/frotz/backup");
383 fetch_invalid("main~1:refs/remotes/frotz/backup");
384 push_valid("HEAD~4:refs/remotes/frotz/new");
385 fetch_invalid("HEAD~4:refs/remotes/frotz/new");
386 }
387
388 #[test]
389 fn bare_head_and_at() {
390 push_valid("HEAD");
391 fetch_valid("HEAD");
392 push_valid("@");
393 fetch_valid("@");
394 push_invalid("refs/heads/ nitfol");
395 fetch_invalid("refs/heads/ nitfol");
396 }
397
398 #[test]
399 fn head_colon() {
400 push_invalid("HEAD:");
401 fetch_valid("HEAD:");
402 push_invalid("refs/heads/ nitfol:");
403 fetch_invalid("refs/heads/ nitfol:");
404 }
405
406 #[test]
407 fn delete_specs() {
408 push_valid(":refs/remotes/frotz/deleteme");
409 fetch_valid(":refs/remotes/frotz/HEAD-to-me");
410 push_invalid(":refs/remotes/frotz/delete me");
411 fetch_invalid(":refs/remotes/frotz/HEAD to me");
412 }
413
414 #[test]
415 fn star_placements() {
416 fetch_valid("refs/heads/*/for-linus:refs/remotes/mine/*-blah");
417 push_valid("refs/heads/*/for-linus:refs/remotes/mine/*-blah");
418 fetch_valid("refs/heads*/for-linus:refs/remotes/mine/*");
419 push_valid("refs/heads*/for-linus:refs/remotes/mine/*");
420 fetch_invalid("refs/heads/*/*/for-linus:refs/remotes/mine/*");
421 push_invalid("refs/heads/*/*/for-linus:refs/remotes/mine/*");
422 fetch_invalid("refs/heads/*g*/for-linus:refs/remotes/mine/*");
423 push_invalid("refs/heads/*g*/for-linus:refs/remotes/mine/*");
424 fetch_valid("refs/heads/*/for-linus:refs/remotes/mine/*");
425 push_valid("refs/heads/*/for-linus:refs/remotes/mine/*");
426 }
427
428 #[test]
429 fn utf8_and_tab() {
430 fetch_valid("refs/heads/\u{00C4}");
431 fetch_invalid("refs/heads/\ttab");
432 }
433
434 #[test]
435 fn fetch_specs_bind_local_ref_maps_tracking_branch() {
436 let specs = vec![parse_fetch_refspec("+refs/heads/*:refs/remotes/origin/*").unwrap()];
437 let binding = fetch_specs_bind_local_ref(&specs, "refs/remotes/origin/main").unwrap();
438 assert_eq!(binding.upstream_ref, "refs/heads/main");
439 }
440
441 #[test]
442 fn fetch_specs_bind_local_ref_honors_negative_line() {
443 let specs = vec![
444 parse_fetch_refspec("+refs/heads/*:refs/remotes/origin/*").unwrap(),
445 parse_fetch_refspec("^refs/heads/private").unwrap(),
446 ];
447 assert!(fetch_specs_bind_local_ref(&specs, "refs/remotes/origin/main").is_some());
448 assert!(fetch_specs_bind_local_ref(&specs, "refs/remotes/origin/private").is_none());
449 }
450}