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(crate) 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
236#[allow(dead_code)] pub(crate) fn valid_fetch_refspec(refspec: &str) -> bool {
239 parse_refspec(refspec, true).is_ok()
240}
241
242#[allow(dead_code)] pub(crate) fn valid_push_refspec(refspec: &str) -> bool {
245 parse_refspec(refspec, false).is_ok()
246}
247
248fn translate_single_star(
250 from_template: &str,
251 into_template: &str,
252 from_ref: &str,
253) -> Option<String> {
254 let (from_pre, from_post) = split_one_star(from_template)?;
255 let (into_pre, into_post) = split_one_star(into_template)?;
256 if !from_ref.starts_with(from_pre) || !from_ref.ends_with(from_post) {
257 return None;
258 }
259 let middle_len = from_ref
260 .len()
261 .checked_sub(from_pre.len() + from_post.len())?;
262 let middle = &from_ref[from_pre.len()..from_pre.len() + middle_len];
263 Some(format!("{into_pre}{middle}{into_post}"))
264}
265
266fn split_one_star(template: &str) -> Option<(&str, &str)> {
267 let star = template.find('*')?;
268 if template[star + 1..].contains('*') {
269 return None;
270 }
271 Some((&template[..star], &template[star + 1..]))
272}
273
274fn refname_matches_template(template: &str, is_pattern: bool, refname: &str) -> bool {
275 if is_pattern {
276 translate_single_star(template, template, refname).is_some()
277 } else {
278 template == refname
279 }
280}
281
282#[derive(Debug, Clone, PartialEq, Eq)]
284pub(crate) struct FetchLocalBinding {
285 pub force: bool,
286 pub upstream_ref: String,
287}
288
289fn positive_local_binding(spec: &RefspecItem, local_ref: &str) -> Option<FetchLocalBinding> {
290 if spec.negative {
291 return None;
292 }
293 let dst = spec.dst.as_deref()?;
294 let src = spec.src.as_deref().unwrap_or("");
295 let upstream_ref = if spec.pattern {
296 translate_single_star(dst, src, local_ref)?
297 } else if dst == local_ref {
298 src.to_owned()
299 } else {
300 return None;
301 };
302 Some(FetchLocalBinding {
303 force: spec.force,
304 upstream_ref,
305 })
306}
307
308fn excluded_by_negative_specs(specs: &[RefspecItem], upstream_ref: &str) -> bool {
309 specs.iter().any(|spec| {
310 spec.negative
311 && spec
312 .src
313 .as_deref()
314 .is_some_and(|src| refname_matches_template(src, spec.pattern, upstream_ref))
315 })
316}
317
318#[must_use]
320pub(crate) fn fetch_specs_bind_local_ref(
321 specs: &[RefspecItem],
322 local_ref: &str,
323) -> Option<FetchLocalBinding> {
324 let binding = specs
325 .iter()
326 .find_map(|spec| positive_local_binding(spec, local_ref))?;
327 if excluded_by_negative_specs(specs, &binding.upstream_ref) {
328 return None;
329 }
330 Some(binding)
331}
332
333#[cfg(test)]
334mod tests {
335 use super::*;
336
337 fn fetch_valid(s: &str) {
340 assert!(valid_fetch_refspec(s), "expected fetch '{s}' to be valid");
341 }
342 fn fetch_invalid(s: &str) {
343 assert!(
344 !valid_fetch_refspec(s),
345 "expected fetch '{s}' to be invalid"
346 );
347 }
348 fn push_valid(s: &str) {
349 assert!(valid_push_refspec(s), "expected push '{s}' to be valid");
350 }
351 fn push_invalid(s: &str) {
352 assert!(!valid_push_refspec(s), "expected push '{s}' to be invalid");
353 }
354
355 #[test]
356 fn empty_and_colon() {
357 push_invalid("");
358 push_valid(":");
359 push_invalid("::");
360 push_valid("+:");
361 fetch_valid("");
362 fetch_valid(":");
363 fetch_invalid("::");
364 }
365
366 #[test]
367 fn glob_balance() {
368 push_valid("refs/heads/*:refs/remotes/frotz/*");
369 push_invalid("refs/heads/*:refs/remotes/frotz");
370 push_invalid("refs/heads:refs/remotes/frotz/*");
371 push_valid("refs/heads/main:refs/remotes/frotz/xyzzy");
372
373 fetch_valid("refs/heads/*:refs/remotes/frotz/*");
374 fetch_valid("refs/heads/*:");
375 fetch_invalid("refs/heads/*:refs/remotes/frotz");
376 fetch_invalid("refs/heads:refs/remotes/frotz/*");
377 fetch_valid("refs/heads/main:refs/remotes/frotz/xyzzy");
378 fetch_invalid("refs/heads/main::refs/remotes/frotz/xyzzy");
379 fetch_invalid("refs/heads/maste :refs/remotes/frotz/xyzzy");
380 }
381
382 #[test]
383 fn rev_expressions() {
384 push_valid("main~1:refs/remotes/frotz/backup");
385 fetch_invalid("main~1:refs/remotes/frotz/backup");
386 push_valid("HEAD~4:refs/remotes/frotz/new");
387 fetch_invalid("HEAD~4:refs/remotes/frotz/new");
388 }
389
390 #[test]
391 fn bare_head_and_at() {
392 push_valid("HEAD");
393 fetch_valid("HEAD");
394 push_valid("@");
395 fetch_valid("@");
396 push_invalid("refs/heads/ nitfol");
397 fetch_invalid("refs/heads/ nitfol");
398 }
399
400 #[test]
401 fn head_colon() {
402 push_invalid("HEAD:");
403 fetch_valid("HEAD:");
404 push_invalid("refs/heads/ nitfol:");
405 fetch_invalid("refs/heads/ nitfol:");
406 }
407
408 #[test]
409 fn delete_specs() {
410 push_valid(":refs/remotes/frotz/deleteme");
411 fetch_valid(":refs/remotes/frotz/HEAD-to-me");
412 push_invalid(":refs/remotes/frotz/delete me");
413 fetch_invalid(":refs/remotes/frotz/HEAD to me");
414 }
415
416 #[test]
417 fn star_placements() {
418 fetch_valid("refs/heads/*/for-linus:refs/remotes/mine/*-blah");
419 push_valid("refs/heads/*/for-linus:refs/remotes/mine/*-blah");
420 fetch_valid("refs/heads*/for-linus:refs/remotes/mine/*");
421 push_valid("refs/heads*/for-linus:refs/remotes/mine/*");
422 fetch_invalid("refs/heads/*/*/for-linus:refs/remotes/mine/*");
423 push_invalid("refs/heads/*/*/for-linus:refs/remotes/mine/*");
424 fetch_invalid("refs/heads/*g*/for-linus:refs/remotes/mine/*");
425 push_invalid("refs/heads/*g*/for-linus:refs/remotes/mine/*");
426 fetch_valid("refs/heads/*/for-linus:refs/remotes/mine/*");
427 push_valid("refs/heads/*/for-linus:refs/remotes/mine/*");
428 }
429
430 #[test]
431 fn utf8_and_tab() {
432 fetch_valid("refs/heads/\u{00C4}");
433 fetch_invalid("refs/heads/\ttab");
434 }
435
436 #[test]
437 fn fetch_specs_bind_local_ref_maps_tracking_branch() {
438 let specs = vec![parse_fetch_refspec("+refs/heads/*:refs/remotes/origin/*").unwrap()];
439 let binding = fetch_specs_bind_local_ref(&specs, "refs/remotes/origin/main").unwrap();
440 assert_eq!(binding.upstream_ref, "refs/heads/main");
441 }
442
443 #[test]
444 fn fetch_specs_bind_local_ref_honors_negative_line() {
445 let specs = vec![
446 parse_fetch_refspec("+refs/heads/*:refs/remotes/origin/*").unwrap(),
447 parse_fetch_refspec("^refs/heads/private").unwrap(),
448 ];
449 assert!(fetch_specs_bind_local_ref(&specs, "refs/remotes/origin/main").is_some());
450 assert!(fetch_specs_bind_local_ref(&specs, "refs/remotes/origin/private").is_none());
451 }
452}