git-ents.gitmain
⌘K
foforge
rev.rs184 lines · 6.2 KB · rusthistorycomment on this file
1//! `rev()` expressions over code refs (`query.rev`).
2
3use gix_hash::ObjectId;
4
5use crate::error::ParseError;
6use crate::pattern::RefPattern;
7
8/// One positive or negated term inside a `rev()` expression.
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub(crate) enum RevTerm {
11 /// A refname, exact (`refs/heads/main`) or short (`main`, resolved
12 /// through the standard gitrevisions lookup order).
13 Name(String),
14 /// A ref glob (`refs/heads/*`). Globs must be written in full
15 /// `refs/...` form.
16 Glob(RefPattern),
17 /// A full hex object id.
18 Oid(ObjectId),
19}
20
21impl RevTerm {
22 /// The refname patterns this term can be affected by — the term's
23 /// contribution to the query footprint (`query.footprint`). A short
24 /// name contributes every candidate of the lookup order, since a
25 /// transition on any of them can change what the name resolves to.
26 pub(crate) fn patterns(&self) -> Vec<RefPattern> {
27 match self {
28 Self::Name(name) => dwim_candidates(name)
29 .iter()
30 .filter_map(|c| RefPattern::new(c.clone()).ok())
31 .collect(),
32 Self::Glob(pattern) => vec![pattern.clone()],
33 Self::Oid(_) => Vec::new(),
34 }
35 }
36}
37
38/// The gitrevisions lookup order for a short refname, restricted to the
39/// namespaces a ref store serves (`refs/*`). A full `refs/...` name is
40/// its own single candidate.
41pub(crate) fn dwim_candidates(name: &str) -> Vec<String> {
42 if name.starts_with("refs/") {
43 vec![name.to_owned()]
44 } else {
45 vec![
46 format!("refs/{name}"),
47 format!("refs/tags/{name}"),
48 format!("refs/heads/{name}"),
49 format!("refs/remotes/{name}"),
50 ]
51 }
52}
53
54/// A parsed `rev()` expression: whitespace-separated terms, `^`-negated
55/// terms subtracted, `A..B` sugar for `^A B` — exactly the
56/// rev-list-shaped subset of gitrevisions(7) that `query.rev`
57/// normatively states.
58///
59/// The forms outside the subset (`~n`/`^n` suffixes, `A...B`, `@{...}`,
60/// `^{...}`, abbreviated hex) are an explicit [`ParseError`], never a
61/// silent empty or wrong set, exactly as the requirement demands;
62/// growing the subset is a compatible, additive extension.
63// @relation(query.rev, scope=file)
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct RevExpr {
66 include: Vec<RevTerm>,
67 exclude: Vec<RevTerm>,
68 raw: String,
69}
70
71impl RevExpr {
72 /// Parse the text between `rev(` and `)`.
73 pub(crate) fn parse(raw: &str) -> Result<Self, ParseError> {
74 let raw = raw.trim();
75 if raw.is_empty() {
76 return Err(ParseError::EmptyRev);
77 }
78 let mut include = Vec::new();
79 let mut exclude = Vec::new();
80 for token in raw.split_whitespace() {
81 if let Some(negated) = token.strip_prefix('^') {
82 exclude.push(parse_term(negated, token)?);
83 } else if token.contains("...") {
84 return Err(ParseError::UnsupportedRev {
85 token: token.to_owned(),
86 });
87 } else if let Some((base, tip)) = token.split_once("..") {
88 if base.is_empty() || tip.is_empty() {
89 return Err(ParseError::UnsupportedRev {
90 token: token.to_owned(),
91 });
92 }
93 exclude.push(parse_term(base, token)?);
94 include.push(parse_term(tip, token)?);
95 } else {
96 include.push(parse_term(token, token)?);
97 }
98 }
99 if include.is_empty() {
100 return Err(ParseError::NoPositiveRev);
101 }
102 Ok(Self {
103 include,
104 exclude,
105 raw: raw.to_owned(),
106 })
107 }
108
109 /// The expression text as written (trimmed), for display.
110 ///
111 /// # Examples
112 ///
113 /// ```
114 /// use ents_query::Query;
115 ///
116 /// let query: Query = "rev(main ^release)".parse().expect("valid");
117 /// let Query::Rev(expr) = query else { panic!("a rev atom") };
118 /// assert_eq!(expr.raw(), "main ^release");
119 /// ```
120 #[must_use]
121 pub fn raw(&self) -> &str {
122 &self.raw
123 }
124
125 pub(crate) fn include(&self) -> &[RevTerm] {
126 &self.include
127 }
128
129 pub(crate) fn exclude(&self) -> &[RevTerm] {
130 &self.exclude
131 }
132
133 /// Every pattern of every term, positive and negated: a transition
134 /// on a negated ref changes the denoted set too.
135 pub(crate) fn patterns(&self) -> Vec<RefPattern> {
136 self.include
137 .iter()
138 .chain(&self.exclude)
139 .flat_map(RevTerm::patterns)
140 .collect()
141 }
142}
143
144/// Parse one term, rejecting `refs/meta/*` shapes (`query.rev`) and
145/// every revspec operator outside the requirement's stated subset.
146// @relation(query.rev, scope=function)
147fn parse_term(term: &str, whole_token: &str) -> Result<RevTerm, ParseError> {
148 let unsupported = || ParseError::UnsupportedRev {
149 token: whole_token.to_owned(),
150 };
151 if term.is_empty() || term.contains(['~', ':', '@', '{', '}', '^']) {
152 return Err(unsupported());
153 }
154 if term.len() == 40 && term.bytes().all(|b| b.is_ascii_hexdigit()) {
155 let oid = ObjectId::from_hex(term.as_bytes()).map_err(|_e| unsupported())?;
156 return Ok(RevTerm::Oid(oid));
157 }
158 let meta = |pattern: &str| ParseError::MetaInRev {
159 pattern: pattern.to_owned(),
160 };
161 if term.contains('*') {
162 if !term.starts_with("refs/") {
163 return Err(unsupported());
164 }
165 let pattern = RefPattern::new(term).map_err(|_e| unsupported())?;
166 // Rejected only when the pattern *names* the meta namespace; a
167 // broad glob like `refs/*` is legal because `refs/meta/*` is
168 // outside rev()'s domain by definition and is excluded at
169 // evaluation, not silently matched (`query.rev`).
170 if pattern.literal_prefix().starts_with("refs/meta") {
171 return Err(meta(term));
172 }
173 return Ok(RevTerm::Glob(pattern));
174 }
175 // Charset sanity via the pattern validator (no wildcard present).
176 let _validated = RefPattern::new(term).map_err(|_e| unsupported())?;
177 if dwim_candidates(term)
178 .iter()
179 .any(|c| c.starts_with("refs/meta"))
180 {
181 return Err(meta(term));
182 }
183 Ok(RevTerm::Name(term.to_owned()))
184}