gix_submodule/
access.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
use std::{borrow::Cow, collections::HashSet, path::Path};

use bstr::BStr;

use crate::{
    config,
    config::{Branch, FetchRecurse, Ignore, Update},
    File, IsActivePlatform,
};

/// High-Level Access
///
/// Note that all methods perform validation of the requested value and report issues right away.
/// If a bypass is needed, use [`config()`](File::config()) for direct access.
impl File {
    /// Return the underlying configuration file.
    ///
    /// Note that it might have been merged with values from another configuration file and may
    /// thus not be accurately reflecting that state of a `.gitmodules` file anymore.
    pub fn config(&self) -> &gix_config::File<'static> {
        &self.config
    }

    /// Return the path at which the `.gitmodules` file lives, if it is known.
    pub fn config_path(&self) -> Option<&Path> {
        self.config.sections().filter_map(|s| s.meta().path.as_deref()).next()
    }

    /// Return the unvalidated names of the submodules for which configuration is present.
    ///
    /// Note that these exact names have to be used for querying submodule values.
    pub fn names(&self) -> impl Iterator<Item = &BStr> {
        let mut seen = HashSet::<&BStr>::default();
        self.config
            .sections_by_name("submodule")
            .into_iter()
            .flatten()
            .filter_map(move |s| {
                s.header()
                    .subsection_name()
                    .filter(|_| s.meta().source == crate::init::META_MARKER)
                    .filter(|name| seen.insert(*name))
            })
    }

    /// Similar to [Self::is_active_platform()], but automatically applies it to each name to learn if a submodule is active or not.
    pub fn names_and_active_state<'a>(
        &'a self,
        config: &'a gix_config::File<'static>,
        defaults: gix_pathspec::Defaults,
        attributes: &'a mut (dyn FnMut(
            &BStr,
            gix_pathspec::attributes::glob::pattern::Case,
            bool,
            &mut gix_pathspec::attributes::search::Outcome,
        ) -> bool
                     + 'a),
    ) -> Result<
        impl Iterator<Item = (&'a BStr, Result<bool, gix_config::value::Error>)> + 'a,
        crate::is_active_platform::Error,
    > {
        let mut platform = self.is_active_platform(config, defaults)?;
        let iter = self
            .names()
            .map(move |name| (name, platform.is_active(config, name, attributes)));
        Ok(iter)
    }

    /// Return a platform which allows to check if a submodule name is active or inactive.
    /// Use `defaults` for parsing the pathspecs used to later match on names via `submodule.active` configuration retrieved from `config`.
    ///
    /// All `submodule.active` pathspecs are considered to be top-level specs and match the name of submodules, which are active
    /// on inclusive match.
    /// The full algorithm is described as [hierarchy of rules](https://git-scm.com/docs/gitsubmodules#_active_submodules).
    pub fn is_active_platform(
        &self,
        config: &gix_config::File<'_>,
        defaults: gix_pathspec::Defaults,
    ) -> Result<IsActivePlatform, crate::is_active_platform::Error> {
        let search = config
            .strings("submodule.active")
            .map(|patterns| -> Result<_, crate::is_active_platform::Error> {
                let patterns = patterns
                    .into_iter()
                    .map(|pattern| gix_pathspec::parse(&pattern, defaults))
                    .collect::<Result<Vec<_>, _>>()?;
                Ok(gix_pathspec::Search::from_specs(
                    patterns,
                    None,
                    std::path::Path::new(""),
                )?)
            })
            .transpose()?;
        Ok(IsActivePlatform { search })
    }

    /// Given the `relative_path` (as seen from the root of the worktree) of a submodule with possibly platform-specific
    /// component separators, find the submodule's name associated with this path, or `None` if none was found.
    ///
    /// Note that this does a linear search and compares `relative_path` in a normalized form to the same form of the path
    /// associated with the submodule.
    pub fn name_by_path(&self, relative_path: &BStr) -> Option<&BStr> {
        self.names()
            .filter_map(|n| self.path(n).ok().map(|p| (n, p)))
            .find_map(|(n, p)| (p == relative_path).then_some(n))
    }
}

/// Per-Submodule Access
impl File {
    /// Return the path relative to the root directory of the working tree at which the submodule is expected to be checked out.
    /// 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
    /// information, like the URL to fetch from.
    ///
    /// ### Deviation
    ///
    /// Git currently allows absolute paths to be used when adding submodules, but fails later as it can't find the submodule by
    /// relative path anymore. Let's play it safe here.
    pub fn path(&self, name: &BStr) -> Result<Cow<'_, BStr>, config::path::Error> {
        let path_bstr =
            self.config
                .string(format!("submodule.{name}.path"))
                .ok_or_else(|| config::path::Error::Missing {
                    submodule: name.to_owned(),
                })?;
        if path_bstr.is_empty() {
            return Err(config::path::Error::Missing {
                submodule: name.to_owned(),
            });
        }
        let path = gix_path::from_bstr(path_bstr.as_ref());
        if path.is_absolute() {
            return Err(config::path::Error::Absolute {
                submodule: name.to_owned(),
                actual: path_bstr.into_owned(),
            });
        }
        if gix_path::normalize(path, "".as_ref()).is_none() {
            return Err(config::path::Error::OutsideOfWorktree {
                submodule: name.to_owned(),
                actual: path_bstr.into_owned(),
            });
        }
        Ok(path_bstr)
    }

    /// Retrieve the `url` field of the submodule named `name`. It's an error if it doesn't exist or is empty.
    pub fn url(&self, name: &BStr) -> Result<gix_url::Url, config::url::Error> {
        let url = self
            .config
            .string(format!("submodule.{name}.url"))
            .ok_or_else(|| config::url::Error::Missing {
                submodule: name.to_owned(),
            })?;

        if url.is_empty() {
            return Err(config::url::Error::Missing {
                submodule: name.to_owned(),
            });
        }
        gix_url::Url::from_bytes(url.as_ref()).map_err(|err| config::url::Error::Parse {
            submodule: name.to_owned(),
            source: err,
        })
    }

    /// Retrieve the `update` field of the submodule named `name`, if present.
    pub fn update(&self, name: &BStr) -> Result<Option<Update>, config::update::Error> {
        let value: Update = match self.config.string(format!("submodule.{name}.update")) {
            Some(v) => v.as_ref().try_into().map_err(|()| config::update::Error::Invalid {
                submodule: name.to_owned(),
                actual: v.into_owned(),
            })?,
            None => return Ok(None),
        };

        if let Update::Command(cmd) = &value {
            let ours = self.config.meta();
            let has_value_from_foreign_section = self
                .config
                .sections_by_name("submodule")
                .into_iter()
                .flatten()
                .any(|s| (s.header().subsection_name() == Some(name) && s.meta() as *const _ != ours as *const _));
            if !has_value_from_foreign_section {
                return Err(config::update::Error::CommandForbiddenInModulesConfiguration {
                    submodule: name.to_owned(),
                    actual: cmd.to_owned(),
                });
            }
        }
        Ok(Some(value))
    }

    /// Retrieve the `branch` field of the submodule named `name`, or `None` if unset.
    ///
    /// Note that `Default` is implemented for [`Branch`].
    pub fn branch(&self, name: &BStr) -> Result<Option<Branch>, config::branch::Error> {
        let branch = match self.config.string(format!("submodule.{name}.branch")) {
            Some(v) => v,
            None => return Ok(None),
        };

        Branch::try_from(branch.as_ref())
            .map(Some)
            .map_err(|err| config::branch::Error {
                submodule: name.to_owned(),
                actual: branch.into_owned(),
                source: err,
            })
    }

    /// Retrieve the `fetchRecurseSubmodules` field of the submodule named `name`, or `None` if unset.
    ///
    /// Note that if it's unset, it should be retrieved from `fetch.recurseSubmodules` in the configuration.
    pub fn fetch_recurse(&self, name: &BStr) -> Result<Option<FetchRecurse>, config::Error> {
        self.config
            .boolean(format!("submodule.{name}.fetchRecurseSubmodules"))
            .map(FetchRecurse::new)
            .transpose()
            .map_err(|value| config::Error {
                field: "fetchRecurseSubmodules",
                submodule: name.to_owned(),
                actual: value,
            })
    }

    /// Retrieve the `ignore` field of the submodule named `name`, or `None` if unset.
    pub fn ignore(&self, name: &BStr) -> Result<Option<Ignore>, config::Error> {
        self.config
            .string(format!("submodule.{name}.ignore"))
            .map(|value| {
                Ignore::try_from(value.as_ref()).map_err(|()| config::Error {
                    field: "ignore",
                    submodule: name.to_owned(),
                    actual: value.into_owned(),
                })
            })
            .transpose()
    }

    /// Retrieve the `shallow` field of the submodule named `name`, or `None` if unset.
    ///
    /// If `true`, the submodule will be checked out with `depth = 1`. If unset, `false` is assumed.
    pub fn shallow(&self, name: &BStr) -> Result<Option<bool>, gix_config::value::Error> {
        self.config.boolean(format!("submodule.{name}.shallow")).transpose()
    }
}