Skip to content

Commit

Permalink
feat: TBD a way to learn if submodules are active efficiently
Browse files Browse the repository at this point in the history
Byron committed Aug 17, 2023
1 parent 07a3e93 commit af1cab3
Showing 5 changed files with 201 additions and 1 deletion.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions gix-submodule/Cargo.toml
Original file line number Diff line number Diff line change
@@ -12,6 +12,7 @@ rust-version = "1.65"
doctest = false

[dependencies]
gix-pathspec = { version = "^0.1.0", path = "../gix-pathspec" }
gix-refspec = { version = "^0.15.0", path = "../gix-refspec" }
gix-config = { version = "^0.27.0", path = "../gix-config" }
gix-path = { version = "^0.8.4", path = "../gix-path" }
68 changes: 67 additions & 1 deletion gix-submodule/src/access.rs
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ use bstr::BStr;
use std::borrow::Cow;
use std::path::Path;

/// Access
/// 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.
@@ -33,6 +33,69 @@ impl File {
.filter_map(|s| s.header().subsection_name())
}

/// Return an iterator of names along with a boolean that indicates the submodule is active (`true`) or inactive (`false`).
/// If the boolean was wrapped in an error, there was a configuration error.
/// Use `defaults` for parsing the pathspecs used to match on names via `submodule.active` configuration retrieved from `config`.
/// `attributes` provides a way to resolve the attributes mentioned in pathspecs.
///
/// Inactive submodules should not participate in any operations that are applying to all submodules.
///
/// Note that the entirety of sections in `config` are considered, not just the ones of the configuration for the repository itself.
/// `submodule.active` pathspecs are considered to be top-level specs and match the name of submodules, which may be considered active
/// on match. However, there is a [hierarchy of rules](https://git-scm.com/docs/gitsubmodules#_active_submodules) that's
/// implemented here, but pathspecs add the most complexity.
pub fn names_and_active_state<'a>(
&'a self,
config: &'a gix_config::File<'static>,
defaults: gix_pathspec::Defaults,
mut attributes: impl FnMut(
&BStr,
gix_pathspec::attributes::glob::pattern::Case,
bool,
&mut gix_pathspec::attributes::search::Outcome,
) -> bool
+ 'a,
) -> Result<
impl Iterator<Item = (&BStr, Result<bool, config::names_and_active_state::iter::Error>)> + 'a,
config::names_and_active_state::Error,
> {
let mut search = config
.strings_by_key("submodule.active")
.map(|patterns| -> Result<_, config::names_and_active_state::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()?;
let iter = self.names().map(move |name| {
let active = (|| -> Result<_, config::names_and_active_state::iter::Error> {
if let Some(val) = config.boolean("submodule", Some(name), "active").transpose()? {
return Ok(val);
};
if let Some(val) = search
.as_mut()
.and_then(|search| search.pattern_matching_relative_path(name, Some(true), &mut attributes))
.map(|m| !m.is_excluded())
{
return Ok(val);
}
Ok(match self.url(name) {
Ok(_) => true,
Err(config::url::Error::Missing { .. }) => false,
Err(err) => return Err(err.into()),
})
})();
(name, active)
});
Ok(iter)
}

/// 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.
///
@@ -43,7 +106,10 @@ impl File {
.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.
25 changes: 25 additions & 0 deletions gix-submodule/src/config.rs
Original file line number Diff line number Diff line change
@@ -214,3 +214,28 @@ pub mod path {
OutsideOfWorktree { actual: BString, submodule: BString },
}
}
///
pub mod names_and_active_state {
/// The error returned by [File::names_and_active_state](crate::File::names_and_active_state()).
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum Error {
#[error(transparent)]
NormalizePattern(#[from] gix_pathspec::normalize::Error),
#[error(transparent)]
ParsePattern(#[from] gix_pathspec::parse::Error),
}

///
pub mod iter {
/// The error returned by the iterator of [File::names_and_active_state](crate::File::names_and_active_state()).
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum Error {
#[error("The value of the 'active' field of a submodule could not be decoded")]
ActiveField(#[from] gix_config::value::Error),
#[error(transparent)]
Url(#[from] crate::config::url::Error),
}
}
}
107 changes: 107 additions & 0 deletions gix-submodule/tests/file/mod.rs
Original file line number Diff line number Diff line change
@@ -2,6 +2,109 @@ fn submodule(bytes: &str) -> gix_submodule::File {
gix_submodule::File::from_bytes(bytes.as_bytes(), None).expect("valid module")
}

mod names_and_active_state {
use bstr::{BStr, ByteSlice};
use std::str::FromStr;

fn multi_modules() -> crate::Result<gix_submodule::File> {
let modules = gix_testtools::scripted_fixture_read_only("basic.sh")?
.join("multiple")
.join(".gitmodules");
Ok(gix_submodule::File::from_bytes(
std::fs::read(&modules)?.as_slice(),
modules,
)?)
}

fn assume_valid_active_state<'a>(
module: &'a gix_submodule::File,
config: &'a gix_config::File<'static>,
defaults: gix_pathspec::Defaults,
) -> crate::Result<Vec<(&'a str, bool)>> {
assume_valid_active_state_with_attrs(module, config, defaults, |_, _, _, _| {
unreachable!("shouldn't be called")
})
}

fn assume_valid_active_state_with_attrs<'a>(
module: &'a gix_submodule::File,
config: &'a gix_config::File<'static>,
defaults: gix_pathspec::Defaults,
attributes: impl FnMut(
&BStr,
gix_pathspec::attributes::glob::pattern::Case,
bool,
&mut gix_pathspec::attributes::search::Outcome,
) -> bool
+ 'a,
) -> crate::Result<Vec<(&'a str, bool)>> {
Ok(module
.names_and_active_state(config, defaults, attributes)?
.map(|(name, bool)| (name.to_str().expect("valid"), bool.expect("valid")))
.collect())
}

#[test]
fn without_any_additional_settings_all_are_active_if_they_have_a_url() -> crate::Result {
let module = multi_modules()?;
assert_eq!(
assume_valid_active_state(&module, &Default::default(), Default::default())?,
&[
("submodule", true),
("a/b", true),
(".a/..c", true),
("a/d\\", true),
("a\\e", true)
]
);
Ok(())
}

#[test]
fn submodules_with_active_config_are_considered_active_or_inactive() -> crate::Result {
let module = multi_modules()?;
assert_eq!(
assume_valid_active_state(
&module,
&gix_config::File::from_str(
"[submodule.submodule]\n active = 0\n[submodule \"a/b\"]\n active = false"
)?,
Default::default()
)?,
&[
("submodule", false),
("a/b", false),
(".a/..c", true),
("a/d\\", true),
("a\\e", true)
]
);
Ok(())
}

#[test]
fn submodules_with_active_config_override_pathspecs() -> crate::Result {
let module = multi_modules()?;
assert_eq!(
assume_valid_active_state(
&module,
&gix_config::File::from_str(
"[submodule.submodule]\n active = 0\n[submodule]\n active = *\n[submodule]\n active = :!a*"
)?,
Default::default()
)?,
&[
("submodule", false),
("a/b", false),
(".a/..c", true),
("a/d\\", false),
("a\\e", false)
]
);
Ok(())
}
}

mod path {
use crate::file::submodule;
use gix_submodule::config::path::Error;
@@ -228,6 +331,10 @@ mod branch {
("", Branch::Name("HEAD".into())),
("master", Branch::Name("master".into())),
("feature/a", Branch::Name("feature/a".into())),
(
"abcde12345abcde12345abcde12345abcde12345",
Branch::Name("abcde12345abcde12345abcde12345abcde12345".into()),
),
] {
let module = submodule(&format!("[submodule.a]\n branch = {valid}"));
assert_eq!(module.branch("a".into())?.expect("present"), expected);

0 comments on commit af1cab3

Please sign in to comment.