diff --git a/git-config/src/file/access/mutate.rs b/git-config/src/file/access/mutate.rs index f0c233d2013..189a62bf6b3 100644 --- a/git-config/src/file/access/mutate.rs +++ b/git-config/src/file/access/mutate.rs @@ -24,11 +24,12 @@ impl<'event> File<'event> { .rev() .next() .expect("BUG: Section lookup vec was empty"); - Ok(SectionMut::new( - self.sections - .get_mut(&id) - .expect("BUG: Section did not have id from lookup"), - )) + let nl = self.detect_newline_style_smallvec(); + Ok(self + .sections + .get_mut(&id) + .expect("BUG: Section did not have id from lookup") + .to_mut(nl)) } /// Returns the last found mutable section with a given `name` and optional `subsection_name`, that matches `filter`. @@ -48,7 +49,8 @@ impl<'event> File<'event> { let s = &self.sections[id]; filter(s.meta()) }); - Ok(id.and_then(move |id| self.sections.get_mut(&id).map(|s| s.to_mut()))) + let nl = self.detect_newline_style_smallvec(); + Ok(id.and_then(move |id| self.sections.get_mut(&id).map(move |s| s.to_mut(nl)))) } /// Adds a new section. If a subsection name was provided, then @@ -63,8 +65,10 @@ impl<'event> File<'event> { /// # use git_config::File; /// # use std::convert::TryFrom; /// let mut git_config = git_config::File::default(); - /// let _section = git_config.new_section("hello", Some("world".into())); - /// assert_eq!(git_config.to_string(), "[hello \"world\"]\n"); + /// let section = git_config.new_section("hello", Some("world".into()))?; + /// let nl = section.newline().to_owned(); + /// assert_eq!(git_config.to_string(), format!("[hello \"world\"]{nl}")); + /// # Ok::<(), Box>(()) /// ``` /// /// Creating a new empty section and adding values to it: @@ -77,9 +81,10 @@ impl<'event> File<'event> { /// let mut git_config = git_config::File::default(); /// let mut section = git_config.new_section("hello", Some("world".into()))?; /// section.push(section::Key::try_from("a")?, "b"); - /// assert_eq!(git_config.to_string(), "[hello \"world\"]\n\ta = b\n"); + /// let nl = section.newline().to_owned(); + /// assert_eq!(git_config.to_string(), format!("[hello \"world\"]{nl}\ta = b{nl}")); /// let _section = git_config.new_section("core", None); - /// assert_eq!(git_config.to_string(), "[hello \"world\"]\n\ta = b\n[core]\n"); + /// assert_eq!(git_config.to_string(), format!("[hello \"world\"]{nl}\ta = b{nl}[core]{nl}")); /// # Ok::<(), Box>(()) /// ``` pub fn new_section( @@ -87,8 +92,9 @@ impl<'event> File<'event> { name: impl Into>, subsection: impl Into>>, ) -> Result, section::header::Error> { - let mut section = - self.push_section_internal(file::Section::new(name, subsection, OwnShared::clone(&self.meta))?); + let id = self.push_section_internal(file::Section::new(name, subsection, OwnShared::clone(&self.meta))?); + let nl = self.detect_newline_style_smallvec(); + let mut section = self.sections.get_mut(&id).expect("each id yields a section").to_mut(nl); section.push_newline(); Ok(section) } @@ -180,7 +186,10 @@ impl<'event> File<'event> { &mut self, section: file::Section<'event>, ) -> Result, section::header::Error> { - Ok(self.push_section_internal(section)) + let id = self.push_section_internal(section); + let nl = self.detect_newline_style_smallvec(); + let section = self.sections.get_mut(&id).expect("each id yields a section").to_mut(nl); + Ok(section) } /// Renames the section with `name` and `subsection_name`, modifying the last matching section @@ -290,22 +299,4 @@ impl<'event> File<'event> { } self } - - fn detect_newline_style(&self) -> &BStr { - fn extract_newline<'a, 'b>(e: &'a Event<'b>) -> Option<&'a BStr> { - match e { - Event::Newline(b) => b.as_ref().into(), - _ => None, - } - } - - self.frontmatter_events - .iter() - .find_map(extract_newline) - .or_else(|| { - self.sections() - .find_map(|s| s.body.as_ref().iter().find_map(extract_newline)) - }) - .unwrap_or_else(|| if cfg!(windows) { "\r\n" } else { "\n" }.into()) - } } diff --git a/git-config/src/file/access/raw.rs b/git-config/src/file/access/raw.rs index cf992d111bd..60c4aae2ade 100644 --- a/git-config/src/file/access/raw.rs +++ b/git-config/src/file/access/raw.rs @@ -1,10 +1,11 @@ use std::{borrow::Cow, collections::HashMap}; use bstr::BStr; +use smallvec::ToSmallVec; use crate::file::MetadataFilter; use crate::{ - file::{mutable::multi_value::EntryData, Index, MultiValueMut, SectionMut, Size, ValueMut}, + file::{mutable::multi_value::EntryData, Index, MultiValueMut, Size, ValueMut}, lookup, parse::{section, Event}, File, @@ -120,8 +121,9 @@ impl<'event> File<'event> { } drop(section_ids); + let nl = self.detect_newline_style().to_smallvec(); return Ok(ValueMut { - section: SectionMut::new(self.sections.get_mut(§ion_id).expect("known section-id")), + section: self.sections.get_mut(§ion_id).expect("known section-id").to_mut(nl), key, index: Index(index), size: Size(size), diff --git a/git-config/src/file/access/read_only.rs b/git-config/src/file/access/read_only.rs index 51c6801c7a6..37c6e6fd1ef 100644 --- a/git-config/src/file/access/read_only.rs +++ b/git-config/src/file/access/read_only.rs @@ -1,9 +1,12 @@ +use std::iter::FromIterator; use std::{borrow::Cow, convert::TryFrom}; use bstr::BStr; use git_features::threading::OwnShared; +use smallvec::SmallVec; use crate::file::{Metadata, MetadataFilter}; +use crate::parse::Event; use crate::{file, lookup, File}; /// Read-only low-level access methods, as it requires generics for converting into @@ -254,9 +257,7 @@ impl<'event> File<'event> { /// /// This allows to reproduce the look of sections perfectly when serializing them with /// [`write_to()`][file::Section::write_to()]. - pub fn sections_and_postmatter( - &self, - ) -> impl Iterator, Vec<&crate::parse::Event<'event>>)> { + pub fn sections_and_postmatter(&self) -> impl Iterator, Vec<&Event<'event>>)> { self.section_order.iter().map(move |id| { let s = &self.sections[id]; let pm: Vec<_> = self @@ -269,7 +270,33 @@ impl<'event> File<'event> { } /// Return all events which are in front of the first of our sections, or `None` if there are none. - pub fn frontmatter(&self) -> Option>> { + pub fn frontmatter(&self) -> Option>> { (!self.frontmatter_events.is_empty()).then(|| self.frontmatter_events.iter()) } + + /// Return the newline characters that have been detected in this config file or the default ones + /// for the current platform. + /// + /// Note that the first found newline is the one we use in the assumption of consistency. + pub fn detect_newline_style(&self) -> &BStr { + fn extract_newline<'a, 'b>(e: &'a Event<'b>) -> Option<&'a BStr> { + match e { + Event::Newline(b) => b.as_ref().into(), + _ => None, + } + } + + self.frontmatter_events + .iter() + .find_map(extract_newline) + .or_else(|| { + self.sections() + .find_map(|s| s.body.as_ref().iter().find_map(extract_newline)) + }) + .unwrap_or_else(|| if cfg!(windows) { "\r\n" } else { "\n" }.into()) + } + + pub(crate) fn detect_newline_style_smallvec(&self) -> SmallVec<[u8; 2]> { + SmallVec::from_iter(self.detect_newline_style().iter().copied()) + } } diff --git a/git-config/src/file/init/from_env.rs b/git-config/src/file/init/from_env.rs index dc136b27a3a..9d759f1c122 100644 --- a/git-config/src/file/init/from_env.rs +++ b/git-config/src/file/init/from_env.rs @@ -3,7 +3,7 @@ use std::convert::TryFrom; use std::{borrow::Cow, path::PathBuf}; use crate::file::{init, Metadata}; -use crate::{file, parse::section, path::interpolate, File, Source}; +use crate::{file, parse, parse::section, path::interpolate, File, Source}; /// Represents the errors that may occur when calling [`File::from_env`][crate::File::from_env()]. #[derive(Debug, thiserror::Error)] @@ -129,33 +129,23 @@ impl File<'static> { for i in 0..count { let key = env::var(format!("GIT_CONFIG_KEY_{}", i)).map_err(|_| Error::InvalidKeyId { key_id: i })?; let value = env::var_os(format!("GIT_CONFIG_VALUE_{}", i)).ok_or(Error::InvalidValueId { value_id: i })?; - match key.split_once('.') { - Some((section_name, maybe_subsection)) => { - let (subsection, key) = match maybe_subsection.rsplit_once('.') { - Some((subsection, key)) => (Some(subsection), key), - None => (None, maybe_subsection), - }; + let key = parse::key(&key).ok_or_else(|| Error::InvalidKeyValue { + key_id: i, + key_val: key.to_string(), + })?; - let mut section = match config.section_mut(section_name, subsection) { - Ok(section) => section, - Err(_) => config.new_section( - section_name.to_string(), - subsection.map(|subsection| Cow::Owned(subsection.to_string())), - )?, - }; + let mut section = match config.section_mut(key.section_name, key.subsection_name) { + Ok(section) => section, + Err(_) => config.new_section( + key.section_name.to_owned(), + key.subsection_name.map(|subsection| Cow::Owned(subsection.to_owned())), + )?, + }; - section.push( - section::Key::try_from(key.to_owned())?, - git_path::os_str_into_bstr(&value).expect("no illformed UTF-8").as_ref(), - ); - } - None => { - return Err(Error::InvalidKeyValue { - key_id: i, - key_val: key.to_string(), - }) - } - } + section.push( + section::Key::try_from(key.value_name.to_owned())?, + git_path::os_str_into_bstr(&value).expect("no illformed UTF-8").as_ref(), + ); } let mut buf = Vec::new(); diff --git a/git-config/src/file/mutable/mod.rs b/git-config/src/file/mutable/mod.rs index 39b8a505452..efe896f0210 100644 --- a/git-config/src/file/mutable/mod.rs +++ b/git-config/src/file/mutable/mod.rs @@ -66,10 +66,8 @@ impl<'a> Whitespace<'a> { } out } -} -impl<'a> From<&file::section::Body<'a>> for Whitespace<'a> { - fn from(s: &file::section::Body<'a>) -> Self { + fn from_body(s: &file::section::Body<'a>) -> Self { let key_pos = s.0.iter() .enumerate() diff --git a/git-config/src/file/mutable/multi_value.rs b/git-config/src/file/mutable/multi_value.rs index db4b34df947..2ddbff34b7b 100644 --- a/git-config/src/file/mutable/multi_value.rs +++ b/git-config/src/file/mutable/multi_value.rs @@ -170,7 +170,7 @@ impl<'borrow, 'lookup, 'event> MultiValueMut<'borrow, 'lookup, 'event> { value: &BStr, ) { let (offset, size) = MultiValueMut::index_and_size(offsets, section_id, offset_index); - let whitespace: Whitespace<'_> = (&*section).into(); + let whitespace = Whitespace::from_body(section); let section = section.as_mut(); section.drain(offset..offset + size); diff --git a/git-config/src/file/mutable/section.rs b/git-config/src/file/mutable/section.rs index 7a7ec8183ed..276faff7148 100644 --- a/git-config/src/file/mutable/section.rs +++ b/git-config/src/file/mutable/section.rs @@ -3,7 +3,8 @@ use std::{ ops::{Deref, Range}, }; -use bstr::{BStr, BString, ByteVec}; +use bstr::{BStr, BString, ByteSlice, ByteVec}; +use smallvec::SmallVec; use crate::file::{self, Section}; use crate::{ @@ -22,6 +23,7 @@ pub struct SectionMut<'a, 'event> { section: &'a mut Section<'event>, implicit_newline: bool, whitespace: Whitespace<'event>, + newline: SmallVec<[u8; 2]>, } /// Mutating methods. @@ -37,7 +39,7 @@ impl<'a, 'event> SectionMut<'a, 'event> { body.extend(self.whitespace.key_value_separators()); body.push(Event::Value(escape_value(value.into()).into())); if self.implicit_newline { - body.push(Event::Newline(BString::from("\n").into())); + body.push(Event::Newline(BString::from(self.newline.to_vec()).into())); } } @@ -109,7 +111,15 @@ impl<'a, 'event> SectionMut<'a, 'event> { /// Adds a new line event. Note that you don't need to call this unless /// you've disabled implicit newlines. pub fn push_newline(&mut self) { - self.section.body.0.push(Event::Newline(Cow::Borrowed("\n".into()))); + self.section + .body + .0 + .push(Event::Newline(Cow::Owned(BString::from(self.newline.to_vec())))); + } + + /// Return the newline used when calling [`push_newline()`][Self::push_newline()]. + pub fn newline(&self) -> &BStr { + self.newline.as_slice().as_bstr() } /// Enables or disables automatically adding newline events after adding @@ -158,12 +168,13 @@ impl<'a, 'event> SectionMut<'a, 'event> { // Internal methods that may require exact indices for faster operations. impl<'a, 'event> SectionMut<'a, 'event> { - pub(crate) fn new(section: &'a mut Section<'event>) -> Self { - let whitespace = (§ion.body).into(); + pub(crate) fn new(section: &'a mut Section<'event>, newline: SmallVec<[u8; 2]>) -> Self { + let whitespace = Whitespace::from_body(§ion.body); Self { section, implicit_newline: true, whitespace, + newline, } } @@ -231,7 +242,7 @@ impl<'a, 'event> SectionMut<'a, 'event> { } impl<'event> Deref for SectionMut<'_, 'event> { - type Target = file::section::Body<'event>; + type Target = file::Section<'event>; fn deref(&self) -> &Self::Target { self.section diff --git a/git-config/src/file/mutable/value.rs b/git-config/src/file/mutable/value.rs index 63d39ed1ec0..2bccfd32ab4 100644 --- a/git-config/src/file/mutable/value.rs +++ b/git-config/src/file/mutable/value.rs @@ -3,6 +3,7 @@ use std::borrow::Cow; use bstr::BStr; use crate::{ + file, file::{mutable::section::SectionMut, Index, Size}, lookup, parse::section, @@ -49,4 +50,14 @@ impl<'borrow, 'lookup, 'event> ValueMut<'borrow, 'lookup, 'event> { self.size = Size(0); } } + + /// Return the section containing the value. + pub fn section(&self) -> &file::Section<'event> { + &self.section + } + + /// Convert this value into its owning mutable section. + pub fn into_section_mut(self) -> file::SectionMut<'borrow, 'event> { + self.section + } } diff --git a/git-config/src/file/section/mod.rs b/git-config/src/file/section/mod.rs index f6c8a24790e..8631243a611 100644 --- a/git-config/src/file/section/mod.rs +++ b/git-config/src/file/section/mod.rs @@ -2,6 +2,7 @@ use crate::file::{Metadata, Section, SectionMut}; use crate::parse::section; use crate::{file, parse}; use bstr::BString; +use smallvec::SmallVec; use std::borrow::Cow; use std::ops::Deref; @@ -71,7 +72,7 @@ impl<'a> Section<'a> { } /// Returns a mutable version of this section for adjustment of values. - pub fn to_mut(&mut self) -> SectionMut<'_, 'a> { - SectionMut::new(self) + pub fn to_mut(&mut self, newline: SmallVec<[u8; 2]>) -> SectionMut<'_, 'a> { + SectionMut::new(self, newline) } } diff --git a/git-config/src/file/utils.rs b/git-config/src/file/utils.rs index e5e816ce083..ea4cfc5267f 100644 --- a/git-config/src/file/utils.rs +++ b/git-config/src/file/utils.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use bstr::BStr; use crate::{ - file::{self, SectionBodyIds, SectionId, SectionMut}, + file::{self, SectionBodyIds, SectionId}, lookup, parse::section, File, @@ -11,8 +11,8 @@ use crate::{ /// Private helper functions impl<'event> File<'event> { - /// Adds a new section to the config file. - pub(crate) fn push_section_internal(&mut self, section: file::Section<'event>) -> SectionMut<'_, 'event> { + /// Adds a new section to the config file, returning the section id of the newly added section. + pub(crate) fn push_section_internal(&mut self, section: file::Section<'event>) -> SectionId { let new_section_id = SectionId(self.section_id_counter); self.sections.insert(new_section_id, section); let header = &self.sections[&new_section_id].header; @@ -49,10 +49,7 @@ impl<'event> File<'event> { } self.section_order.push_back(new_section_id); self.section_id_counter += 1; - self.sections - .get_mut(&new_section_id) - .map(SectionMut::new) - .expect("previously inserted section") + new_section_id } /// Returns the mapping between section and subsection name to section ids. diff --git a/git-config/src/parse/key.rs b/git-config/src/parse/key.rs new file mode 100644 index 00000000000..d4b11bb4707 --- /dev/null +++ b/git-config/src/parse/key.rs @@ -0,0 +1,27 @@ +/// An unvalidated parse result of parsing input like `remote.origin.url` or `core.bare`. +#[derive(Debug, PartialEq, Ord, PartialOrd, Eq, Hash, Clone, Copy)] +pub struct Key<'a> { + /// The name of the section, like `core` in `core.bare`. + pub section_name: &'a str, + /// The name of the sub-section, like `origin` in `remote.origin.url`. + pub subsection_name: Option<&'a str>, + /// The name of the section key, like `url` in `remote.origin.url`. + pub value_name: &'a str, +} + +/// Parse `input` like `core.bare` or `remote.origin.url` as a `Key` to make its fields available, +/// or `None` if there were not at least 2 tokens separated by `.`. +/// Note that `input` isn't validated, and is `str` as ascii is a subset of UTF-8 which is required for any valid keys. +pub fn parse_unvalidated(input: &str) -> Option> { + let (section_name, subsection_or_key) = input.split_once('.')?; + let (subsection_name, value_name) = match subsection_or_key.rsplit_once('.') { + Some((subsection, key)) => (Some(subsection), key), + None => (None, subsection_or_key), + }; + + Some(Key { + section_name, + subsection_name, + value_name, + }) +} diff --git a/git-config/src/parse/mod.rs b/git-config/src/parse/mod.rs index fd1f779b77e..33c51b19d57 100644 --- a/git-config/src/parse/mod.rs +++ b/git-config/src/parse/mod.rs @@ -26,6 +26,10 @@ mod error; /// pub mod section; +/// +mod key; +pub use key::{parse_unvalidated as key, Key}; + #[cfg(test)] pub(crate) mod tests; diff --git a/git-config/src/types.rs b/git-config/src/types.rs index 8fa77cf4d51..f442e372de2 100644 --- a/git-config/src/types.rs +++ b/git-config/src/types.rs @@ -37,6 +37,13 @@ pub enum Source { Api, } +impl Source { + /// Return true if the source indicates a location within a file of a repository. + pub fn is_in_repository(self) -> bool { + matches!(self, Source::Local | Source::Worktree) + } +} + /// High level `git-config` reader and writer. /// /// This is the full-featured implementation that can deserialize, serialize, diff --git a/git-config/tests/file/mutable/section.rs b/git-config/tests/file/mutable/section.rs index 1ae8f8c1290..c2d35f96afe 100644 --- a/git-config/tests/file/mutable/section.rs +++ b/git-config/tests/file/mutable/section.rs @@ -128,23 +128,26 @@ mod push { #[test] fn values_are_escaped() { for (value, expected) in [ - ("a b", "[a]\n\tk = a b"), - (" a b", "[a]\n\tk = \" a b\""), - ("a b\t", "[a]\n\tk = \"a b\\t\""), - (";c", "[a]\n\tk = \";c\""), - ("#c", "[a]\n\tk = \"#c\""), - ("a\nb\n\tc", "[a]\n\tk = a\\nb\\n\\tc"), + ("a b", "$head\tk = a b"), + (" a b", "$head\tk = \" a b\""), + ("a b\t", "$head\tk = \"a b\\t\""), + (";c", "$head\tk = \";c\""), + ("#c", "$head\tk = \"#c\""), + ("a\nb\n\tc", "$head\tk = a\\nb\\n\\tc"), ] { let mut config = git_config::File::default(); let mut section = config.new_section("a", None).unwrap(); section.set_implicit_newline(false); section.push(Key::try_from("k").unwrap(), value); + let expected = expected.replace("$head", &format!("[a]{nl}", nl = section.newline())); assert_eq!(config.to_bstring(), expected); } } } mod set_leading_whitespace { + use bstr::BString; + use std::borrow::Cow; use std::convert::TryFrom; use git_config::parse::section::Key; @@ -155,9 +158,12 @@ mod set_leading_whitespace { fn any_whitespace_is_ok() -> crate::Result { let mut config = git_config::File::default(); let mut section = config.new_section("core", None)?; - section.set_leading_whitespace(cow_str("\n\t").into()); + + let nl = section.newline().to_owned(); + section.set_leading_whitespace(Some(Cow::Owned(BString::from(format!("{nl}\t"))))); section.push(Key::try_from("a")?, "v"); - assert_eq!(config.to_string(), "[core]\n\n\ta = v\n"); + + assert_eq!(config.to_string(), format!("[core]{nl}{nl}\ta = v{nl}")); Ok(()) } diff --git a/git-config/tests/parse/key.rs b/git-config/tests/parse/key.rs new file mode 100644 index 00000000000..af46e647486 --- /dev/null +++ b/git-config/tests/parse/key.rs @@ -0,0 +1,39 @@ +use git_config::parse; + +#[test] +fn missing_dot_is_invalid() { + assert_eq!(parse::key("hello"), None); +} + +#[test] +fn section_name_and_key() { + assert_eq!( + parse::key("core.bare"), + Some(parse::Key { + section_name: "core", + subsection_name: None, + value_name: "bare" + }) + ); +} + +#[test] +fn section_name_subsection_and_key() { + assert_eq!( + parse::key("remote.origin.url"), + Some(parse::Key { + section_name: "remote", + subsection_name: Some("origin"), + value_name: "url" + }) + ); + + assert_eq!( + parse::key("includeIf.gitdir/i:C:\\bare.git.path"), + Some(parse::Key { + section_name: "includeIf", + subsection_name: Some("gitdir/i:C:\\bare.git"), + value_name: "path" + }) + ); +} diff --git a/git-config/tests/parse/mod.rs b/git-config/tests/parse/mod.rs index 36d4df3ba37..659ebc26db9 100644 --- a/git-config/tests/parse/mod.rs +++ b/git-config/tests/parse/mod.rs @@ -4,6 +4,7 @@ use git_config::parse::{Event, Events, Section}; mod error; mod from_bytes; +mod key; mod section; #[test] diff --git a/git-repository/src/config.rs b/git-repository/src/config.rs deleted file mode 100644 index a622f05f994..00000000000 --- a/git-repository/src/config.rs +++ /dev/null @@ -1,217 +0,0 @@ -use crate::{bstr::BString, permission}; - -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error("Could not open repository conifguration file")] - Open(#[from] git_config::file::init::from_paths::Error), - #[error("Cannot handle objects formatted as {:?}", .name)] - UnsupportedObjectFormat { name: BString }, - #[error("The value for '{}' cannot be empty", .key)] - EmptyValue { key: &'static str }, - #[error("Invalid value for 'core.abbrev' = '{}'. It must be between 4 and {}", .value, .max)] - CoreAbbrev { value: BString, max: u8 }, - #[error("Value '{}' at key '{}' could not be decoded as boolean", .value, .key)] - DecodeBoolean { key: String, value: BString }, - #[error(transparent)] - PathInterpolation(#[from] git_config::path::interpolate::Error), -} - -/// Utility type to keep pre-obtained configuration values. -#[derive(Debug, Clone)] -pub(crate) struct Cache { - pub resolved: crate::Config, - /// The hex-length to assume when shortening object ids. If `None`, it should be computed based on the approximate object count. - pub hex_len: Option, - /// true if the repository is designated as 'bare', without work tree. - pub is_bare: bool, - /// The type of hash to use. - pub object_hash: git_hash::Kind, - /// If true, multi-pack indices, whether present or not, may be used by the object database. - pub use_multi_pack_index: bool, - /// The representation of `core.logallrefupdates`, or `None` if the variable wasn't set. - pub reflog: Option, - /// If true, we are on a case-insensitive file system. - #[cfg_attr(not(feature = "git-index"), allow(dead_code))] - pub ignore_case: bool, - /// The path to the user-level excludes file to ignore certain files in the worktree. - #[cfg_attr(not(feature = "git-index"), allow(dead_code))] - pub excludes_file: Option, - /// Define how we can use values obtained with `xdg_config(…)` and its `XDG_CONFIG_HOME` variable. - #[cfg_attr(not(feature = "git-index"), allow(dead_code))] - xdg_config_home_env: permission::env_var::Resource, - /// Define how we can use values obtained with `xdg_config(…)`. and its `HOME` variable. - #[cfg_attr(not(feature = "git-index"), allow(dead_code))] - home_env: permission::env_var::Resource, - // TODO: make core.precomposeUnicode available as well. -} - -mod cache { - use std::{convert::TryFrom, path::PathBuf}; - - use git_config::{path, Boolean, File, Integer}; - - use super::{Cache, Error}; - use crate::{bstr::ByteSlice, permission}; - - impl Cache { - pub fn new( - git_dir: &std::path::Path, - xdg_config_home_env: permission::env_var::Resource, - home_env: permission::env_var::Resource, - git_install_dir: Option<&std::path::Path>, - ) -> Result { - let home = std::env::var_os("HOME") - .map(PathBuf::from) - .and_then(|home| home_env.check(home).ok().flatten()); - // TODO: don't forget to use the canonicalized home for initializing the stacked config. - // like git here: https://github.com/git/git/blob/master/config.c#L208:L208 - let config = { - let mut buf = Vec::with_capacity(512); - File::from_path_with_buf( - &git_dir.join("config"), - &mut buf, - git_config::file::Metadata::from(git_config::Source::Local), - git_config::file::init::Options { - lossy: true, - includes: git_config::file::init::includes::Options::follow( - git_config::path::interpolate::Context { - git_install_dir, - home_dir: None, - home_for_user: None, // TODO: figure out how to configure this - }, - git_config::file::init::includes::conditional::Context { - git_dir: git_dir.into(), - branch_name: None, - }, - ), - }, - )? - }; - - let is_bare = config_bool(&config, "core.bare", false)?; - let use_multi_pack_index = config_bool(&config, "core.multiPackIndex", true)?; - let ignore_case = config_bool(&config, "core.ignoreCase", false)?; - let excludes_file = config - .path("core", None, "excludesFile") - .map(|p| { - p.interpolate(path::interpolate::Context { - git_install_dir, - home_dir: home.as_deref(), - home_for_user: Some(git_config::path::interpolate::home_for_user), - }) - .map(|p| p.into_owned()) - }) - .transpose()?; - let repo_format_version = config - .value::("core", None, "repositoryFormatVersion") - .map_or(0, |v| v.to_decimal().unwrap_or_default()); - let object_hash = (repo_format_version != 1) - .then(|| Ok(git_hash::Kind::Sha1)) - .or_else(|| { - config.string("extensions", None, "objectFormat").map(|format| { - if format.as_ref().eq_ignore_ascii_case(b"sha1") { - Ok(git_hash::Kind::Sha1) - } else { - Err(Error::UnsupportedObjectFormat { - name: format.to_vec().into(), - }) - } - }) - }) - .transpose()? - .unwrap_or(git_hash::Kind::Sha1); - let reflog = config.string("core", None, "logallrefupdates").map(|val| { - (val.eq_ignore_ascii_case(b"always")) - .then(|| git_ref::store::WriteReflog::Always) - .or_else(|| { - git_config::Boolean::try_from(val) - .ok() - .and_then(|b| b.is_true().then(|| git_ref::store::WriteReflog::Normal)) - }) - .unwrap_or(git_ref::store::WriteReflog::Disable) - }); - - let mut hex_len = None; - if let Some(hex_len_str) = config.string("core", None, "abbrev") { - if hex_len_str.trim().is_empty() { - return Err(Error::EmptyValue { key: "core.abbrev" }); - } - if !hex_len_str.eq_ignore_ascii_case(b"auto") { - let value_bytes = hex_len_str.as_ref(); - if let Ok(false) = Boolean::try_from(value_bytes).map(Into::into) { - hex_len = object_hash.len_in_hex().into(); - } else { - let value = Integer::try_from(value_bytes) - .map_err(|_| Error::CoreAbbrev { - value: hex_len_str.clone().into_owned(), - max: object_hash.len_in_hex() as u8, - })? - .to_decimal() - .ok_or_else(|| Error::CoreAbbrev { - value: hex_len_str.clone().into_owned(), - max: object_hash.len_in_hex() as u8, - })?; - if value < 4 || value as usize > object_hash.len_in_hex() { - return Err(Error::CoreAbbrev { - value: hex_len_str.clone().into_owned(), - max: object_hash.len_in_hex() as u8, - }); - } - hex_len = Some(value as usize); - } - } - } - - Ok(Cache { - resolved: config.into(), - use_multi_pack_index, - object_hash, - reflog, - is_bare, - ignore_case, - hex_len, - excludes_file, - xdg_config_home_env, - home_env, - }) - } - - /// Return a path by using the `$XDF_CONFIG_HOME` or `$HOME/.config/…` environment variables locations. - #[cfg_attr(not(feature = "git-index"), allow(dead_code))] - pub fn xdg_config_path( - &self, - resource_file_name: &str, - ) -> Result, git_sec::permission::Error> { - std::env::var_os("XDG_CONFIG_HOME") - .map(|path| (path, &self.xdg_config_home_env)) - .or_else(|| std::env::var_os("HOME").map(|path| (path, &self.home_env))) - .and_then(|(base, permission)| { - let resource = std::path::PathBuf::from(base).join("git").join(resource_file_name); - permission.check(resource).transpose() - }) - .transpose() - } - - /// Return the home directory if we are allowed to read it and if it is set in the environment. - /// - /// We never fail for here even if the permission is set to deny as we `git-config` will fail later - /// if it actually wants to use the home directory - we don't want to fail prematurely. - #[cfg(feature = "git-mailmap")] - pub fn home_dir(&self) -> Option { - std::env::var_os("HOME") - .map(PathBuf::from) - .and_then(|path| self.home_env.check(path).ok().flatten()) - } - } - - fn config_bool(config: &File<'_>, key: &str, default: bool) -> Result { - let (section, key) = key.split_once('.').expect("valid section.key format"); - config - .boolean(section, None, key) - .unwrap_or(Ok(default)) - .map_err(|err| Error::DecodeBoolean { - value: err.input, - key: key.into(), - }) - } -} diff --git a/git-repository/src/config/cache.rs b/git-repository/src/config/cache.rs new file mode 100644 index 00000000000..7f4998c9ba1 --- /dev/null +++ b/git-repository/src/config/cache.rs @@ -0,0 +1,172 @@ +use std::{convert::TryFrom, path::PathBuf}; + +use git_config::{Boolean, Integer}; + +use super::{Cache, Error}; +use crate::config::section::is_trusted; +use crate::{bstr::ByteSlice, permission}; + +impl Cache { + pub fn new( + git_dir_trust: git_sec::Trust, + git_dir: &std::path::Path, + xdg_config_home_env: permission::env_var::Resource, + home_env: permission::env_var::Resource, + git_install_dir: Option<&std::path::Path>, + ) -> Result { + let home = std::env::var_os("HOME") + .map(PathBuf::from) + .and_then(|home| home_env.check(home).ok().flatten()); + // TODO: don't forget to use the canonicalized home for initializing the stacked config. + // like git here: https://github.com/git/git/blob/master/config.c#L208:L208 + let config = { + let mut buf = Vec::with_capacity(512); + git_config::File::from_path_with_buf( + &git_dir.join("config"), + &mut buf, + git_config::file::Metadata::from(git_config::Source::Local).with(git_dir_trust), + git_config::file::init::Options { + lossy: !cfg!(debug_assertions), + includes: git_config::file::init::includes::Options::follow( + interpolate_context(git_install_dir, home.as_deref()), + git_config::file::init::includes::conditional::Context { + git_dir: git_dir.into(), + branch_name: None, + }, + ), + }, + )? + }; + + let is_bare = config_bool(&config, "core.bare", false)?; + let use_multi_pack_index = config_bool(&config, "core.multiPackIndex", true)?; + let ignore_case = config_bool(&config, "core.ignoreCase", false)?; + let excludes_file = config + .path_filter("core", None, "excludesFile", &mut is_trusted) + .map(|p| { + p.interpolate(interpolate_context(git_install_dir, home.as_deref())) + .map(|p| p.into_owned()) + }) + .transpose()?; + let repo_format_version = config + .value::("core", None, "repositoryFormatVersion") + .map_or(0, |v| v.to_decimal().unwrap_or_default()); + let object_hash = (repo_format_version != 1) + .then(|| Ok(git_hash::Kind::Sha1)) + .or_else(|| { + config.string("extensions", None, "objectFormat").map(|format| { + if format.as_ref().eq_ignore_ascii_case(b"sha1") { + Ok(git_hash::Kind::Sha1) + } else { + Err(Error::UnsupportedObjectFormat { + name: format.to_vec().into(), + }) + } + }) + }) + .transpose()? + .unwrap_or(git_hash::Kind::Sha1); + let reflog = config.string("core", None, "logallrefupdates").map(|val| { + (val.eq_ignore_ascii_case(b"always")) + .then(|| git_ref::store::WriteReflog::Always) + .or_else(|| { + git_config::Boolean::try_from(val) + .ok() + .and_then(|b| b.is_true().then(|| git_ref::store::WriteReflog::Normal)) + }) + .unwrap_or(git_ref::store::WriteReflog::Disable) + }); + + let mut hex_len = None; + if let Some(hex_len_str) = config.string("core", None, "abbrev") { + if hex_len_str.trim().is_empty() { + return Err(Error::EmptyValue { key: "core.abbrev" }); + } + if !hex_len_str.eq_ignore_ascii_case(b"auto") { + let value_bytes = hex_len_str.as_ref(); + if let Ok(false) = Boolean::try_from(value_bytes).map(Into::into) { + hex_len = object_hash.len_in_hex().into(); + } else { + let value = Integer::try_from(value_bytes) + .map_err(|_| Error::CoreAbbrev { + value: hex_len_str.clone().into_owned(), + max: object_hash.len_in_hex() as u8, + })? + .to_decimal() + .ok_or_else(|| Error::CoreAbbrev { + value: hex_len_str.clone().into_owned(), + max: object_hash.len_in_hex() as u8, + })?; + if value < 4 || value as usize > object_hash.len_in_hex() { + return Err(Error::CoreAbbrev { + value: hex_len_str.clone().into_owned(), + max: object_hash.len_in_hex() as u8, + }); + } + hex_len = Some(value as usize); + } + } + } + + Ok(Cache { + resolved: config.into(), + use_multi_pack_index, + object_hash, + reflog, + is_bare, + ignore_case, + hex_len, + excludes_file, + xdg_config_home_env, + home_env, + }) + } + + /// Return a path by using the `$XDF_CONFIG_HOME` or `$HOME/.config/…` environment variables locations. + #[cfg_attr(not(feature = "git-index"), allow(dead_code))] + pub fn xdg_config_path( + &self, + resource_file_name: &str, + ) -> Result, git_sec::permission::Error> { + std::env::var_os("XDG_CONFIG_HOME") + .map(|path| (path, &self.xdg_config_home_env)) + .or_else(|| std::env::var_os("HOME").map(|path| (path, &self.home_env))) + .and_then(|(base, permission)| { + let resource = std::path::PathBuf::from(base).join("git").join(resource_file_name); + permission.check(resource).transpose() + }) + .transpose() + } + + /// Return the home directory if we are allowed to read it and if it is set in the environment. + /// + /// We never fail for here even if the permission is set to deny as we `git-config` will fail later + /// if it actually wants to use the home directory - we don't want to fail prematurely. + pub fn home_dir(&self) -> Option { + std::env::var_os("HOME") + .map(PathBuf::from) + .and_then(|path| self.home_env.check(path).ok().flatten()) + } +} + +pub(crate) fn interpolate_context<'a>( + git_install_dir: Option<&'a std::path::Path>, + home_dir: Option<&'a std::path::Path>, +) -> git_config::path::interpolate::Context<'a> { + git_config::path::interpolate::Context { + git_install_dir, + home_dir, + home_for_user: Some(git_config::path::interpolate::home_for_user), // TODO: figure out how to configure this + } +} + +fn config_bool(config: &git_config::File<'_>, key: &str, default: bool) -> Result { + let (section, key) = key.split_once('.').expect("valid section.key format"); + config + .boolean(section, None, key) + .unwrap_or(Ok(default)) + .map_err(|err| Error::DecodeBoolean { + value: err.input, + key: key.into(), + }) +} diff --git a/git-repository/src/config/mod.rs b/git-repository/src/config/mod.rs new file mode 100644 index 00000000000..6cad58abba5 --- /dev/null +++ b/git-repository/src/config/mod.rs @@ -0,0 +1,61 @@ +use crate::{bstr::BString, permission, Repository}; + +mod cache; +mod snapshot; + +/// A platform to access configuration values as read from disk. +/// +/// Note that these values won't update even if the underlying file(s) change. +pub struct Snapshot<'repo> { + pub(crate) repo: &'repo Repository, +} + +pub(crate) mod section { + pub fn is_trusted(meta: &git_config::file::Metadata) -> bool { + meta.trust == git_sec::Trust::Full || !meta.source.is_in_repository() + } +} + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Could not open repository conifguration file")] + Open(#[from] git_config::file::init::from_paths::Error), + #[error("Cannot handle objects formatted as {:?}", .name)] + UnsupportedObjectFormat { name: BString }, + #[error("The value for '{}' cannot be empty", .key)] + EmptyValue { key: &'static str }, + #[error("Invalid value for 'core.abbrev' = '{}'. It must be between 4 and {}", .value, .max)] + CoreAbbrev { value: BString, max: u8 }, + #[error("Value '{}' at key '{}' could not be decoded as boolean", .value, .key)] + DecodeBoolean { key: String, value: BString }, + #[error(transparent)] + PathInterpolation(#[from] git_config::path::interpolate::Error), +} + +/// Utility type to keep pre-obtained configuration values. +#[derive(Debug, Clone)] +pub(crate) struct Cache { + pub resolved: crate::Config, + /// The hex-length to assume when shortening object ids. If `None`, it should be computed based on the approximate object count. + pub hex_len: Option, + /// true if the repository is designated as 'bare', without work tree. + pub is_bare: bool, + /// The type of hash to use. + pub object_hash: git_hash::Kind, + /// If true, multi-pack indices, whether present or not, may be used by the object database. + pub use_multi_pack_index: bool, + /// The representation of `core.logallrefupdates`, or `None` if the variable wasn't set. + pub reflog: Option, + /// If true, we are on a case-insensitive file system. + #[cfg_attr(not(feature = "git-index"), allow(dead_code))] + pub ignore_case: bool, + /// The path to the user-level excludes file to ignore certain files in the worktree. + #[cfg_attr(not(feature = "git-index"), allow(dead_code))] + pub excludes_file: Option, + /// Define how we can use values obtained with `xdg_config(…)` and its `XDG_CONFIG_HOME` variable. + #[cfg_attr(not(feature = "git-index"), allow(dead_code))] + xdg_config_home_env: permission::env_var::Resource, + /// Define how we can use values obtained with `xdg_config(…)`. and its `HOME` variable. + home_env: permission::env_var::Resource, + // TODO: make core.precomposeUnicode available as well. +} diff --git a/git-repository/src/config/snapshot.rs b/git-repository/src/config/snapshot.rs new file mode 100644 index 00000000000..53b1c11a159 --- /dev/null +++ b/git-repository/src/config/snapshot.rs @@ -0,0 +1,101 @@ +use crate::bstr::BStr; +use crate::config::cache::interpolate_context; +use crate::config::Snapshot; +use std::borrow::Cow; +use std::fmt::{Debug, Formatter}; + +/// Access configuration values, frozen in time, using a `key` which is a `.` separated string of up to +/// three tokens, namely `section_name.[subsection_name.]value_name`, like `core.bare` or `remote.origin.url`. +/// +/// Note that single-value methods always return the last value found, which is the one set most recently in the +/// hierarchy of configuration files, aka 'last one wins'. +impl<'repo> Snapshot<'repo> { + /// Return the boolean at `key`, or `None` if there is no such value or if the value can't be interpreted as + /// boolean. + /// + /// For a non-degenerating version, use [`try_boolean(…)`][Self::try_boolean()]. + /// + /// Note that this method takes the most recent value at `key` even if it is from a file with reduced trust. + pub fn boolean(&self, key: &str) -> Option { + self.try_boolean(key).and_then(Result::ok) + } + + /// Like [`boolean()`][Self::boolean()], but it will report an error if the value couldn't be interpreted as boolean. + pub fn try_boolean(&self, key: &str) -> Option> { + let key = git_config::parse::key(key)?; + self.repo + .config + .resolved + .boolean(key.section_name, key.subsection_name, key.value_name) + } + + /// Return the resolved integer at `key`, or `None` if there is no such value or if the value can't be interpreted as + /// integer or exceeded the value range. + /// + /// For a non-degenerating version, use [`try_integer(…)`][Self::try_integer()]. + /// + /// Note that this method takes the most recent value at `key` even if it is from a file with reduced trust. + pub fn integer(&self, key: &str) -> Option { + self.try_integer(key).and_then(Result::ok) + } + + /// Like [`integer()`][Self::integer()], but it will report an error if the value couldn't be interpreted as boolean. + pub fn try_integer(&self, key: &str) -> Option> { + let key = git_config::parse::key(key)?; + self.repo + .config + .resolved + .integer(key.section_name, key.subsection_name, key.value_name) + } + + /// Return the string at `key`, or `None` if there is no such value. + /// + /// Note that this method takes the most recent value at `key` even if it is from a file with reduced trust. + pub fn string(&self, key: &str) -> Option> { + let key = git_config::parse::key(key)?; + self.repo + .config + .resolved + .string(key.section_name, key.subsection_name, key.value_name) + } + + /// Return the trusted and fully interpolated path at `key`, or `None` if there is no such value + /// or if no value was found in a trusted file. + /// An error occours if the path could not be interpolated to its final value. + pub fn trusted_path( + &self, + key: &str, + ) -> Option, git_config::path::interpolate::Error>> { + let key = git_config::parse::key(key)?; + let path = self.repo.config.resolved.path_filter( + key.section_name, + key.subsection_name, + key.value_name, + &mut crate::config::section::is_trusted, + )?; + + let install_dir = self.repo.install_dir().ok(); + let home = self.repo.config.home_dir(); + Some(path.interpolate(interpolate_context(install_dir.as_deref(), home.as_deref()))) + } +} + +/// Utilities and additional access +impl<'repo> Snapshot<'repo> { + /// Returns the underlying configuration implementation for a complete API, despite being a little less convenient. + /// + /// It's expected that more functionality will move up depending on demand. + pub fn plumbing(&self) -> &git_config::File<'static> { + &self.repo.config.resolved + } +} + +impl Debug for Snapshot<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + if cfg!(debug_assertions) { + f.write_str(&self.repo.config.resolved.to_string()) + } else { + Debug::fmt(&self.repo.config.resolved, f) + } + } +} diff --git a/git-repository/src/lib.rs b/git-repository/src/lib.rs index ea100128695..587e27c0b58 100644 --- a/git-repository/src/lib.rs +++ b/git-repository/src/lib.rs @@ -261,6 +261,11 @@ pub fn open(directory: impl Into) -> Result, options: open::Options) -> Result { + ThreadSafeRepository::open_opts(directory, options).map(Into::into) +} + /// pub mod permission { /// diff --git a/git-repository/src/open.rs b/git-repository/src/open.rs index e2f34292e37..eaee0cb6b13 100644 --- a/git-repository/src/open.rs +++ b/git-repository/src/open.rs @@ -1,7 +1,6 @@ use std::path::PathBuf; use git_features::threading::OwnShared; -use git_sec::Trust; use crate::{Permissions, ThreadSafeRepository}; @@ -66,6 +65,7 @@ pub struct Options { pub(crate) object_store_slots: git_odb::store::init::Slots, pub(crate) replacement_objects: ReplacementObjects, pub(crate) permissions: Permissions, + pub(crate) git_dir_trust: Option, } #[derive(Default, Clone)] @@ -117,6 +117,19 @@ impl Options { self } + /// Set the trust level of the `.git` directory we are about to open. + /// + /// This can be set manually to force trust even though otherwise it might + /// not be fully trusted, leading to limitations in how configuration files + /// are interpreted. + /// + /// If not called explicitly, it will be determined by looking at its + /// ownership via [`git_sec::Trust::from_path_ownership()`]. + pub fn with(mut self, trust: git_sec::Trust) -> Self { + self.git_dir_trust = trust.into(); + self + } + /// Open a repository at `path` with the options set so far. pub fn open(self, path: impl Into) -> Result { ThreadSafeRepository::open_opts(path, self) @@ -124,17 +137,19 @@ impl Options { } impl git_sec::trust::DefaultForLevel for Options { - fn default_for_level(level: Trust) -> Self { + fn default_for_level(level: git_sec::Trust) -> Self { match level { git_sec::Trust::Full => Options { object_store_slots: Default::default(), replacement_objects: Default::default(), permissions: Permissions::all(), + git_dir_trust: git_sec::Trust::Full.into(), }, git_sec::Trust::Reduced => Options { object_store_slots: git_odb::store::init::Slots::Given(32), // limit resource usage replacement_objects: ReplacementObjects::Disable, // don't be tricked into seeing manufactured objects permissions: Default::default(), + git_dir_trust: git_sec::Trust::Reduced.into(), }, } } @@ -166,7 +181,7 @@ impl ThreadSafeRepository { /// `options` for fine-grained control. /// /// Note that you should use [`crate::discover()`] if security should be adjusted by ownership. - pub fn open_opts(path: impl Into, options: Options) -> Result { + pub fn open_opts(path: impl Into, mut options: Options) -> Result { let (path, kind) = { let path = path.into(); match git_discover::is_git(&path) { @@ -179,6 +194,9 @@ impl ThreadSafeRepository { }; let (git_dir, worktree_dir) = git_discover::repository::Path::from_dot_git_dir(path, kind).into_repository_and_work_tree_directories(); + if options.git_dir_trust.is_none() { + options.git_dir_trust = git_sec::Trust::from_path_ownership(&git_dir)?.into(); + } ThreadSafeRepository::open_from_paths(git_dir, worktree_dir, options) } @@ -208,24 +226,28 @@ impl ThreadSafeRepository { .into_repository_and_work_tree_directories(); let worktree_dir = worktree_dir.or(overrides.worktree_dir); - let trust = git_sec::Trust::from_path_ownership(&git_dir)?; - let options = trust_map.into_value_by_level(trust); + let git_dir_trust = git_sec::Trust::from_path_ownership(&git_dir)?; + let options = trust_map.into_value_by_level(git_dir_trust); ThreadSafeRepository::open_from_paths(git_dir, worktree_dir, options) } pub(crate) fn open_from_paths( git_dir: PathBuf, mut worktree_dir: Option, - Options { + options: Options, + ) -> Result { + let Options { + git_dir_trust, object_store_slots, - replacement_objects, + ref replacement_objects, permissions: Permissions { - git_dir: git_dir_perm, - env, + git_dir: ref git_dir_perm, + ref env, }, - }: Options, - ) -> Result { - if *git_dir_perm != git_sec::ReadWrite::all() { + } = options; + let git_dir_trust = git_dir_trust.expect("trust must be been determined by now"); + + if **git_dir_perm != git_sec::ReadWrite::all() { // TODO: respect `save.directory`, which needs more support from git-config to do properly. return Err(Error::UnsafeGitDir { path: git_dir }); } @@ -238,6 +260,7 @@ impl ThreadSafeRepository { .map(|cd| git_dir.join(cd)); let common_dir_ref = common_dir.as_deref().unwrap_or(&git_dir); let config = crate::config::Cache::new( + git_dir_trust, common_dir_ref, env.xdg_config_home.clone(), env.home.clone(), @@ -290,16 +313,6 @@ impl ThreadSafeRepository { }) .unwrap_or_default(); - // used when spawning new repositories off this one when following worktrees - let linked_worktree_options = Options { - object_store_slots, - replacement_objects, - permissions: Permissions { - env, - git_dir: git_dir_perm, - }, - }; - Ok(ThreadSafeRepository { objects: OwnShared::new(git_odb::Store::at_opts( common_dir_ref.join("objects"), @@ -314,7 +327,8 @@ impl ThreadSafeRepository { refs, work_tree: worktree_dir, config, - linked_worktree_options, + // used when spawning new repositories off this one when following worktrees + linked_worktree_options: options, }) } } diff --git a/git-repository/src/repository/config.rs b/git-repository/src/repository/config.rs new file mode 100644 index 00000000000..728222f9312 --- /dev/null +++ b/git-repository/src/repository/config.rs @@ -0,0 +1,13 @@ +use crate::config; + +impl crate::Repository { + /// Return a snapshot of the configuration as seen upon opening the repository. + pub fn config_snapshot(&self) -> config::Snapshot<'_> { + config::Snapshot { repo: self } + } + + /// The options used to open the repository. + pub fn open_options(&self) -> &crate::open::Options { + &self.linked_worktree_options + } +} diff --git a/git-repository/src/repository/location.rs b/git-repository/src/repository/location.rs index 0e23cbc0a1a..3f2e990f981 100644 --- a/git-repository/src/repository/location.rs +++ b/git-repository/src/repository/location.rs @@ -65,4 +65,11 @@ impl crate::Repository { pub fn git_dir(&self) -> &std::path::Path { self.refs.git_dir() } + + /// The trust we place in the git-dir, with lower amounts of trust causing access to configuration to be limited. + pub fn git_dir_trust(&self) -> git_sec::Trust { + self.linked_worktree_options + .git_dir_trust + .expect("definitely set by now") + } } diff --git a/git-repository/src/repository/mod.rs b/git-repository/src/repository/mod.rs index b0b6fb36b72..c2e0edaf69a 100644 --- a/git-repository/src/repository/mod.rs +++ b/git-repository/src/repository/mod.rs @@ -44,6 +44,8 @@ mod worktree; /// Various permissions for parts of git repositories. pub(crate) mod permissions; +mod config; + mod init; mod location; diff --git a/git-repository/src/repository/worktree.rs b/git-repository/src/repository/worktree.rs index 5826e0fde2b..cef2f54a088 100644 --- a/git-repository/src/repository/worktree.rs +++ b/git-repository/src/repository/worktree.rs @@ -44,7 +44,7 @@ impl crate::Repository { /// Note that it might be the one that is currently open if this repository dosn't point to a linked worktree. /// Also note that the main repo might be bare. pub fn main_repo(&self) -> Result { - crate::open(self.common_dir()) + crate::ThreadSafeRepository::open_opts(self.common_dir(), self.linked_worktree_options.clone()).map(Into::into) } /// Return the currently set worktree if there is one, acting as platform providing a validated worktree base path. diff --git a/git-repository/tests/fixtures/generated-archives/make_config_repo.tar.xz b/git-repository/tests/fixtures/generated-archives/make_config_repo.tar.xz new file mode 100644 index 00000000000..940cd3bb580 --- /dev/null +++ b/git-repository/tests/fixtures/generated-archives/make_config_repo.tar.xz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:68233a1e37e4d423532370d975297116f967c9b5c0044d66534e7074f92acf77 +size 9152 diff --git a/git-repository/tests/fixtures/make_config_repo.sh b/git-repository/tests/fixtures/make_config_repo.sh new file mode 100644 index 00000000000..ff58143975f --- /dev/null +++ b/git-repository/tests/fixtures/make_config_repo.sh @@ -0,0 +1,16 @@ +#!/bin/bash +set -eu -o pipefail + +git init -q + +cat <>.git/config +[a] + bool = on + bad-bool = zero + int = 42 + int-overflowing = 9999999999999g + relative-path = ./something + absolute-path = /etc/man.conf + bad-user-path = ~noname/repo + single-string = hello world +EOF diff --git a/git-repository/tests/git.rs b/git-repository/tests/git.rs index 8c9280a29c4..75a31893a0d 100644 --- a/git-repository/tests/git.rs +++ b/git-repository/tests/git.rs @@ -2,17 +2,17 @@ use git_repository::{Repository, ThreadSafeRepository}; type Result = std::result::Result>; -fn repo(name: &str) -> crate::Result { +fn repo(name: &str) -> Result { let repo_path = git_testtools::scripted_fixture_repo_read_only(name)?; Ok(ThreadSafeRepository::open(repo_path)?) } -fn named_repo(name: &str) -> crate::Result { +fn named_repo(name: &str) -> Result { let repo_path = git_testtools::scripted_fixture_repo_read_only(name)?; Ok(ThreadSafeRepository::open(repo_path)?.to_thread_local()) } -fn repo_rw(name: &str) -> crate::Result<(Repository, tempfile::TempDir)> { +fn repo_rw(name: &str) -> Result<(Repository, tempfile::TempDir)> { let repo_path = git_testtools::scripted_fixture_repo_writable(name)?; Ok(( ThreadSafeRepository::discover(repo_path.path())?.to_thread_local(), @@ -20,11 +20,11 @@ fn repo_rw(name: &str) -> crate::Result<(Repository, tempfile::TempDir)> { )) } -fn basic_repo() -> crate::Result { +fn basic_repo() -> Result { repo("make_basic_repo.sh").map(|r| r.to_thread_local()) } -fn basic_rw_repo() -> crate::Result<(Repository, tempfile::TempDir)> { +fn basic_rw_repo() -> Result<(Repository, tempfile::TempDir)> { repo_rw("make_basic_repo.sh") } diff --git a/git-repository/tests/repository/config.rs b/git-repository/tests/repository/config.rs new file mode 100644 index 00000000000..9a49aedab01 --- /dev/null +++ b/git-repository/tests/repository/config.rs @@ -0,0 +1,55 @@ +use crate::named_repo; +use git_repository as git; +use std::path::Path; + +#[test] +fn access_values() { + for trust in [git_sec::Trust::Full, git_sec::Trust::Reduced] { + let repo = named_repo("make_config_repo.sh").unwrap(); + let repo = git::open_opts(repo.git_dir(), repo.open_options().clone().with(trust)).unwrap(); + + let config = repo.config_snapshot(); + + assert_eq!(config.boolean("core.bare"), Some(false)); + assert_eq!(config.boolean("a.bad-bool"), None); + assert_eq!(config.try_boolean("core.bare"), Some(Ok(false))); + assert!(matches!(config.try_boolean("a.bad-bool"), Some(Err(_)))); + + assert_eq!(config.integer("a.int"), Some(42)); + assert_eq!(config.integer("a.int-overflowing"), None); + assert_eq!(config.integer("a.int-overflowing"), None); + assert!(config.try_integer("a.int-overflowing").expect("present").is_err()); + + assert_eq!( + config.string("a.single-string").expect("present").as_ref(), + "hello world" + ); + + assert_eq!(config.boolean("core.missing"), None); + assert_eq!(config.try_boolean("core.missing"), None); + + let relative_path_key = "a.relative-path"; + if trust == git_sec::Trust::Full { + assert_eq!( + config + .trusted_path(relative_path_key) + .expect("exists") + .expect("no error"), + Path::new("./something") + ); + assert_eq!( + config + .trusted_path("a.absolute-path") + .expect("exists") + .expect("no error"), + Path::new("/etc/man.conf") + ); + assert!(config.trusted_path("a.bad-user-path").expect("exists").is_err()); + } else { + assert!( + config.trusted_path(relative_path_key).is_none(), + "trusted paths need full trust" + ); + } + } +} diff --git a/git-repository/tests/repository/mod.rs b/git-repository/tests/repository/mod.rs index 5d1a173dcf3..dbc256ac1e3 100644 --- a/git-repository/tests/repository/mod.rs +++ b/git-repository/tests/repository/mod.rs @@ -1,3 +1,4 @@ +mod config; mod object; mod reference; mod remote;