summaryrefslogtreecommitdiffstats
path: root/vendor/gix-config/src/file/includes/mod.rs
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/gix-config/src/file/includes/mod.rs')
-rw-r--r--vendor/gix-config/src/file/includes/mod.rs319
1 files changed, 319 insertions, 0 deletions
diff --git a/vendor/gix-config/src/file/includes/mod.rs b/vendor/gix-config/src/file/includes/mod.rs
new file mode 100644
index 000000000..8fd92725f
--- /dev/null
+++ b/vendor/gix-config/src/file/includes/mod.rs
@@ -0,0 +1,319 @@
+use std::{
+ borrow::Cow,
+ path::{Path, PathBuf},
+};
+
+use bstr::{BStr, BString, ByteSlice, ByteVec};
+use gix_features::threading::OwnShared;
+use gix_ref::Category;
+
+use crate::{
+ file,
+ file::{includes, init, Metadata, SectionId},
+ path, File,
+};
+
+impl File<'static> {
+ /// Traverse all `include` and `includeIf` directives found in this instance and follow them, loading the
+ /// referenced files from their location and adding their content right past the value that included them.
+ ///
+ /// # Limitations
+ ///
+ /// - Note that this method is _not idempotent_ and calling it multiple times will resolve includes multiple
+ /// times. It's recommended use is as part of a multi-step bootstrapping which needs fine-grained control,
+ /// and unless that's given one should prefer one of the other ways of initialization that resolve includes
+ /// at the right time.
+ /// - included values are added after the _section_ that included them, not directly after the value. This is
+ /// a deviation from how git does it, as it technically adds new value right after the include path itself,
+ /// technically 'splitting' the section. This can only make a difference if the `include` section also has values
+ /// which later overwrite portions of the included file, which seems unusual as these would be related to `includes`.
+ /// We can fix this by 'splitting' the include section if needed so the included sections are put into the right place.
+ pub fn resolve_includes(&mut self, options: init::Options<'_>) -> Result<(), Error> {
+ if options.includes.max_depth == 0 {
+ return Ok(());
+ }
+ let mut buf = Vec::new();
+ resolve(self, &mut buf, options)
+ }
+}
+
+pub(crate) fn resolve(config: &mut File<'static>, buf: &mut Vec<u8>, options: init::Options<'_>) -> Result<(), Error> {
+ resolve_includes_recursive(config, 0, buf, options)
+}
+
+fn resolve_includes_recursive(
+ target_config: &mut File<'static>,
+ depth: u8,
+ buf: &mut Vec<u8>,
+ options: init::Options<'_>,
+) -> Result<(), Error> {
+ if depth == options.includes.max_depth {
+ return if options.includes.err_on_max_depth_exceeded {
+ Err(Error::IncludeDepthExceeded {
+ max_depth: options.includes.max_depth,
+ })
+ } else {
+ Ok(())
+ };
+ }
+
+ let mut section_ids_and_include_paths = Vec::new();
+ for (id, section) in target_config
+ .section_order
+ .iter()
+ .map(|id| (*id, &target_config.sections[id]))
+ {
+ let header = &section.header;
+ let header_name = header.name.as_ref();
+ if header_name == "include" && header.subsection_name.is_none() {
+ detach_include_paths(&mut section_ids_and_include_paths, section, id)
+ } else if header_name == "includeIf" {
+ if let Some(condition) = &header.subsection_name {
+ let target_config_path = section.meta.path.as_deref();
+ if include_condition_match(condition.as_ref(), target_config_path, options.includes)? {
+ detach_include_paths(&mut section_ids_and_include_paths, section, id)
+ }
+ }
+ }
+ }
+
+ append_followed_includes_recursively(section_ids_and_include_paths, target_config, depth, options, buf)
+}
+
+fn append_followed_includes_recursively(
+ section_ids_and_include_paths: Vec<(SectionId, crate::Path<'_>)>,
+ target_config: &mut File<'static>,
+ depth: u8,
+ options: init::Options<'_>,
+ buf: &mut Vec<u8>,
+) -> Result<(), Error> {
+ for (section_id, config_path) in section_ids_and_include_paths {
+ let meta = OwnShared::clone(&target_config.sections[&section_id].meta);
+ let target_config_path = meta.path.as_deref();
+ let config_path = match resolve_path(config_path, target_config_path, options.includes)? {
+ Some(p) => p,
+ None => continue,
+ };
+ if !config_path.is_file() {
+ continue;
+ }
+
+ buf.clear();
+ std::io::copy(&mut std::fs::File::open(&config_path)?, buf)?;
+ let config_meta = Metadata {
+ path: Some(config_path),
+ trust: meta.trust,
+ level: meta.level + 1,
+ source: meta.source,
+ };
+ let no_follow_options = init::Options {
+ includes: includes::Options::no_follow(),
+ ..options
+ };
+
+ let mut include_config =
+ File::from_bytes_owned(buf, config_meta, no_follow_options).map_err(|err| match err {
+ init::Error::Parse(err) => Error::Parse(err),
+ init::Error::Interpolate(err) => Error::Interpolate(err),
+ init::Error::Includes(_) => unreachable!("BUG: {:?} not possible due to no-follow options", err),
+ })?;
+ resolve_includes_recursive(&mut include_config, depth + 1, buf, options)?;
+
+ target_config.append_or_insert(include_config, Some(section_id));
+ }
+ Ok(())
+}
+
+fn detach_include_paths(
+ include_paths: &mut Vec<(SectionId, crate::Path<'static>)>,
+ section: &file::Section<'_>,
+ id: SectionId,
+) {
+ include_paths.extend(
+ section
+ .body
+ .values("path")
+ .into_iter()
+ .map(|path| (id, crate::Path::from(Cow::Owned(path.into_owned())))),
+ )
+}
+
+fn include_condition_match(
+ condition: &BStr,
+ target_config_path: Option<&Path>,
+ options: Options<'_>,
+) -> Result<bool, Error> {
+ let mut tokens = condition.splitn(2, |b| *b == b':');
+ let (prefix, condition) = match (tokens.next(), tokens.next()) {
+ (Some(a), Some(b)) => (a, b),
+ _ => return Ok(false),
+ };
+ let condition = condition.as_bstr();
+ match prefix {
+ b"gitdir" => gitdir_matches(
+ condition,
+ target_config_path,
+ options,
+ gix_glob::wildmatch::Mode::empty(),
+ ),
+ b"gitdir/i" => gitdir_matches(
+ condition,
+ target_config_path,
+ options,
+ gix_glob::wildmatch::Mode::IGNORE_CASE,
+ ),
+ b"onbranch" => Ok(onbranch_matches(condition, options.conditional).is_some()),
+ _ => Ok(false),
+ }
+}
+
+fn onbranch_matches(
+ condition: &BStr,
+ conditional::Context { branch_name, .. }: conditional::Context<'_>,
+) -> Option<()> {
+ let branch_name = branch_name?;
+ let (_, branch_name) = branch_name
+ .category_and_short_name()
+ .filter(|(cat, _)| *cat == Category::LocalBranch)?;
+
+ let condition = if condition.ends_with(b"/") {
+ let mut condition: BString = condition.into();
+ condition.push_str("**");
+ Cow::Owned(condition)
+ } else {
+ condition.into()
+ };
+
+ gix_glob::wildmatch(
+ condition.as_ref(),
+ branch_name,
+ gix_glob::wildmatch::Mode::NO_MATCH_SLASH_LITERAL,
+ )
+ .then_some(())
+}
+
+fn gitdir_matches(
+ condition_path: &BStr,
+ target_config_path: Option<&Path>,
+ Options {
+ conditional: conditional::Context { git_dir, .. },
+ interpolate: context,
+ err_on_interpolation_failure,
+ err_on_missing_config_path,
+ ..
+ }: Options<'_>,
+ wildmatch_mode: gix_glob::wildmatch::Mode,
+) -> Result<bool, Error> {
+ if !err_on_interpolation_failure && git_dir.is_none() {
+ return Ok(false);
+ }
+ let git_dir = gix_path::to_unix_separators_on_windows(gix_path::into_bstr(git_dir.ok_or(Error::MissingGitDir)?));
+
+ let mut pattern_path: Cow<'_, _> = {
+ let path = match check_interpolation_result(
+ err_on_interpolation_failure,
+ crate::Path::from(Cow::Borrowed(condition_path)).interpolate(context),
+ )? {
+ Some(p) => p,
+ None => return Ok(false),
+ };
+ gix_path::into_bstr(path).into_owned().into()
+ };
+ // NOTE: yes, only if we do path interpolation will the slashes be forced to unix separators on windows
+ if pattern_path != condition_path {
+ pattern_path = gix_path::to_unix_separators_on_windows(pattern_path);
+ }
+
+ if let Some(relative_pattern_path) = pattern_path.strip_prefix(b"./") {
+ if !err_on_missing_config_path && target_config_path.is_none() {
+ return Ok(false);
+ }
+ let parent_dir = target_config_path
+ .ok_or(Error::MissingConfigPath)?
+ .parent()
+ .expect("config path can never be /");
+ let mut joined_path = gix_path::to_unix_separators_on_windows(gix_path::into_bstr(parent_dir)).into_owned();
+ joined_path.push(b'/');
+ joined_path.extend_from_slice(relative_pattern_path);
+ pattern_path = joined_path.into();
+ }
+
+ // NOTE: this special handling of leading backslash is needed to do it like git does
+ if pattern_path.iter().next() != Some(&(std::path::MAIN_SEPARATOR as u8))
+ && !gix_path::from_bstr(pattern_path.clone()).is_absolute()
+ {
+ let mut prefixed = pattern_path.into_owned();
+ prefixed.insert_str(0, "**/");
+ pattern_path = prefixed.into()
+ }
+ if pattern_path.ends_with(b"/") {
+ let mut suffixed = pattern_path.into_owned();
+ suffixed.push_str("**");
+ pattern_path = suffixed.into();
+ }
+
+ let match_mode = gix_glob::wildmatch::Mode::NO_MATCH_SLASH_LITERAL | wildmatch_mode;
+ let is_match = gix_glob::wildmatch(pattern_path.as_bstr(), git_dir.as_bstr(), match_mode);
+ if is_match {
+ return Ok(true);
+ }
+
+ let expanded_git_dir = gix_path::into_bstr(gix_path::realpath(gix_path::from_byte_slice(&git_dir))?);
+ Ok(gix_glob::wildmatch(
+ pattern_path.as_bstr(),
+ expanded_git_dir.as_bstr(),
+ match_mode,
+ ))
+}
+
+fn check_interpolation_result(
+ disable: bool,
+ res: Result<Cow<'_, std::path::Path>, path::interpolate::Error>,
+) -> Result<Option<Cow<'_, std::path::Path>>, path::interpolate::Error> {
+ if disable {
+ return res.map(Some);
+ }
+ match res {
+ Ok(good) => Ok(good.into()),
+ Err(err) => match err {
+ path::interpolate::Error::Missing { .. } | path::interpolate::Error::UserInterpolationUnsupported => {
+ Ok(None)
+ }
+ path::interpolate::Error::UsernameConversion(_) | path::interpolate::Error::Utf8Conversion { .. } => {
+ Err(err)
+ }
+ },
+ }
+}
+
+fn resolve_path(
+ path: crate::Path<'_>,
+ target_config_path: Option<&Path>,
+ includes::Options {
+ interpolate: context,
+ err_on_interpolation_failure,
+ err_on_missing_config_path,
+ ..
+ }: includes::Options<'_>,
+) -> Result<Option<PathBuf>, Error> {
+ let path = match check_interpolation_result(err_on_interpolation_failure, path.interpolate(context))? {
+ Some(p) => p,
+ None => return Ok(None),
+ };
+ let path: PathBuf = if path.is_relative() {
+ if !err_on_missing_config_path && target_config_path.is_none() {
+ return Ok(None);
+ }
+ target_config_path
+ .ok_or(Error::MissingConfigPath)?
+ .parent()
+ .expect("path is a config file which naturally lives in a directory")
+ .join(path)
+ } else {
+ path.into()
+ };
+ Ok(Some(path))
+}
+
+mod types;
+pub use types::{conditional, Error, Options};