Skip to main content

radicle_surf/
glob.rs

1use std::marker::PhantomData;
2
3use git_ext::ref_format::{
4    self, refname,
5    refspec::{self, PatternString, QualifiedPattern},
6    Qualified, RefStr, RefString,
7};
8use thiserror::Error;
9
10use crate::{Branch, Local, Namespace, Remote, Tag};
11
12#[derive(Debug, Error)]
13pub enum Error {
14    #[error(transparent)]
15    RefFormat(#[from] ref_format::Error),
16}
17
18/// A collection of globs for a git reference type.
19#[derive(Clone, Debug)]
20pub struct Glob<T> {
21    globs: Vec<QualifiedPattern<'static>>,
22    glob_type: PhantomData<T>, // To support different methods for different T.
23}
24
25impl<T> Default for Glob<T> {
26    fn default() -> Self {
27        Self {
28            globs: Default::default(),
29            glob_type: PhantomData,
30        }
31    }
32}
33
34impl<T> Glob<T> {
35    /// Return the [`QualifiedPattern`] globs of this `Glob`.
36    pub fn globs(&self) -> impl Iterator<Item = &QualifiedPattern<'static>> {
37        self.globs.iter()
38    }
39
40    /// Combine two `Glob`s together by combining their glob lists together.
41    ///
42    /// Note that the `Glob`s must result in the same type,
43    /// e.g. `Glob<Tag>` can only combine with `Glob<Tag>`,
44    /// `Glob<Local>` can combine with `Glob<Remote>`, etc.
45    pub fn and(mut self, other: impl Into<Self>) -> Self {
46        self.globs.extend(other.into().globs);
47        self
48    }
49}
50
51impl Glob<Namespace> {
52    /// Creates the `Glob` that matches all `refs/namespaces`.
53    pub fn all_namespaces() -> Self {
54        Self::namespaces(refspec::pattern!("*"))
55    }
56
57    /// Creates a `Glob` for `refs/namespaces`, starting with `glob`.
58    pub fn namespaces(glob: PatternString) -> Self {
59        let globs = vec![Self::qualify(glob)];
60        Self {
61            globs,
62            glob_type: PhantomData,
63        }
64    }
65
66    /// Adds a `refs/namespaces` pattern to this `Glob`.
67    pub fn insert(mut self, glob: PatternString) -> Self {
68        self.globs.push(Self::qualify(glob));
69        self
70    }
71
72    fn qualify(glob: PatternString) -> QualifiedPattern<'static> {
73        qualify(&refname!("refs/namespaces"), glob).expect("BUG: pattern is qualified")
74    }
75}
76
77impl FromIterator<PatternString> for Glob<Namespace> {
78    fn from_iter<T: IntoIterator<Item = PatternString>>(iter: T) -> Self {
79        let globs = iter
80            .into_iter()
81            .map(|pat| {
82                qualify(&refname!("refs/namespaces"), pat).expect("BUG: pattern is qualified")
83            })
84            .collect();
85
86        Self {
87            globs,
88            glob_type: PhantomData,
89        }
90    }
91}
92
93impl Extend<PatternString> for Glob<Namespace> {
94    fn extend<T: IntoIterator<Item = PatternString>>(&mut self, iter: T) {
95        self.globs.extend(iter.into_iter().map(|pat| {
96            qualify(&refname!("refs/namespaces"), pat).expect("BUG: pattern is qualified")
97        }))
98    }
99}
100
101impl Glob<Tag> {
102    /// Creates a `Glob` that matches all `refs/tags`.
103    pub fn all_tags() -> Self {
104        Self::tags(refspec::pattern!("*"))
105    }
106
107    /// Creates a `Glob` for `refs/tags`, starting with `glob`.
108    pub fn tags(glob: PatternString) -> Self {
109        let globs = vec![Self::qualify(glob)];
110        Self {
111            globs,
112            glob_type: PhantomData,
113        }
114    }
115
116    /// Adds a `refs/tags` pattern to this `Glob`.
117    pub fn insert(mut self, glob: PatternString) -> Self {
118        self.globs.push(Self::qualify(glob));
119        self
120    }
121
122    fn qualify(glob: PatternString) -> QualifiedPattern<'static> {
123        qualify(&refname!("refs/tags"), glob).expect("BUG: pattern is qualified")
124    }
125}
126
127impl FromIterator<PatternString> for Glob<Tag> {
128    fn from_iter<T: IntoIterator<Item = PatternString>>(iter: T) -> Self {
129        let globs = iter
130            .into_iter()
131            .map(|pat| qualify(&refname!("refs/tags"), pat).expect("BUG: pattern is qualified"))
132            .collect();
133
134        Self {
135            globs,
136            glob_type: PhantomData,
137        }
138    }
139}
140
141impl Extend<PatternString> for Glob<Tag> {
142    fn extend<T: IntoIterator<Item = PatternString>>(&mut self, iter: T) {
143        self.globs.extend(
144            iter.into_iter()
145                .map(|pat| qualify(&refname!("refs/tag"), pat).expect("BUG: pattern is qualified")),
146        )
147    }
148}
149
150impl Glob<Local> {
151    /// Creates the `Glob` that matches all `refs/heads`.
152    pub fn all_heads() -> Self {
153        Self::heads(refspec::pattern!("*"))
154    }
155
156    /// Creates a `Glob` for `refs/heads`, starting with `glob`.
157    pub fn heads(glob: PatternString) -> Self {
158        let globs = vec![Self::qualify_heads(glob)];
159        Self {
160            globs,
161            glob_type: PhantomData,
162        }
163    }
164
165    /// Adds a `refs/heads` pattern to this `Glob`.
166    pub fn insert(mut self, glob: PatternString) -> Self {
167        self.globs.push(Self::qualify_heads(glob));
168        self
169    }
170
171    /// When chaining `Glob<Local>` with `Glob<Remote>`, use
172    /// `branches` to convert this `Glob<Local>` into a
173    /// `Glob<Branch>`.
174    ///
175    /// # Example
176    /// ```no_run
177    /// Glob::heads(pattern!("features/*"))
178    ///     .insert(pattern!("qa/*"))
179    ///     .branches()
180    ///     .and(Glob::remotes(pattern!("origin/features/*")))
181    /// ```
182    pub fn branches(self) -> Glob<Branch> {
183        self.into()
184    }
185
186    fn qualify_heads(glob: PatternString) -> QualifiedPattern<'static> {
187        qualify(&refname!("refs/heads"), glob).expect("BUG: pattern is qualified")
188    }
189}
190
191impl FromIterator<PatternString> for Glob<Local> {
192    fn from_iter<T: IntoIterator<Item = PatternString>>(iter: T) -> Self {
193        let globs = iter
194            .into_iter()
195            .map(|pat| qualify(&refname!("refs/heads"), pat).expect("BUG: pattern is qualified"))
196            .collect();
197
198        Self {
199            globs,
200            glob_type: PhantomData,
201        }
202    }
203}
204
205impl Extend<PatternString> for Glob<Local> {
206    fn extend<T: IntoIterator<Item = PatternString>>(&mut self, iter: T) {
207        self.globs.extend(
208            iter.into_iter().map(|pat| {
209                qualify(&refname!("refs/heads"), pat).expect("BUG: pattern is qualified")
210            }),
211        )
212    }
213}
214
215impl From<Glob<Local>> for Glob<Branch> {
216    fn from(Glob { globs, .. }: Glob<Local>) -> Self {
217        Self {
218            globs,
219            glob_type: PhantomData,
220        }
221    }
222}
223
224impl Glob<Remote> {
225    /// Creates the `Glob` that matches all `refs/remotes`.
226    pub fn all_remotes() -> Self {
227        Self::remotes(refspec::pattern!("*"))
228    }
229
230    /// Creates a `Glob` for `refs/remotes`, starting with `glob`.
231    pub fn remotes(glob: PatternString) -> Self {
232        let globs = vec![Self::qualify_remotes(glob)];
233        Self {
234            globs,
235            glob_type: PhantomData,
236        }
237    }
238
239    /// Adds a `refs/remotes` pattern to this `Glob`.
240    pub fn insert(mut self, glob: PatternString) -> Self {
241        self.globs.push(Self::qualify_remotes(glob));
242        self
243    }
244
245    /// When chaining `Glob<Remote>` with `Glob<Local>`, use
246    /// `branches` to convert this `Glob<Remote>` into a
247    /// `Glob<Branch>`.
248    ///
249    /// # Example
250    /// ```no_run
251    /// Glob::remotes(pattern!("origin/features/*"))
252    ///     .insert(pattern!("origin/qa/*"))
253    ///     .branches()
254    ///     .and(Glob::heads(pattern!("features/*")))
255    /// ```
256    pub fn branches(self) -> Glob<Branch> {
257        self.into()
258    }
259
260    fn qualify_remotes(glob: PatternString) -> QualifiedPattern<'static> {
261        qualify(&refname!("refs/remotes"), glob).expect("BUG: pattern is qualified")
262    }
263}
264
265impl FromIterator<PatternString> for Glob<Remote> {
266    fn from_iter<T: IntoIterator<Item = PatternString>>(iter: T) -> Self {
267        let globs = iter
268            .into_iter()
269            .map(|pat| qualify(&refname!("refs/remotes"), pat).expect("BUG: pattern is qualified"))
270            .collect();
271
272        Self {
273            globs,
274            glob_type: PhantomData,
275        }
276    }
277}
278
279impl Extend<PatternString> for Glob<Remote> {
280    fn extend<T: IntoIterator<Item = PatternString>>(&mut self, iter: T) {
281        self.globs.extend(
282            iter.into_iter().map(|pat| {
283                qualify(&refname!("refs/remotes"), pat).expect("BUG: pattern is qualified")
284            }),
285        )
286    }
287}
288
289impl From<Glob<Remote>> for Glob<Branch> {
290    fn from(Glob { globs, .. }: Glob<Remote>) -> Self {
291        Self {
292            globs,
293            glob_type: PhantomData,
294        }
295    }
296}
297
298impl Glob<Qualified<'_>> {
299    pub fn all_category<R: AsRef<RefStr>>(category: R) -> Self {
300        Self {
301            globs: vec![Self::qualify_category(category, refspec::pattern!("*"))],
302            glob_type: PhantomData,
303        }
304    }
305
306    /// Creates a `Glob` for `refs/<category>`, starting with `glob`.
307    pub fn categories<R>(category: R, glob: PatternString) -> Self
308    where
309        R: AsRef<RefStr>,
310    {
311        let globs = vec![Self::qualify_category(category, glob)];
312        Self {
313            globs,
314            glob_type: PhantomData,
315        }
316    }
317
318    /// Adds a `refs/<category>` pattern to this `Glob`.
319    pub fn insert<R>(mut self, category: R, glob: PatternString) -> Self
320    where
321        R: AsRef<RefStr>,
322    {
323        self.globs.push(Self::qualify_category(category, glob));
324        self
325    }
326
327    fn qualify_category<R>(category: R, glob: PatternString) -> QualifiedPattern<'static>
328    where
329        R: AsRef<RefStr>,
330    {
331        let prefix = refname!("refs").and(category);
332        qualify(&prefix, glob).expect("BUG: pattern is qualified")
333    }
334}
335
336fn qualify(prefix: &RefString, glob: PatternString) -> Option<QualifiedPattern<'static>> {
337    prefix.to_pattern(glob).qualified().map(|q| q.into_owned())
338}