1#[derive(Debug, thiserror::Error)]
3#[expect(missing_docs)]
4pub enum Error {
5 #[error("Empty refspecs are invalid")]
6 Empty,
7 #[error("Negative refspecs cannot have destinations as they exclude sources")]
8 NegativeWithDestination,
9 #[error("Negative specs must not be empty")]
10 NegativeEmpty,
11 #[error("Negative specs must not be object hashes")]
12 NegativeObjectHash,
13 #[error("Negative specs must be full ref names, starting with \"refs/\"")]
15 NegativePartialName,
16 #[error("Negative glob patterns are not allowed")]
18 NegativeGlobPattern,
19 #[error("Fetch destinations must be ref-names, like 'HEAD:refs/heads/branch'")]
21 InvalidFetchDestination,
22 #[error("Cannot push into an empty destination")]
23 PushToEmpty,
24 #[error("refspec patterns may only contain a single '*' character, found {pattern:?}")]
25 PatternUnsupported { pattern: bstr::BString },
26 #[error("Both sides of a two-sided specification need a pattern, like 'a/*:b/*'")]
27 PatternUnbalanced,
28 #[error(transparent)]
29 ReferenceName(#[from] gix_validate::reference::name::Error),
30}
31
32#[derive(PartialOrd, Ord, PartialEq, Eq, Copy, Clone, Hash, Debug)]
34pub enum Operation {
35 Push,
37 Fetch,
39}
40
41pub(crate) mod function {
42 use crate::{
43 RefSpecRef,
44 parse::{Error, Operation},
45 types::Mode,
46 };
47 use bstr::{BStr, ByteSlice};
48
49 pub fn parse(mut spec: &BStr, operation: Operation) -> Result<RefSpecRef<'_>, Error> {
51 fn fetch_head_only(mode: Mode) -> RefSpecRef<'static> {
52 RefSpecRef {
53 mode,
54 op: Operation::Fetch,
55 src: Some("HEAD".into()),
56 dst: None,
57 }
58 }
59
60 let mode = match spec.first() {
61 Some(&b'^') => {
62 spec = &spec[1..];
63 Mode::Negative
64 }
65 Some(&b'+') => {
66 spec = &spec[1..];
67 Mode::Force
68 }
69 Some(_) => Mode::Normal,
70 None => {
71 return match operation {
72 Operation::Push => Err(Error::Empty),
73 Operation::Fetch => Ok(fetch_head_only(Mode::Normal)),
74 };
75 }
76 };
77
78 let (mut src, dst) = match spec.rfind_byte(b':') {
82 Some(pos) => {
83 if mode == Mode::Negative {
84 return Err(Error::NegativeWithDestination);
85 }
86
87 let (src, dst) = spec.split_at(pos);
88 let dst = &dst[1..];
89 let src = (!src.is_empty()).then(|| src.as_bstr());
90 let dst = (!dst.is_empty()).then(|| dst.as_bstr());
91 match (src, dst) {
92 (None, None) => match operation {
93 Operation::Push => (None, None),
94 Operation::Fetch => (Some("HEAD".into()), None),
95 },
96 (None, Some(dst)) => match operation {
97 Operation::Push => (None, Some(dst)),
98 Operation::Fetch => (Some("HEAD".into()), Some(dst)),
99 },
100 (Some(src), None) => match operation {
101 Operation::Push => return Err(Error::PushToEmpty),
102 Operation::Fetch => (Some(src), None),
103 },
104 (Some(src), Some(dst)) => (Some(src), Some(dst)),
105 }
106 }
107 None => {
108 let src = (!spec.is_empty()).then_some(spec);
109 if Operation::Fetch == operation && mode != Mode::Negative && src.is_none() {
110 return Ok(fetch_head_only(mode));
111 } else {
112 (src, None)
113 }
114 }
115 };
116
117 if let Some(spec) = src.as_mut() {
118 if *spec == "@" {
119 *spec = "HEAD".into();
120 }
121 }
122 let (src, src_had_pattern) = validated(src, operation == Operation::Push && dst.is_some())?;
123 let (dst, dst_had_pattern) = validated(dst, false)?;
124 if mode != Mode::Negative
125 && src_had_pattern != dst_had_pattern
126 && !(operation == Operation::Push && dst.is_none())
127 {
128 return Err(Error::PatternUnbalanced);
129 }
130
131 if mode == Mode::Negative {
132 match src {
133 Some(spec) => {
134 if looks_like_object_hash(spec) {
135 return Err(Error::NegativeObjectHash);
136 }
137 }
138 None => return Err(Error::NegativeEmpty),
139 }
140 }
141
142 Ok(RefSpecRef {
143 op: operation,
144 mode,
145 src,
146 dst,
147 })
148 }
149
150 fn looks_like_object_hash(spec: &BStr) -> bool {
151 spec.len() >= gix_hash::Kind::shortest().len_in_hex() && spec.iter().all(u8::is_ascii_hexdigit)
152 }
153
154 fn validate_partial_name_with_single_glob(spec: &BStr) -> Result<(), Error> {
155 let mut buf = smallvec::SmallVec::<[u8; 256]>::with_capacity(spec.len());
156 buf.extend_from_slice(spec);
157 let glob_pos = buf.find_byte(b'*').expect("glob present");
158 buf[glob_pos] = b'a';
159 gix_validate::reference::name_partial(buf.as_bstr())?;
160 Ok(())
161 }
162
163 fn validated(spec: Option<&BStr>, any_name: bool) -> Result<(Option<&BStr>, bool), Error> {
167 match spec {
168 Some(spec) => {
169 let glob_count = spec.iter().filter(|b| **b == b'*').take(2).count();
170 if glob_count > 1 {
171 return Err(Error::PatternUnsupported { pattern: spec.into() });
172 }
173 let has_globs = glob_count > 0;
174 if has_globs {
175 validate_partial_name_with_single_glob(spec)?;
176 } else if !any_name {
177 gix_validate::reference::name_partial(spec)?;
178 }
179 Ok((Some(spec), has_globs))
180 }
181 None => Ok((None, false)),
182 }
183 }
184}