radicle_surf/
glob.rs

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