gix_submodule/
access.rs

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