Skip to content

Commit af1cab3

Browse files
committed
feat: TBD a way to learn if submodules are active efficiently
1 parent 07a3e93 commit af1cab3

File tree

5 files changed

+201
-1
lines changed

5 files changed

+201
-1
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gix-submodule/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ rust-version = "1.65"
1212
doctest = false
1313

1414
[dependencies]
15+
gix-pathspec = { version = "^0.1.0", path = "../gix-pathspec" }
1516
gix-refspec = { version = "^0.15.0", path = "../gix-refspec" }
1617
gix-config = { version = "^0.27.0", path = "../gix-config" }
1718
gix-path = { version = "^0.8.4", path = "../gix-path" }

gix-submodule/src/access.rs

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use bstr::BStr;
44
use std::borrow::Cow;
55
use std::path::Path;
66

7-
/// Access
7+
/// High-Level Access
88
///
99
/// Note that all methods perform validation of the requested value and report issues right away.
1010
/// If a bypass is needed, use [`config()`](File::config()) for direct access.
@@ -33,6 +33,69 @@ impl File {
3333
.filter_map(|s| s.header().subsection_name())
3434
}
3535

36+
/// Return an iterator of names along with a boolean that indicates the submodule is active (`true`) or inactive (`false`).
37+
/// If the boolean was wrapped in an error, there was a configuration error.
38+
/// Use `defaults` for parsing the pathspecs used to match on names via `submodule.active` configuration retrieved from `config`.
39+
/// `attributes` provides a way to resolve the attributes mentioned in pathspecs.
40+
///
41+
/// Inactive submodules should not participate in any operations that are applying to all submodules.
42+
///
43+
/// Note that the entirety of sections in `config` are considered, not just the ones of the configuration for the repository itself.
44+
/// `submodule.active` pathspecs are considered to be top-level specs and match the name of submodules, which may be considered active
45+
/// on match. However, there is a [hierarchy of rules](https://git-scm.com/docs/gitsubmodules#_active_submodules) that's
46+
/// implemented here, but pathspecs add the most complexity.
47+
pub fn names_and_active_state<'a>(
48+
&'a self,
49+
config: &'a gix_config::File<'static>,
50+
defaults: gix_pathspec::Defaults,
51+
mut attributes: impl 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 = (&BStr, Result<bool, config::names_and_active_state::iter::Error>)> + 'a,
60+
config::names_and_active_state::Error,
61+
> {
62+
let mut search = config
63+
.strings_by_key("submodule.active")
64+
.map(|patterns| -> Result<_, config::names_and_active_state::Error> {
65+
let patterns = patterns
66+
.into_iter()
67+
.map(|pattern| gix_pathspec::parse(&pattern, defaults))
68+
.collect::<Result<Vec<_>, _>>()?;
69+
Ok(gix_pathspec::Search::from_specs(
70+
patterns,
71+
None,
72+
std::path::Path::new(""),
73+
)?)
74+
})
75+
.transpose()?;
76+
let iter = self.names().map(move |name| {
77+
let active = (|| -> Result<_, config::names_and_active_state::iter::Error> {
78+
if let Some(val) = config.boolean("submodule", Some(name), "active").transpose()? {
79+
return Ok(val);
80+
};
81+
if let Some(val) = search
82+
.as_mut()
83+
.and_then(|search| search.pattern_matching_relative_path(name, Some(true), &mut attributes))
84+
.map(|m| !m.is_excluded())
85+
{
86+
return Ok(val);
87+
}
88+
Ok(match self.url(name) {
89+
Ok(_) => true,
90+
Err(config::url::Error::Missing { .. }) => false,
91+
Err(err) => return Err(err.into()),
92+
})
93+
})();
94+
(name, active)
95+
});
96+
Ok(iter)
97+
}
98+
3699
/// Given the `relative_path` (as seen from the root of the worktree) of a submodule with possibly platform-specific
37100
/// component separators, find the submodule's name associated with this path, or `None` if none was found.
38101
///
@@ -43,7 +106,10 @@ impl File {
43106
.filter_map(|n| self.path(n).ok().map(|p| (n, p)))
44107
.find_map(|(n, p)| (p == relative_path).then_some(n))
45108
}
109+
}
46110

111+
/// Per-Submodule Access
112+
impl File {
47113
/// Return the path relative to the root directory of the working tree at which the submodule is expected to be checked out.
48114
/// 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
49115
/// information, like the URL to fetch from.

gix-submodule/src/config.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,3 +214,28 @@ pub mod path {
214214
OutsideOfWorktree { actual: BString, submodule: BString },
215215
}
216216
}
217+
///
218+
pub mod names_and_active_state {
219+
/// The error returned by [File::names_and_active_state](crate::File::names_and_active_state()).
220+
#[derive(Debug, thiserror::Error)]
221+
#[allow(missing_docs)]
222+
pub enum Error {
223+
#[error(transparent)]
224+
NormalizePattern(#[from] gix_pathspec::normalize::Error),
225+
#[error(transparent)]
226+
ParsePattern(#[from] gix_pathspec::parse::Error),
227+
}
228+
229+
///
230+
pub mod iter {
231+
/// The error returned by the iterator of [File::names_and_active_state](crate::File::names_and_active_state()).
232+
#[derive(Debug, thiserror::Error)]
233+
#[allow(missing_docs)]
234+
pub enum Error {
235+
#[error("The value of the 'active' field of a submodule could not be decoded")]
236+
ActiveField(#[from] gix_config::value::Error),
237+
#[error(transparent)]
238+
Url(#[from] crate::config::url::Error),
239+
}
240+
}
241+
}

gix-submodule/tests/file/mod.rs

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,109 @@ fn submodule(bytes: &str) -> gix_submodule::File {
22
gix_submodule::File::from_bytes(bytes.as_bytes(), None).expect("valid module")
33
}
44

5+
mod names_and_active_state {
6+
use bstr::{BStr, ByteSlice};
7+
use std::str::FromStr;
8+
9+
fn multi_modules() -> crate::Result<gix_submodule::File> {
10+
let modules = gix_testtools::scripted_fixture_read_only("basic.sh")?
11+
.join("multiple")
12+
.join(".gitmodules");
13+
Ok(gix_submodule::File::from_bytes(
14+
std::fs::read(&modules)?.as_slice(),
15+
modules,
16+
)?)
17+
}
18+
19+
fn assume_valid_active_state<'a>(
20+
module: &'a gix_submodule::File,
21+
config: &'a gix_config::File<'static>,
22+
defaults: gix_pathspec::Defaults,
23+
) -> crate::Result<Vec<(&'a str, bool)>> {
24+
assume_valid_active_state_with_attrs(module, config, defaults, |_, _, _, _| {
25+
unreachable!("shouldn't be called")
26+
})
27+
}
28+
29+
fn assume_valid_active_state_with_attrs<'a>(
30+
module: &'a gix_submodule::File,
31+
config: &'a gix_config::File<'static>,
32+
defaults: gix_pathspec::Defaults,
33+
attributes: impl FnMut(
34+
&BStr,
35+
gix_pathspec::attributes::glob::pattern::Case,
36+
bool,
37+
&mut gix_pathspec::attributes::search::Outcome,
38+
) -> bool
39+
+ 'a,
40+
) -> crate::Result<Vec<(&'a str, bool)>> {
41+
Ok(module
42+
.names_and_active_state(config, defaults, attributes)?
43+
.map(|(name, bool)| (name.to_str().expect("valid"), bool.expect("valid")))
44+
.collect())
45+
}
46+
47+
#[test]
48+
fn without_any_additional_settings_all_are_active_if_they_have_a_url() -> crate::Result {
49+
let module = multi_modules()?;
50+
assert_eq!(
51+
assume_valid_active_state(&module, &Default::default(), Default::default())?,
52+
&[
53+
("submodule", true),
54+
("a/b", true),
55+
(".a/..c", true),
56+
("a/d\\", true),
57+
("a\\e", true)
58+
]
59+
);
60+
Ok(())
61+
}
62+
63+
#[test]
64+
fn submodules_with_active_config_are_considered_active_or_inactive() -> crate::Result {
65+
let module = multi_modules()?;
66+
assert_eq!(
67+
assume_valid_active_state(
68+
&module,
69+
&gix_config::File::from_str(
70+
"[submodule.submodule]\n active = 0\n[submodule \"a/b\"]\n active = false"
71+
)?,
72+
Default::default()
73+
)?,
74+
&[
75+
("submodule", false),
76+
("a/b", false),
77+
(".a/..c", true),
78+
("a/d\\", true),
79+
("a\\e", true)
80+
]
81+
);
82+
Ok(())
83+
}
84+
85+
#[test]
86+
fn submodules_with_active_config_override_pathspecs() -> crate::Result {
87+
let module = multi_modules()?;
88+
assert_eq!(
89+
assume_valid_active_state(
90+
&module,
91+
&gix_config::File::from_str(
92+
"[submodule.submodule]\n active = 0\n[submodule]\n active = *\n[submodule]\n active = :!a*"
93+
)?,
94+
Default::default()
95+
)?,
96+
&[
97+
("submodule", false),
98+
("a/b", false),
99+
(".a/..c", true),
100+
("a/d\\", false),
101+
("a\\e", false)
102+
]
103+
);
104+
Ok(())
105+
}
106+
}
107+
5108
mod path {
6109
use crate::file::submodule;
7110
use gix_submodule::config::path::Error;
@@ -228,6 +331,10 @@ mod branch {
228331
("", Branch::Name("HEAD".into())),
229332
("master", Branch::Name("master".into())),
230333
("feature/a", Branch::Name("feature/a".into())),
334+
(
335+
"abcde12345abcde12345abcde12345abcde12345",
336+
Branch::Name("abcde12345abcde12345abcde12345abcde12345".into()),
337+
),
231338
] {
232339
let module = submodule(&format!("[submodule.a]\n branch = {valid}"));
233340
assert_eq!(module.branch("a".into())?.expect("present"), expected);

0 commit comments

Comments
 (0)