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
11impl File {
16 pub fn config(&self) -> &gix_config::File<'static> {
21 &self.config
22 }
23
24 pub fn config_path(&self) -> Option<&Path> {
26 self.config.sections().filter_map(|s| s.meta().path.as_deref()).next()
27 }
28
29 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 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 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 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
109impl File {
111 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 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 pub fn update(&self, name: &BStr) -> Result<Option<Update>, config::update::Error> {
169 let mut value_is_from_modules_file = None;
170 let our_meta = self.config.meta();
171 let value: Update = match self.config.string_filter(&format!("submodule.{name}.update"), |meta| {
172 value_is_from_modules_file = Some(std::ptr::eq(meta, our_meta));
173 true
174 }) {
175 Some(v) => v.as_ref().try_into().map_err(|()| config::update::Error::Invalid {
176 submodule: name.to_owned(),
177 actual: v.into_owned(),
178 })?,
179 None => return Ok(None),
180 };
181
182 if let Update::Command(cmd) = &value {
183 if value_is_from_modules_file.unwrap_or_default() {
184 return Err(config::update::Error::CommandForbiddenInModulesConfiguration {
185 submodule: name.to_owned(),
186 actual: cmd.to_owned(),
187 });
188 }
189 }
190 Ok(Some(value))
191 }
192
193 pub fn branch(&self, name: &BStr) -> Result<Option<Branch>, config::branch::Error> {
197 let branch = match self.config.string(&format!("submodule.{name}.branch")) {
198 Some(v) => v,
199 None => return Ok(None),
200 };
201
202 Branch::try_from(branch.as_ref())
203 .map(Some)
204 .map_err(|err| config::branch::Error {
205 submodule: name.to_owned(),
206 actual: branch.into_owned(),
207 source: err,
208 })
209 }
210
211 pub fn fetch_recurse(&self, name: &BStr) -> Result<Option<FetchRecurse>, config::Error> {
215 self.config
216 .boolean(&format!("submodule.{name}.fetchRecurseSubmodules"))
217 .map(FetchRecurse::new)
218 .transpose()
219 .map_err(|value| config::Error {
220 field: "fetchRecurseSubmodules",
221 submodule: name.to_owned(),
222 actual: value,
223 })
224 }
225
226 pub fn ignore(&self, name: &BStr) -> Result<Option<Ignore>, config::Error> {
228 self.config
229 .string(&format!("submodule.{name}.ignore"))
230 .map(|value| {
231 Ignore::try_from(value.as_ref()).map_err(|()| config::Error {
232 field: "ignore",
233 submodule: name.to_owned(),
234 actual: value.into_owned(),
235 })
236 })
237 .transpose()
238 }
239
240 pub fn shallow(&self, name: &BStr) -> Result<Option<bool>, gix_config::value::Error> {
244 self.config.boolean(&format!("submodule.{name}.shallow")).transpose()
245 }
246}