1#[derive(Debug, thiserror::Error)]
3#[allow(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 be object hashes")]
12 NegativeObjectHash,
13 #[error("Negative specs must be full ref names, starting with \"refs/\"")]
14 NegativePartialName,
15 #[error("Negative glob patterns are not allowed")]
16 NegativeGlobPattern,
17 #[error("Fetch destinations must be ref-names, like 'HEAD:refs/heads/branch'")]
18 InvalidFetchDestination,
19 #[error("Cannot push into an empty destination")]
20 PushToEmpty,
21 #[error("glob patterns may only involved a single '*' character, found {pattern:?}")]
22 PatternUnsupported { pattern: bstr::BString },
23 #[error("Both sides of the specification need a pattern, like 'a/*:b/*'")]
24 PatternUnbalanced,
25 #[error(transparent)]
26 ReferenceName(#[from] gix_validate::reference::name::Error),
27 #[error(transparent)]
28 RevSpec(#[from] gix_revision::spec::parse::Error),
29}
30
31#[derive(PartialOrd, Ord, PartialEq, Eq, Copy, Clone, Hash, Debug)]
33pub enum Operation {
34 Push,
36 Fetch,
38}
39
40pub(crate) mod function {
41 use bstr::{BStr, ByteSlice};
42
43 use crate::{
44 parse::{Error, Operation},
45 types::Mode,
46 RefSpecRef,
47 };
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.find_byte(b':') {
79 Some(pos) => {
80 if mode == Mode::Negative {
81 return Err(Error::NegativeWithDestination);
82 }
83
84 let (src, dst) = spec.split_at(pos);
85 let dst = &dst[1..];
86 let src = (!src.is_empty()).then(|| src.as_bstr());
87 let dst = (!dst.is_empty()).then(|| dst.as_bstr());
88 match (src, dst) {
89 (None, None) => match operation {
90 Operation::Push => (None, None),
91 Operation::Fetch => (Some("HEAD".into()), None),
92 },
93 (None, Some(dst)) => match operation {
94 Operation::Push => (None, Some(dst)),
95 Operation::Fetch => (Some("HEAD".into()), Some(dst)),
96 },
97 (Some(src), None) => match operation {
98 Operation::Push => return Err(Error::PushToEmpty),
99 Operation::Fetch => (Some(src), None),
100 },
101 (Some(src), Some(dst)) => (Some(src), Some(dst)),
102 }
103 }
104 None => {
105 let src = (!spec.is_empty()).then_some(spec);
106 if Operation::Fetch == operation && mode != Mode::Negative && src.is_none() {
107 return Ok(fetch_head_only(mode));
108 } else {
109 (src, None)
110 }
111 }
112 };
113
114 if let Some(spec) = src.as_mut() {
115 if *spec == "@" {
116 *spec = "HEAD".into();
117 }
118 }
119 let is_one_sided = dst.is_none();
120 let (src, src_had_pattern) = validated(src, operation == Operation::Push && dst.is_some(), is_one_sided)?;
121 let (dst, dst_had_pattern) = validated(dst, false, false)?;
122 if !is_one_sided && mode != Mode::Negative && src_had_pattern != dst_had_pattern {
124 return Err(Error::PatternUnbalanced);
125 }
126
127 if mode == Mode::Negative {
128 match src {
129 Some(spec) => {
130 if src_had_pattern {
131 return Err(Error::NegativeGlobPattern);
132 } else if looks_like_object_hash(spec) {
133 return Err(Error::NegativeObjectHash);
134 } else if !spec.starts_with(b"refs/") && spec != "HEAD" {
135 return Err(Error::NegativePartialName);
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 validated(
155 spec: Option<&BStr>,
156 allow_revspecs: bool,
157 is_one_sided: bool,
158 ) -> Result<(Option<&BStr>, bool), Error> {
159 match spec {
160 Some(spec) => {
161 let glob_count = spec.iter().filter(|b| **b == b'*').take(2).count();
162 if glob_count > 1 {
163 if !is_one_sided {
165 return Err(Error::PatternUnsupported { pattern: spec.into() });
166 }
167 }
168 let has_globs = glob_count > 0;
170 if has_globs {
171 if !is_one_sided {
173 let mut buf = smallvec::SmallVec::<[u8; 256]>::with_capacity(spec.len());
174 buf.extend_from_slice(spec);
175 let glob_pos = buf.find_byte(b'*').expect("glob present");
176 buf[glob_pos] = b'a';
177 gix_validate::reference::name_partial(buf.as_bstr())?;
178 }
179 } else {
180 gix_validate::reference::name_partial(spec)
181 .map_err(Error::from)
182 .or_else(|err| {
183 if allow_revspecs {
184 gix_revision::spec::parse(spec, &mut super::revparse::Noop)?;
185 Ok(spec)
186 } else {
187 Err(err)
188 }
189 })?;
190 }
191 Ok((Some(spec), has_globs))
192 }
193 None => Ok((None, false)),
194 }
195 }
196}
197
198mod revparse {
199 use bstr::BStr;
200 use gix_revision::spec::parse::delegate::{
201 Kind, Navigate, PeelTo, PrefixHint, ReflogLookup, Revision, SiblingBranch, Traversal,
202 };
203
204 pub(crate) struct Noop;
205
206 impl Revision for Noop {
207 fn find_ref(&mut self, _name: &BStr) -> Option<()> {
208 Some(())
209 }
210
211 fn disambiguate_prefix(&mut self, _prefix: gix_hash::Prefix, _hint: Option<PrefixHint<'_>>) -> Option<()> {
212 Some(())
213 }
214
215 fn reflog(&mut self, _query: ReflogLookup) -> Option<()> {
216 Some(())
217 }
218
219 fn nth_checked_out_branch(&mut self, _branch_no: usize) -> Option<()> {
220 Some(())
221 }
222
223 fn sibling_branch(&mut self, _kind: SiblingBranch) -> Option<()> {
224 Some(())
225 }
226 }
227
228 impl Navigate for Noop {
229 fn traverse(&mut self, _kind: Traversal) -> Option<()> {
230 Some(())
231 }
232
233 fn peel_until(&mut self, _kind: PeelTo<'_>) -> Option<()> {
234 Some(())
235 }
236
237 fn find(&mut self, _regex: &BStr, _negated: bool) -> Option<()> {
238 Some(())
239 }
240
241 fn index_lookup(&mut self, _path: &BStr, _stage: u8) -> Option<()> {
242 Some(())
243 }
244 }
245
246 impl Kind for Noop {
247 fn kind(&mut self, _kind: gix_revision::spec::Kind) -> Option<()> {
248 Some(())
249 }
250 }
251
252 impl gix_revision::spec::parse::Delegate for Noop {
253 fn done(&mut self) {}
254 }
255}