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 (src, src_had_pattern) = validated(src, operation == Operation::Push && dst.is_some())?;
120 let (dst, dst_had_pattern) = validated(dst, false)?;
121 if mode != Mode::Negative && src_had_pattern != dst_had_pattern {
122 return Err(Error::PatternUnbalanced);
123 }
124
125 if mode == Mode::Negative {
126 match src {
127 Some(spec) => {
128 if src_had_pattern {
129 return Err(Error::NegativeGlobPattern);
130 } else if looks_like_object_hash(spec) {
131 return Err(Error::NegativeObjectHash);
132 } else if !spec.starts_with(b"refs/") && spec != "HEAD" {
133 return Err(Error::NegativePartialName);
134 }
135 }
136 None => return Err(Error::NegativeEmpty),
137 }
138 }
139
140 Ok(RefSpecRef {
141 op: operation,
142 mode,
143 src,
144 dst,
145 })
146 }
147
148 fn looks_like_object_hash(spec: &BStr) -> bool {
149 spec.len() >= gix_hash::Kind::shortest().len_in_hex() && spec.iter().all(u8::is_ascii_hexdigit)
150 }
151
152 fn validated(spec: Option<&BStr>, allow_revspecs: bool) -> Result<(Option<&BStr>, bool), Error> {
153 match spec {
154 Some(spec) => {
155 let glob_count = spec.iter().filter(|b| **b == b'*').take(2).count();
156 if glob_count > 1 {
157 return Err(Error::PatternUnsupported { pattern: spec.into() });
158 }
159 let has_globs = glob_count == 1;
160 if has_globs {
161 let mut buf = smallvec::SmallVec::<[u8; 256]>::with_capacity(spec.len());
162 buf.extend_from_slice(spec);
163 let glob_pos = buf.find_byte(b'*').expect("glob present");
164 buf[glob_pos] = b'a';
165 gix_validate::reference::name_partial(buf.as_bstr())?;
166 } else {
167 gix_validate::reference::name_partial(spec)
168 .map_err(Error::from)
169 .or_else(|err| {
170 if allow_revspecs {
171 gix_revision::spec::parse(spec, &mut super::revparse::Noop)?;
172 Ok(spec)
173 } else {
174 Err(err)
175 }
176 })?;
177 }
178 Ok((Some(spec), has_globs))
179 }
180 None => Ok((None, false)),
181 }
182 }
183}
184
185mod revparse {
186 use bstr::BStr;
187 use gix_revision::spec::parse::delegate::{
188 Kind, Navigate, PeelTo, PrefixHint, ReflogLookup, Revision, SiblingBranch, Traversal,
189 };
190
191 pub(crate) struct Noop;
192
193 impl Revision for Noop {
194 fn find_ref(&mut self, _name: &BStr) -> Option<()> {
195 Some(())
196 }
197
198 fn disambiguate_prefix(&mut self, _prefix: gix_hash::Prefix, _hint: Option<PrefixHint<'_>>) -> Option<()> {
199 Some(())
200 }
201
202 fn reflog(&mut self, _query: ReflogLookup) -> Option<()> {
203 Some(())
204 }
205
206 fn nth_checked_out_branch(&mut self, _branch_no: usize) -> Option<()> {
207 Some(())
208 }
209
210 fn sibling_branch(&mut self, _kind: SiblingBranch) -> Option<()> {
211 Some(())
212 }
213 }
214
215 impl Navigate for Noop {
216 fn traverse(&mut self, _kind: Traversal) -> Option<()> {
217 Some(())
218 }
219
220 fn peel_until(&mut self, _kind: PeelTo<'_>) -> Option<()> {
221 Some(())
222 }
223
224 fn find(&mut self, _regex: &BStr, _negated: bool) -> Option<()> {
225 Some(())
226 }
227
228 fn index_lookup(&mut self, _path: &BStr, _stage: u8) -> Option<()> {
229 Some(())
230 }
231 }
232
233 impl Kind for Noop {
234 fn kind(&mut self, _kind: gix_revision::spec::Kind) -> Option<()> {
235 Some(())
236 }
237 }
238
239 impl gix_revision::spec::parse::Delegate for Noop {
240 fn done(&mut self) {}
241 }
242}