Skip to main content

gix_submodule/
access.rs

1use std::{borrow::Cow, collections::HashSet, path::Path};
2
3use bstr::BStr;
4
5use crate::{
6    File, IsActivePlatform, config,
7    config::{Branch, FetchRecurse, Ignore, Update},
8};
9
10/// High-Level Access
11///
12/// Note that all methods perform validation of the requested value and report issues right away.
13/// If a bypass is needed, use [`config()`](File::config()) for direct access.
14impl File {
15    /// Return the underlying configuration file.
16    ///
17    /// Note that it might have been merged with values from another configuration file and may
18    /// thus not be accurately reflecting that state of a `.gitmodules` file anymore.
19    pub fn config(&self) -> &gix_config::File<'static> {
20        &self.config
21    }
22
23    /// Return the path at which the `.gitmodules` file lives, if it is known.
24    pub fn config_path(&self) -> Option<&Path> {
25        self.config.sections().filter_map(|s| s.meta().path.as_deref()).next()
26    }
27
28    /// Return the unvalidated names of the submodules for which configuration is present.
29    ///
30    /// Note that these exact names have to be used for querying submodule values.
31    pub fn names(&self) -> impl Iterator<Item = &BStr> {
32        let mut seen = HashSet::<&BStr>::default();
33        self.config
34            .sections_by_name("submodule")
35            .into_iter()
36            .flatten()
37            .filter_map(move |s| {
38                s.header()
39                    .subsection_name()
40                    .filter(|_| s.meta().source == crate::init::META_MARKER)
41                    .filter(|name| seen.insert(*name))
42            })
43    }
44
45    /// Similar to [Self::is_active_platform()], but automatically applies it to each name to learn if a submodule is active or not.
46    pub fn names_and_active_state<'a>(
47        &'a self,
48        config: &'a gix_config::File<'static>,
49        defaults: gix_pathspec::Defaults,
50        attributes: &'a mut (
51                    dyn FnMut(
52            &BStr,
53            gix_pathspec::attributes::glob::pattern::Case,
54            bool,
55            &mut gix_pathspec::attributes::search::Outcome,
56        ) -> bool
57                        + 'a
58                ),
59    ) -> Result<
60        impl Iterator<Item = (&'a BStr, Result<bool, gix_config::value::Error>)> + 'a,
61        crate::is_active_platform::Error,
62    > {
63        let mut platform = self.is_active_platform(config, defaults)?;
64        let iter = self
65            .names()
66            .map(move |name| (name, platform.is_active(config, name, attributes)));
67        Ok(iter)
68    }
69
70    /// Return a platform which allows to check if a submodule name is active or inactive.
71    /// Use `defaults` for parsing the pathspecs used to later match on names via `submodule.active` configuration retrieved from `config`.
72    ///
73    /// All `submodule.active` pathspecs are considered to be top-level specs and match the name of submodules, which are active
74    /// on inclusive match.
75    /// The full algorithm is described as [hierarchy of rules](https://git-scm.com/docs/gitsubmodules#_active_submodules).
76    pub fn is_active_platform(
77        &self,
78        config: &gix_config::File<'_>,
79        defaults: gix_pathspec::Defaults,
80    ) -> Result<IsActivePlatform, crate::is_active_platform::Error> {
81        let search = config
82            .strings("submodule.active")
83            .map(|patterns| -> Result<_, crate::is_active_platform::Error> {
84                let patterns = patterns
85                    .into_iter()
86                    .map(|pattern| gix_pathspec::parse(&pattern, defaults))
87                    .collect::<Result<Vec<_>, _>>()?;
88                Ok(gix_pathspec::Search::from_specs(
89                    patterns,
90                    None,
91                    std::path::Path::new(""),
92                )?)
93            })
94            .transpose()?;
95        Ok(IsActivePlatform { search })
96    }
97
98    /// Given the `relative_path` (as seen from the root of the worktree) of a submodule with possibly platform-specific
99    /// component separators, find the submodule's name associated with this path, or `None` if none was found.
100    ///
101    /// Note that this does a linear search and compares `relative_path` in a normalized form to the same form of the path
102    /// associated with the submodule.
103    pub fn name_by_path(&self, relative_path: &BStr) -> Option<&BStr> {
104        self.names()
105            .filter_map(|n| self.path(n).ok().map(|p| (n, p)))
106            .find_map(|(n, p)| (p == relative_path).then_some(n))
107    }
108}
109
110/// Per-Submodule Access
111impl File {
112    /// Return the path relative to the root directory of the working tree at which the submodule is expected to be checked out.
113    /// It's an error if the path doesn't exist as it's the only way to associate a path in the index with additional submodule
114    /// information, like the URL to fetch from.
115    ///
116    /// ### Deviation
117    ///
118    /// Git currently allows absolute paths to be used when adding submodules, but fails later as it can't find the submodule by
119    /// relative path anymore. Let's play it safe here.
120    pub fn path(&self, name: &BStr) -> Result<Cow<'_, BStr>, config::path::Error> {
121        let path_bstr =
122            self.config
123                .string(&format!("submodule.{name}.path"))
124                .ok_or_else(|| config::path::Error::Missing {
125                    submodule: name.to_owned(),
126                })?;
127        if path_bstr.is_empty() {
128            return Err(config::path::Error::Missing {
129                submodule: name.to_owned(),
130            });
131        }
132        let path = gix_path::from_bstr(path_bstr.as_ref());
133        if path.is_absolute() {
134            return Err(config::path::Error::Absolute {
135                submodule: name.to_owned(),
136                actual: path_bstr.into_owned(),
137            });
138        }
139        if gix_path::normalize(path, "".as_ref()).is_none() {
140            return Err(config::path::Error::OutsideOfWorktree {
141                submodule: name.to_owned(),
142                actual: path_bstr.into_owned(),
143            });
144        }
145        Ok(path_bstr)
146    }
147
148    /// Retrieve the `url` field of the submodule named `name`. It's an error if it doesn't exist or is empty.
149    pub fn url(&self, name: &BStr) -> Result<gix_url::Url, config::url::Error> {
150        let url = self
151            .config
152            .string(&format!("submodule.{name}.url"))
153            .ok_or_else(|| config::url::Error::Missing {
154                submodule: name.to_owned(),
155            })?;
156
157        if url.is_empty() {
158            return Err(config::url::Error::Missing {
159                submodule: name.to_owned(),
160            });
161        }
162        gix_url::Url::from_bytes(url.as_ref()).map_err(|err| config::url::Error::Parse {
163            submodule: name.to_owned(),
164            source: err,
165        })
166    }
167
168    /// Retrieve the `update` field of the submodule named `name`, if present.
169    pub fn update(&self, name: &BStr) -> Result<Option<Update>, config::update::Error> {
170        let mut value_is_from_modules_file = None;
171        let our_meta = self.config.meta();
172        let value: Update = match self.config.string_filter(&format!("submodule.{name}.update"), |meta| {
173            value_is_from_modules_file = Some(std::ptr::eq(meta, our_meta));
174            true
175        }) {
176            Some(v) => v.as_ref().try_into().map_err(|()| config::update::Error::Invalid {
177                submodule: name.to_owned(),
178                actual: v.into_owned(),
179            })?,
180            None => return Ok(None),
181        };
182
183        if let Update::Command(cmd) = &value {
184            if value_is_from_modules_file.unwrap_or_default() {
185                return Err(config::update::Error::CommandForbiddenInModulesConfiguration {
186                    submodule: name.to_owned(),
187                    actual: cmd.to_owned(),
188                });
189            }
190        }
191        Ok(Some(value))
192    }
193
194    /// Retrieve the `branch` field of the submodule named `name`, or `None` if unset.
195    ///
196    /// Note that `Default` is implemented for [`Branch`].
197    pub fn branch(&self, name: &BStr) -> Result<Option<Branch>, config::branch::Error> {
198        let branch = match self.config.string(&format!("submodule.{name}.branch")) {
199            Some(v) => v,
200            None => return Ok(None),
201        };
202
203        Branch::try_from(branch.as_ref())
204            .map(Some)
205            .map_err(|err| config::branch::Error {
206                submodule: name.to_owned(),
207                actual: branch.into_owned(),
208                source: err,
209            })
210    }
211
212    /// Retrieve the `fetchRecurseSubmodules` field of the submodule named `name`, or `None` if unset.
213    ///
214    /// Note that if it's unset, it should be retrieved from `fetch.recurseSubmodules` in the configuration.
215    pub fn fetch_recurse(&self, name: &BStr) -> Result<Option<FetchRecurse>, config::Error> {
216        self.config
217            .boolean(&format!("submodule.{name}.fetchRecurseSubmodules"))
218            .map(FetchRecurse::new)
219            .transpose()
220            .map_err(|value| config::Error {
221                field: "fetchRecurseSubmodules",
222                submodule: name.to_owned(),
223                actual: value,
224            })
225    }
226
227    /// Retrieve the `ignore` field of the submodule named `name`, or `None` if unset.
228    pub fn ignore(&self, name: &BStr) -> Result<Option<Ignore>, config::Error> {
229        self.config
230            .string(&format!("submodule.{name}.ignore"))
231            .map(|value| {
232                Ignore::try_from(value.as_ref()).map_err(|()| config::Error {
233                    field: "ignore",
234                    submodule: name.to_owned(),
235                    actual: value.into_owned(),
236                })
237            })
238            .transpose()
239    }
240
241    /// Retrieve the `shallow` field of the submodule named `name`, or `None` if unset.
242    ///
243    /// If `true`, the submodule will be checked out with `depth = 1`. If unset, `false` is assumed.
244    pub fn shallow(&self, name: &BStr) -> Result<Option<bool>, gix_config::value::Error> {
245        self.config.boolean(&format!("submodule.{name}.shallow")).transpose()
246    }
247}