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